]> git.openstreetmap.org Git - nominatim-ui.git/commitdiff
Handle all page changes via page store (#70)
authorSarah Hoffmann <lonvia@denofr.de>
Sat, 13 Feb 2021 11:11:08 +0000 (12:11 +0100)
committerGitHub <noreply@github.com>
Sat, 13 Feb 2021 11:11:08 +0000 (12:11 +0100)
* move serialize_form and clean_up_url_parameters to UrlSubmitForm
* switch pages by setting page store
* simplify parameter handling in reverse bar
* add documentation for refresh_page() function
* scroll to top of details page on reload

12 files changed:
src/App.svelte
src/components/DetailsLink.svelte
src/components/PageLink.svelte
src/components/ReverseBar.svelte
src/components/ReverseLink.svelte
src/components/SearchBar.svelte
src/components/UrlSubmitForm.svelte
src/lib/api_utils.js
src/lib/stores.js
src/pages/DetailsPage.svelte
src/pages/ReversePage.svelte
src/pages/SearchPage.svelte

index 54cad5ddad2e59a75151ae5da1a59310b9d2af68..ec1ca0bb2bc4b47d4912d29c6215dea80e14065c 100644 (file)
@@ -19,7 +19,7 @@
 </script>
 
 <!-- deal with back-button and other user action -->
-<svelte:window on:popstate={refresh_page} />
+<svelte:window on:popstate={() => refresh_page()} />
 
 <Header/>
 {#if view === 'search'}
index 51a0c8ce8004cd0c49d1a724a3f78d30bfcea7de..fbeb4f1a03ddcb577cbfabeff8f801885ae44122 100644 (file)
@@ -4,7 +4,8 @@
   export let extra_classes = '';
   export let feature = null;
 
-  let url_params = '';
+  let url_params = new URLSearchParams();
+  let href = 'details.html';
 
   function formatShortOSMType(sType) {
     if (sType === 'node') return 'N';
   }
 
   function handleClick() {
-    window.history.pushState([], '', 'details.html' + url_params);
-    refresh_page();
+    refresh_page('details', url_params);
   }
 
   $: {
+    let new_params = new URLSearchParams();
+
     if (feature !== null && feature.osm_type) {
-      let param = '?osmtype=';
-      if (feature.osm_type.length == 1) {
-        param += encodeURIComponent(feature.osm_type);
+      if (feature.osm_type.length === 1) {
+        new_params.set('osmtype', feature.osm_type);
       } else {
-        param += formatShortOSMType(feature.osm_type);
+        new_params.set('osmtype', formatShortOSMType(feature.osm_type));
       }
-      param += '&osmid=' + encodeURIComponent(feature.osm_id);
+
+      new_params.set('osmid', feature.osm_id);
+
       if (feature.class) {
-        param += '&class=' + encodeURIComponent(feature.class);
+        new_params.set('class', feature.class);
       } else if (feature.category) {
-        param += '&class=' + encodeURIComponent(feature.category);
+        new_params.set('class', feature.category);
       }
-      url_params = param
-    } else {
-        url_params = '';
     }
- }
+
+    url_params = new_params;
+  }
+
+  $: {
+    let param_str = url_params.toString();
+    href = 'details.html' + (param_str ? '?' : '') + param_str;
+  }
 </script>
 
-<a on:click|preventDefault|stopPropagation={handleClick} href="details.html{url_params}" class={extra_classes}><slot></slot></a>
+<a on:click|preventDefault|stopPropagation={handleClick} href={href} class={extra_classes}><slot></slot></a>
index d70c3f0c7c691aad2ed4855dbb441543391c3aca..9205c2f726470c8e97a9adf3cccb218a7baa8fed 100644 (file)
@@ -4,8 +4,7 @@ import { refresh_page } from '../lib/stores.js';
 export let page;
 
 function handleClick() {
-  window.history.pushState([], '', page + '.html');
-  refresh_page();
+  refresh_page(page);
 }
 </script>
 
index 53d7bffe2187fb2768ef19da61b0a64282e5f1a9..62227c84e583f9c4a46b2cdb774f959c46f071c9 100644 (file)
@@ -4,28 +4,29 @@
   import PageLink from '../components/PageLink.svelte';
 
   import { zoomLevels } from '../lib/helpers.js';
-  import { map_store } from '../lib/stores.js';
+  import { map_store, refresh_page } from '../lib/stores.js';
 
-  export let api_request_params = {};
+  export let lat = '';
+  export let lon = '';
+  export let zoom = '';
 
-  map_store.subscribe(map => {
-    if (!map) { return; }
+  function gotoCoordinates(newlat, newlon, newzoom) {
+    let params = new URLSearchParams();
+    params.set('lat', newlat);
+    params.set('lon', newlon);
+    params.set('zoom', newzoom || zoom);
+    refresh_page('reverse', params);
+  }
 
-    map.on('click', function (e) {
-      document.querySelector('input[name=lat]').value = e.latlng.lat.toFixed(5);
-      document.querySelector('input[name=lon]').value = e.latlng.wrap().lng.toFixed(5);
-      document.querySelector('form').submit();
-    });
+  map_store.subscribe(map => {
+    if (map) {
+      map.on('click', (e) => {
+        let coords = e.latlng.wrap();
+        gotoCoordinates(coords.lat.toFixed(5), coords.lng.toFixed(5));
+      });
+    }
   });
 
-  function handleSwitchCoords() {
-    let lat = document.querySelector('input[name=lat]').value;
-    let lon = document.querySelector('input[name=lon]').value;
-    document.querySelector('input[name=lat]').value = lon;
-    document.querySelector('input[name=lon]').value = lat;
-    document.querySelector('form').submit();
-  }
-
   // common mistake is to copy&paste latitude and longitude into the 'lat' search box
   function maybeSplitLatitude(e) {
     var coords_split = e.target.value.split(',');
@@ -38,7 +39,7 @@
 </script>
 
 <div class="top-bar">
-  <UrlSubmitForm>
+  <UrlSubmitForm page="reverse">
     <div class="form-group">
       <input name="format" type="hidden" value="html">
       <label for="reverse-lat">lat</label>
              type="text"
              class="form-control form-control-sm"
              placeholder="latitude"
-             value="{api_request_params.lat || ''}"
+             bind:value={lat}
              on:change={maybeSplitLatitude} />
       <a id="switch-coords"
-         on:click|preventDefault|stopPropagation={handleSwitchCoords}
+         on:click|preventDefault|stopPropagation={() => gotoCoordinates(lon, lat)}
          class="btn btn-outline-secondary btn-sm"
          title="switch lat and lon">&lt;&gt;</a>
       <label for="reverse-lon">lon</label>
              type="text"
              class="form-control form-control-sm"
              placeholder="longitude"
-             value="{api_request_params.lon || ''}" />
+             bind:value={lon} />
       <label for="reverse-zoom">max zoom</label>
-      <select id="reverse-zoom" name="zoom" class="form-control form-control-sm" value="{api_request_params.zoom}">
-        <option value="" selected={!api_request_params.zoom}>---</option>
+      <select id="reverse-zoom" name="zoom" class="form-control form-control-sm" bind:value={zoom}>
+        <option value="">---</option>
         {#each zoomLevels() as zoomTitle, i}
-          <option value="{i}" selected={i === api_request_params.zoom}>{i} - {zoomTitle}</option>
+          <option value="{i}">{i} - {zoomTitle}</option>
         {/each}
       </select>
       <button type="submit" class="btn btn-primary btn-sm mx-1">
index 5289999246413aa4f31d1756e99a0e155c11a0af..f6657deb5257cd6ae9b230ddb64d2c62a83361a5 100644 (file)
@@ -5,27 +5,28 @@ export let lat;
 export let lon;
 export let zoom = null;
 
-let params = '';
+let params = new URLSearchParams();
+let href = 'reverse.html';
 
 $: {
+  let new_params = new URLSearchParams();
+
   if (lat && lon) {
-    let new_params = '?lat=' + encodeURIComponent(lat);
-    new_params += '&lon=' + encodeURIComponent(lon);
+    new_params.set('lat', lat);
+    new_params.set('lon', lon);
 
     if (zoom) {
-      new_params += '&zoom=' + encodeURIComponent(zoom);
+      new_params.set('zoom', zoom);
     }
-
-    params = new_params;
-  } else {
-    params = '';
   }
+
+  params = new_params;
 }
 
-function handleClick() {
-  window.history.pushState([], '', 'reverse.html' + params);
-  refresh_page();
+$: {
+  let param_str = params.toString();
+  href = 'reverse.html' + (param_str ? '?' : '') + param_str;
 }
 </script>
 
-<a on:click|preventDefault|stopPropagation={handleClick} href="reverse.html{params}"><slot></slot></a>
+<a on:click|preventDefault|stopPropagation={() => refresh_page('reverse', params)} href={href}><slot></slot></a>
index dca6161ad7b187a3924a70d593056ce04d30f277..d0890a9076e30b24be567b6d7b97ad80a28dd5db 100644 (file)
@@ -95,7 +95,7 @@
   </ul>
   <div class="tab-content p-2">
     <div class="tab-pane" class:active={!bStructuredSearch} id="simple" role="tabpanel">
-      <UrlSubmitForm>
+      <UrlSubmitForm page="search">
         <input id="q"
                name="q"
                type="text"
       </UrlSubmitForm>
     </div>
     <div class="tab-pane" class:active={bStructuredSearch} id="structured" role="tabpanel">
-      <UrlSubmitForm>
+      <UrlSubmitForm page="search">
         <input name="street" type="text" class="form-control form-control-sm mr-1"
                placeholder="House number/Street"
                value="{api_request_params.street || ''}" />
index f902f70dc3cba8d62ccbd4d6480edf27ffead0f5..72864b19676ee49715e155ffde8a31ee92f6ce39 100644 (file)
@@ -1,18 +1,24 @@
 <script>
-  import { serialize_form, clean_up_url_parameters } from '../lib/api_utils.js';
   import { refresh_page } from '../lib/stores.js';
 
-  function handleSubmit(event) {
-    event.preventDefault();
+  export let page;
 
-    var target_url = serialize_form(event.target);
-    target_url = clean_up_url_parameters(target_url);
+  function serialize_form(form) {
+    var params = new URLSearchParams();
 
-    window.history.pushState({}, '', '?' + target_url);
-    refresh_page();
+    Array.prototype.slice.call(form.elements).forEach(function (field) {
+      if (!field.name || field.disabled || ['submit', 'button'].indexOf(field.type) > -1) return;
+
+      if (['checkbox', 'radio'].indexOf(field.type) > -1 && !field.checked) return;
+      if (typeof field.value === 'undefined' || field.value === '') return;
+
+      params.set(field.name, field.value);
+    });
+
+    return params;
   }
 </script>
 
-<form on:submit={handleSubmit} class="form-inline" role="search" accept-charset="UTF-8" action="">
+<form on:submit|preventDefault={(e) => refresh_page(page, serialize_form(e.target))} class="form-inline" role="search" accept-charset="UTF-8" action="">
     <slot></slot>
 </form>
index 0317e13c031fccc7471f4f9edc3029710cf546cc..70acda6c858689b6bd821b6ce896c294f928a8c6 100644 (file)
@@ -32,44 +32,6 @@ function generate_nominatim_api_url(endpoint_name, params) {
          }).join('&');
 }
 
-/*!
- * Serialize all form data into a SearchParams string
- * (c) 2020 Chris Ferdinandi, MIT License, https://gomakethings.com
- * @param  {Node}   form The form to serialize
- * @return {String}      The serialized form data
- */
-export function serialize_form(form) {
-  var arr = [];
-  Array.prototype.slice.call(form.elements).forEach(function (field) {
-    if (!field.name || field.disabled || ['submit', 'button'].indexOf(field.type) > -1) return;
-    // if (field.type === 'select-multiple') {
-    //   Array.prototype.slice.call(field.options).forEach(function (option) {
-    //     if (!option.selected) return;
-    //     arr.push(encodeURIComponent(field.name) + '=' + encodeURIComponent(option.value));
-    //   });
-    //   return;
-    // }
-    if (['checkbox', 'radio'].indexOf(field.type) > -1 && !field.checked) return;
-    if (typeof field.value === 'undefined') return;
-    arr.push(encodeURIComponent(field.name) + '=' + encodeURIComponent(field.value));
-  });
-  return arr.join('&');
-}
-
-
-// remove any URL paramters with empty values
-// '&empty=&filled=value' => 'filled=value'
-export function clean_up_url_parameters(url) {
-  var url_params = new URLSearchParams(url);
-  var to_delete = []; // deleting inside loop would skip iterations
-  url_params.forEach(function (value, key) {
-    if (value === '') to_delete.push(key);
-  });
-  for (var i = 0; i < to_delete.length; i += 1) {
-    url_params.delete(to_delete[i]);
-  }
-  return url_params.toString();
-}
 
 function clean_up_parameters(params) {
   // `&a=&b=&c=1` => '&c=1'
index 0707eeff4ca4c1ae9342cf22ce6b7e591e3c9738..a7a524a3b4f738cf3f5ef4abec407dba552ccf42 100644 (file)
@@ -3,16 +3,38 @@ import { writable } from 'svelte/store';
 export const map_store = writable();
 export const results_store = writable();
 export const last_api_request_url_store = writable();
-export const page = writable({ count: 0 });
+export const page = writable();
 
-export function refresh_page() {
-  let pagename = window.location.pathname.replace('.html', '').replace(/^.*\//, '');
+/**
+ * Update the global page state.
+ *
+ * When called without a parameter, then the current window.location is
+ * parsed and the page state is set accordingly. Otherwise the page state
+ * is set from the parameters. 'pagename' is the overall subpage (without
+ * .html extension). 'params' must be an URLSearchParams object and contain
+ * the requested query parameters. It may also be omitted completely for a
+ * link without query parameters.
+ */
+export function refresh_page(pagename, params) {
+  if (typeof pagename === 'undefined') {
+    pagename = window.location.pathname.replace('.html', '').replace(/^.*\//, '');
 
-  if (['search', 'reverse', 'details', 'deletable', 'polygons'].indexOf(pagename) === -1) {
-    pagename = 'search';
+    if (['search', 'reverse', 'details', 'deletable', 'polygons'].indexOf(pagename) === -1) {
+      pagename = 'search';
+    }
+
+    params = new URLSearchParams(window.location.search);
+  } else {
+    if (typeof params === 'undefined') {
+      params = new URLSearchParams();
+    }
+
+    let param_str = params.toString();
+    if (param_str) {
+      param_str = '?' + param_str;
+    }
+    window.history.pushState([], '', pagename + '.html' + param_str);
   }
 
-  // Add a counter here to make sure the store change is triggered
-  // everytime we refresh, not just when the page changes.
-  page.update(function (v) { return { tab: pagename, count: v.count + 1 }; });
+  page.set({ tab: pagename, params: params });
 }
index e9c74828add4584e16df51bb42abb317a88847b6..40a92f6cf7f9c20137c847156bbdbe6fef90855e 100644 (file)
@@ -1,5 +1,4 @@
 <script>
-  import { onMount, onDestroy } from 'svelte';
   import { fetch_from_api, update_html_title } from '../lib/api_utils.js';
   import { page } from '../lib/stores.js';
 
@@ -18,9 +17,7 @@
   let base_url = window.location.search;
   let current_result;
 
-  function loaddata() {
-    var search_params = new URLSearchParams(window.location.search);
-
+  function loaddata(search_params) {
     var api_request_params = {
       place_id: search_params.get('place_id'),
       osmtype: search_params.get('osmtype'),
@@ -43,6 +40,7 @@
       }
 
       fetch_from_api('details', api_request_params, function (data) {
+        window.scrollTo(0, 0)
         if (data.error) {
           errorResponse = data;
           current_result = undefined;
     }
   }
 
-  let page_subscription;
-  onMount(() => { page_subscription = page.subscribe(loaddata); });
-  onDestroy(() => { page_subscription(); });
-
+  $: {
+    let pageinfo = $page;
+    if (pageinfo.tab === 'details') {
+      loaddata(pageinfo.params);
+    }
+  }
 </script>
 
 {#if errorResponse}
index 3f49c0a1a03de246ec7924724c7b244075ae3134..a990646a392ff3b61bef96869a3230466a4739a4 100644 (file)
@@ -1,6 +1,4 @@
 <script>
-  import { onMount, onDestroy } from 'svelte';
-
   import { page, results_store } from '../lib/stores.js';
   import { get_config_value } from '../lib/config_reader.js';
   import { fetch_from_api, update_html_title } from '../lib/api_utils.js';
@@ -13,9 +11,7 @@
   let current_result;
   let position_marker; // what the user searched for
 
-  function loaddata() {
-    let search_params = new URLSearchParams(window.location.search);
-
+  function loaddata(search_params) {
     update_html_title();
 
     api_request_params = {
                             + api_request_params.lon);
         document.querySelector('input[name=lat]').focus();
       });
+    } else {
+      results_store.set(undefined);
     }
   }
 
-  let page_subscription;
-  onMount(() => { page_subscription = page.subscribe(loaddata); });
-  onDestroy(() => { page_subscription(); });
+  $: {
+    let pageinfo = $page;
+    if (pageinfo.tab === 'reverse') {
+      loaddata(pageinfo.params);
+    }
+  }
 </script>
 
-<ReverseBar api_request_params={api_request_params} />
+<ReverseBar {...api_request_params} />
 
 <div id="content">
   <div class="sidebar">
index caa4e9687974d51ec8a630c0c1247cb194102418..ae9aec8b84cdd780e8ef702ebb17fdd8b14ee647 100644 (file)
@@ -1,6 +1,4 @@
 <script>
-  import { onMount, onDestroy } from 'svelte';
-
   import { page, results_store } from '../lib/stores.js';
   import { get_config_value } from '../lib/config_reader.js';
   import { fetch_from_api, update_html_title } from '../lib/api_utils.js';
@@ -13,9 +11,7 @@
   let bStructuredSearch;
   let current_result;
 
-  function loaddata() {
-    let search_params = new URLSearchParams(window.location.search);
-
+  function loaddata(search_params) {
     update_html_title();
 
     api_request_params = {
 
         document.querySelector('input[name=q]').focus();
       });
+    } else {
+      results_store.set(undefined);
     }
   }
 
-  let page_subscription;
-  onMount(() => { page_subscription = page.subscribe(loaddata); });
-  onDestroy(() => { page_subscription(); });
+  $: {
+    let pageinfo = $page;
+    if (pageinfo.tab === 'search') {
+      loaddata(pageinfo.params);
+    }
+  }
 </script>
 
 <SearchBar api_request_params={api_request_params} bStructuredSearch={bStructuredSearch} />