From ac081591f971864671408fc08de95a64f01fffeb Mon Sep 17 00:00:00 2001
From: xaralis <filip.varecha@fragaria.cz>
Date: Mon, 23 May 2022 10:27:34 +0100
Subject: [PATCH] feat(district,maps_utils): improve map back&foward navigation

---
 .../district_geo_feature_collection_page.html |  2 +-
 .../maps_utils/geo-feature-collection.js      | 85 +++++++++----------
 2 files changed, 42 insertions(+), 45 deletions(-)

diff --git a/district/templates/district/district_geo_feature_collection_page.html b/district/templates/district/district_geo_feature_collection_page.html
index ba9f8dd0..fc4db176 100644
--- a/district/templates/district/district_geo_feature_collection_page.html
+++ b/district/templates/district/district_geo_feature_collection_page.html
@@ -68,7 +68,7 @@
       </div>
 
       <section>
-        <h2 class="head-heavy-base mt-8 mb-4">{{ page.category_list_title|default:"Přehled dle kategorií" }}</h2>
+        <h2 id="kategorie" class="head-heavy-base mt-8 mb-4">{{ page.category_list_title|default:"Přehled dle kategorií" }}</h2>
 
         <div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8">
           {% for category, features in features_by_category %}
diff --git a/maps_utils/static/maps_utils/geo-feature-collection.js b/maps_utils/static/maps_utils/geo-feature-collection.js
index f1471f3b..800ff933 100644
--- a/maps_utils/static/maps_utils/geo-feature-collection.js
+++ b/maps_utils/static/maps_utils/geo-feature-collection.js
@@ -327,7 +327,7 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
              */
             const onEachFeature = (feature, layer) => {
                 const markerPosLatLng = [];
-                const onClick = (evt) => this.zoomToLayer(layer);
+                const onClick = (evt) => this.zoomToLayer(layer, true);
 
                 const markerForPolyCoords = (coords) => {
                     // Find pole of inaccessibility (not centroid) for the polygon
@@ -382,7 +382,7 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
                         addMarker(feature, pos, onClick);
                         // Bind click event on the layer.
                         layer.on({
-                            click: (evt) => this.zoomToLayer(evt.target),
+                            click: (evt) => this.zoomToLayer(evt.target, true),
                         });
                     });
                 }
@@ -409,48 +409,56 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
                 this.map.panTo(this.layer.getBounds().getCenter());
             }
 
-            // If hash is present when starting, locate the item and zoom to it.
-            if (window.location.hash) {
-                this.zoomToItemBySlugUrl(window.location.hash.substring(1));
-            }
+            // Initial check - open item detail if in URL.
+            this.onUserNavigation();
+
+            // Listen to URL changes when user triggers browser navigation.
+            window.addEventListener("popstate", this.onUserNavigation);
 
-            // Listen to hashbang changes when user users browser history navigation.
-            window.addEventListener("hashchange", this.onHashChange, false);
         },
         /**
-         * Stores item's url in the hashbang.
+         * Stores item's slug in URL `item` query param.
          *
          * @param {Object} item
          */
-        setUrlHash(item) {
+        pushToUrl(item) {
+            const url = new URL(window.location);
+
             if (item === null) {
-                history.replaceState({}, null, "#");
+                url.searchParams.delete("item");
+                history.pushState({}, "", url);
             } else {
-                history.pushState({}, item.title, "#" + item.slug);
+                url.searchParams.set("item", item.slug);
+                history.pushState({}, "", url);
             }
         },
         /**
-         * Called when URL hash changes. Will display detail
+         * Called when user navigates. Will display detail
          * of corresponding item if such exist.
          *
          * @param {Event} evt
          */
-        onHashChange(evt) {
-            const urlBits = evt.newURL.split("#");
-
-            if (urlBits.length == 2 && urlBits[1]) {
-                this.zoomToItemBySlugUrl(urlBits[1]);
-            } else {
-                this.closeItemInfo();
+        onUserNavigation(evt) {
+            const urlParams = new URLSearchParams(window.location.search);
+
+            // If query is present when starting, locate the item and zoom to it.
+            if (urlParams.has("item")) {
+                this.zoomToFeature(urlParams.get("item"), false);
+            } else if (this.currentItem) {
+                this.closeItemInfo(false);
             }
         },
         /**
          * Hide current item detail, drop it from URL.
+         * @param {Boolean} updateUrl whether to push new state to history.
          */
-        closeItemInfo() {
+        closeItemInfo(updateUrl = true) {
             if (this.currentItem) {
                 this.currentItem = null;
-                this.setUrlHash(null);
+
+                if (updateUrl) {
+                    this.pushToUrl(null);
+                }
             }
         },
         /**
@@ -464,55 +472,44 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
          * Zoom to a point.
          *
          * @param {L.Latlng} Latlng
-         * @param {Boolean} pushState whether to push new state to history.
+         * @param {Object} feature
+         * @param {Boolean} updateUrl whether to push new state to history.
          */
         zoomToPoint(latlng, feature, updateUrl = true) {
             this.map.panTo(latlng);
             this.currentItem = feature.properties;
 
             if (updateUrl) {
-                this.setUrlHash(this.currentItem);
+                this.pushToUrl(this.currentItem);
             }
         },
         /**
          * Zoom to a detail of a layer.
          *
          * @param {L.Layer} layer
-         * @param {Boolean} pushState whether to push new state to history.
+         * @param {Boolean} updateUrl whether to push new state to history.
          */
         zoomToLayer(layer, updateUrl = true) {
             this.map.fitBounds(layer.getBounds());
             this.currentItem = layer.feature.properties;
 
             if (updateUrl) {
-                this.setUrlHash(this.currentItem);
-            }
-        },
-        /**
-         * Zoom to a cateogry item. Wraps `zoomTo`.
-         *
-         * @param {Object} category
-         * @param {Object} item
-         */
-        zoomToCategoryItem(category, item) {
-            const layer = Object.values(this.layer._layers).find(
-                (l) => l.feature.properties.id == item.properties.id
-            );
-            if (layer) {
-                this.zoomToLayer(layer);
+                this.pushToUrl(this.currentItem);
             }
         },
         /**
-         * Zoom to a item with corresponding slugified URL.
+         * Zoom to a feature with corresponding slugified URL.
+         * Only extracted to make things DRY, will not update the URL!
          *
          * @param {String} slugUrl
+         * @param {Boolean} updateUrl whether to push new state to history.
          */
-        zoomToItemBySlugUrl(slugUrl) {
+        zoomToFeature(slugUrl, updateUrl = true) {
             const layer = Object.values(this.layer._layers).find(
                 (l) => l.feature.properties.slug == slugUrl
             );
             if (layer) {
-                this.zoomToLayer(layer, false);
+                this.zoomToLayer(layer, updateUrl);
             }
         },
         /**
@@ -546,7 +543,7 @@ const GeoFeatureCollection = Vue.component("GeoFeatureCollection", {
                             </div>
                             <ul v-show="category.expanded" :class="{'mb-2': index != categoryCount - 1}">
                                 <li v-for="item in category.items" :key="item.properties.title">
-                                    <button @click="zoomToCategoryItem(category, item)" class="text-left leading-tight text-xs" style="max-width: 17em;">
+                                    <button @click="zoomToFeature(item.properties.slug)" class="text-left leading-tight text-xs" style="max-width: 17em;">
                                         <span v-if="item.properties.index" class="rounded-full inline-flex items-center justify-center bg-grey-125 font-bold text-center text-2xs w-4 h-4 mr-1">{{ item.properties.index }}</span>
                                         <span>{{ item.properties.title }}</span>
                                     </button>
-- 
GitLab