From 05ff4bd43087ef5ccee50ac64cfe0188c10b0e85 Mon Sep 17 00:00:00 2001 From: The Ripper Date: Wed, 13 Oct 2021 19:39:48 +0200 Subject: [PATCH] Favorite list for search (geocode control) (#452) * search favorites Adds the ability to save search terms, use this for autoexpand during typing in searchbox or select from whole list * moved class vars to contructor Co-authored-by: Karl Schweiger --- css/style.css | 62 +++++++++ js/plugin/Search.js | 304 +++++++++++++++++++++++++++++++++++++++++++- locales/en.json | 8 ++ 3 files changed, 373 insertions(+), 1 deletion(-) diff --git a/css/style.css b/css/style.css index de47a03..461e975 100644 --- a/css/style.css +++ b/css/style.css @@ -854,6 +854,68 @@ table.dataTable.display tbody tr:hover.selected { border-radius: 0; } + +/** + * Autocompleter styles + */ +.leaflet-control-geocoder-form.stayvisible { display: inline-block; } +.leaflet-control-geocoder-alternatives{ + /* display: block; */ + position: absolute; + background-color: white; + left: 2rem; +} + +.leaflet-control-geocoder-alternatives{ + background-color: rgb(255, 255, 255); +} + +.autocomplete-container { + position:absolute; + left: 2rem; + width: calc( 100% - 3rem ); + z-index: 100; +} + +.autocomplete-select-container { + max-height: 15vh; + overflow-y: auto; +} + +.autocomplete-container .autocomplete-select button{ + text-align: left; + padding-left: 1rem; + padding-right: 1rem; + width: 100%; +} + +#search-fav-menu-toggle { + text-align: right; + padding-right: 2rem; + line-height: 100%; + cursor: pointer; +} + +#search-fav-menu-toggle > span { + font-weight: bold; + line-height: 0; + font-size: 1.2rem; +} + +.autocomplete-container.filtered .autocomplete-select button{ display: none; } +.autocomplete-container.filtered .autocomplete-select button.match{ display: inherit; } + +@media (max-width: 320.98px) { + .leaflet-control-geocoder-form > input[type=text] { + max-width: 50vw; + } + + .leaflet-control-geocoder-alternatives { + max-width: 65vw; + } +} + + @media (max-width: 575.98px) { .modal-fullscreen-sm-down { width: 100vw; diff --git a/js/plugin/Search.js b/js/plugin/Search.js index e810867..7af0987 100644 --- a/js/plugin/Search.js +++ b/js/plugin/Search.js @@ -22,7 +22,8 @@ BR.Search = class extends L.Control.Geocoder { L.DomEvent.addListener(document, 'keydown', this._keydownListener, this); } - + + markGeocode(result) { this._map.fitBounds(result.geocode.bbox, { maxZoom: 17, @@ -51,4 +52,305 @@ BR.Search = class extends L.Control.Geocoder { e.preventDefault(); } } + + /* Search favorites handling */ + onAdd(map) { + + if(!BR.Util.localStorageAvailable()) + return super.onAdd(map); + + let container=super.onAdd(map); + new SearchFavorites(this,container); + return container; + + } }; + + +class SearchFavorites { + + constructor(geocoder,container) { + //because eslint does not support instance var declaration + this.searchInput=undefined; + this.autocompleteContainer=undefined; + this.autocompleteSelect=undefined; + this.autocompleteMenu=undefined; + this.geocoderForm=undefined; + this.geocoder=undefined; + this.favElements=undefined; + this.isFiltered=true; + this.arFavitems=[]; + this.arFavitemsLC=[]; + + this.geocoder=geocoder; + this.searchInput=$(container).find(".leaflet-control-geocoder-form input[type=text]"); + this.searchInput.after(` + + + `); + + //otherwise parent catches event and click is never fired + $(container).find(".leaflet-control-geocoder-form .search-fav-ctrls").on("mousedown", + (e) => { e.stopPropagation();e.preventDefault(); }); + + $(container).find(".leaflet-control-geocoder-form .search-fav-ctrls").click((e) => this.onCtrlsClick(e)); + + $(container).find(".leaflet-control-geocoder-form").append(` +
+
+
+
+
+
+ ... +
+
+ + + +
+
+ `); + + this.autocompleteContainer=$(container).find("#search-autocomplete-container"); + this.autocompleteSelect=this.autocompleteContainer.find("#search-autocomplete-select"); + this.autocompleteMenu=this.autocompleteContainer.find("#autocomplete-btngroup"); + this.geocoderForm=$(container).find(".leaflet-control-geocoder-form"); + + this.autocompleteContainer.on("mousedown", + (e) => { e.stopPropagation();e.preventDefault(); }); + + this.autocompleteContainer.find("#search-fav-menu-toggle").on("mousedown touchend", + (e) => { e.stopPropagation();e.preventDefault();}); + + this.autocompleteContainer.find("#search-fav-menu-toggle").on("click touchend", + (e) => { + e.stopPropagation();e.preventDefault(); + this.autocompleteMenu.collapse('toggle'); + }); + + this.autocompleteContainer.find(".autocomplete-select-container").on("wheel", + (e) => { e.stopPropagation(); }); + + this.autocompleteContainer.on("shown.bs.collapse", (e) => this.geocoderForm.addClass("stayvisible")); + this.autocompleteContainer.on("hidden.bs.collapse", (e) => { + this.geocoderForm.removeClass("stayvisible"); + this.autocompleteMenu.collapse('hide'); + + }); + + + + this.autocompleteSelect.click((e) => this.onFavItemClicked(e)); + + this.autocompleteContainer.find(".autocomplete-menu").click((e) => this.onFavMenuClicked(e)); + this.autocompleteContainer.find("#search-fav-file").on("change",(e) => this.onImportFavFile(e)); + + let strFavitems=localStorage['searchFavItems']; + if(strFavitems) { + this.arFavitems=JSON.parse(strFavitems); + this.arFavitemsLC = this.arFavitems.map(x => x.toLowerCase()); //copy to Lowercase + } + + this.updateFavList(); + + this.searchInput.on("keyup",(e) => this.onInput(e)); + + } + + updateFavList() { + if(this.arFavitems.length > 0){ + let opts=this.arFavitems.join('`); + } + else + this.autocompleteSelect.empty(); + + this.favElements=this.autocompleteSelect.find("button"); + } + + appendFavorite(strFav) { + this.arFavitems.push(strFav); + + this.arFavitems=[... new Set(this.arFavitems)]; //remove duplicates + this.arFavitems.sort(); + this.arFavitemsLC = this.arFavitems.map(x => x.toLowerCase()); //copy to Lowercase + + localStorage['searchFavItems']=JSON.stringify(this.arFavitems); + this.updateFavList(); + } + + deleteFavorite(strFav) { + let pos=this.arFavitems.indexOf(strFav); + if(pos >= 0) { + this.arFavitems.splice(pos, 1); + this.arFavitems=[... new Set(this.arFavitems)]; //remove duplicates + this.arFavitems.sort(); + this.arFavitemsLC = this.arFavitems.map(x => x.toLowerCase()); //copy to Lowercase + + localStorage['searchFavItems']=JSON.stringify(this.arFavitems); + + this.updateFavList(); + } + + } + + onInput(e) { + if(e.keyCode == 13) { + this.autocompleteContainer.collapse('hide'); + return; + } + + if(e.keyCode <=45 && e.keyCode != 8) return; + + let srch=this.searchInput.val().toLowerCase(); + + if(!srch || srch.length < 2) { + this.autocompleteContainer.collapse('hide'); + return; + } + + if(!this.isFiltered) { + this.autocompleteContainer.addClass("filtered"); + this.isFiltered=true; + } + + let matches=false; + this.favElements.removeClass("match"); + this.arFavitemsLC.forEach((val,idx) => { + if(val.indexOf(srch) != -1) { + this.favElements.eq(idx).addClass("match"); + matches=true; + } + }); + + if(matches) + this.autocompleteContainer.collapse('show'); + else + this.autocompleteContainer.collapse('hide'); + } + + + onCtrlsClick(e) { + e.stopPropagation();e.preventDefault(); + switch(e.target.id) { + case "search-fav-add": + this.appendFavorite(this.searchInput.val()); + break; + + case "search-fav-expand": + if(this.autocompleteContainer.hasClass("show")) { + if(this.isFiltered) { + this.autocompleteContainer.removeClass("filtered"); + this.isFiltered=false; + } + else + { + this.autocompleteContainer.collapse('hide') + this.autocompleteContainer.addClass("filtered"); + this.isFiltered=true; + } + } + else + { + this.autocompleteContainer.removeClass("filtered"); + this.isFiltered=false; + this.autocompleteContainer.collapse('show'); + } + break; + + default: + break; + } + } + + onFavItemClicked(e) { + e.stopPropagation();e.preventDefault(); + + if($(e.target).hasClass("favitem")) { + this.autocompleteContainer.collapse('hide'); + this.geocoder.setQuery(e.target.innerText); + this.searchInput.focus(); + + this.geocoder._keydown({keyCode: 13 }); + } + else if($(e.target).hasClass("del-favitem")) { + this.deleteFavorite($(e.target).closest("button").text()); + } + } + + onFavMenuClicked(e) { + + switch(e.target.id) { + case "search-fav-deleteall": + + if(confirm(i18next.t("searchfav.ask_removeall")) ) { + this.arFavitems=[]; + this.arFavitemsLC=[]; + localStorage['searchFavItems']=JSON.stringify(this.arFavitems); + this.updateFavList(); + } + break; + + case "search-fav-export": + let exp=JSON.stringify(this.arFavitems,null,2); + const blob = new Blob([exp], { + type: 'application/json;charset=utf-8', + }); + const objectUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = objectUrl; + link.download = 'SearchFavorites.json'; + link.click(); + break; + + case "search-fav-import": + $(e.target).find("input[type=file]").click(); + e.preventDefault(); + break; + + default: + break; + + } + } + + onImportFavFile(e) { + if(!e.target.files[0]) return; + + let r=new FileReader(); + r.onload=(f) => { + let importFavItems=JSON.parse(f.target.result); + this.arFavitems=this.arFavitems.concat(importFavItems); + this.arFavitems=[... new Set(this.arFavitems)]; //remove duplicates + this.arFavitems.sort(); + this.arFavitemsLC = this.arFavitems.map(x => x.toLowerCase()); //copy to Lowercase + localStorage['searchFavItems']=JSON.stringify(this.arFavitems); + this.updateFavList(); + + }; + + r.readAsText(e.target.files[0]); + + e.target.value=''; + } +} + diff --git a/locales/en.json b/locales/en.json index 7c3b784..d9dc498 100644 --- a/locales/en.json +++ b/locales/en.json @@ -296,6 +296,14 @@ "tracklayer": "Track Layer", "tuning": "Tuning" }, + "searchfav": { + "addfavorite": "add as favorite", + "openfavorites": "open favorite list", + "removeall": "delete all favorites", + "export": "export favorites to file", + "import": "import favorites from file", + "ask_removeall": "Do you want to delete all search favorites?" + }, "warning": { "cannot-get-route": "Error getting route URL", "invalid-route-from": "Start marker is too far from a route.",