From bd7fba6efc8a71293830f3c7665dee91225201c0 Mon Sep 17 00:00:00 2001 From: AliyanH Date: Wed, 15 Mar 2023 16:33:59 -0400 Subject: [PATCH 1/5] initial searchbar setup --- src/mapml-viewer.js | 1 + src/mapml/control/SearchControl.js | 106 +++++++++++++++++++++++++++++ src/mapml/index.js | 4 ++ 3 files changed, 111 insertions(+) create mode 100644 src/mapml/control/SearchControl.js diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 51644ad8d..2696dced8 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -354,6 +354,7 @@ export class MapViewer extends HTMLElement { totalSize += 49; this._fullScreenControl = M.fullscreenButton().addTo(this._map); } + this._searchBar = M.searchBar().addTo(this._map); } // Sets controls by hiding/unhiding them based on the map attribute diff --git a/src/mapml/control/SearchControl.js b/src/mapml/control/SearchControl.js new file mode 100644 index 000000000..3791258e3 --- /dev/null +++ b/src/mapml/control/SearchControl.js @@ -0,0 +1,106 @@ +export var SearchBar = L.Control.extend({ + + options: { + // @option collapsed: Boolean = true + // If `true`, the control will be collapsed into an icon and expanded on mouse hover, touch, or keyboard activation. + collapsed: true, + position: 'topright' + }, + + onAdd: function(map) { + this._initLayout(); + this._map = map; + + return this._container; + }, + + onRemove: function(map) { + // Nothing to do here + }, + + // Expand the control container if collapsed. + expand() { + console.log("expand called"); + this._container.classList.add('leaflet-control-layers-expanded'); + this._section.style.height = null; + return this; + }, + + // @method collapse(): this + // Collapse the control container if expanded. + collapse() { + console.log("collapse called"); + this._container.classList.remove('leaflet-control-layers-expanded'); + return this; + }, + + _initLayout() { + const className = 'leaflet-control-layers', + container = this._container = L.DomUtil.create('div', className), + collapsed = this.options.collapsed; + + // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released + container.setAttribute('aria-haspopup', true); + + L.DomEvent.disableClickPropagation(container); + L.DomEvent.disableScrollPropagation(container); + + const section = this._section = L.DomUtil.create('div', `${className}-list`); + + if (collapsed) { + this._map.on('click', this.collapse, this); + + L.DomEvent.on(container, { + mouseenter: this._expandSafely, + mouseleave: this.collapse + }, this); + } + + const link = this._layersLink = L.DomUtil.create('a', `${className}-toggle`, container); + link.href = '#'; + link.title = 'Layers'; + link.setAttribute('role', 'button'); + + L.DomEvent.on(link, { + keydown(e) { + if (e.code === 'Enter') { + this._expandSafely(); + } + }, + // Certain screen readers intercept the key event and instead send a click event + click(e) { + L.DomEvent.preventDefault(e); + this._expandSafely(); + } + }, this); + + if (!collapsed) { + this.expand(); + } + + // for testing + this._img = L.DomUtil.create('img', `${className}-img`, section); + this._img.src = './dist/images/layers-2x.png'; + this._img.style.width = '30px'; + + this._input = L.DomUtil.create('input', `${className}-input`, section); + this._input.type = 'text'; + this._input.size = '15'; + + container.appendChild(section); + }, + + _expandSafely() { + const section = this._section; + L.DomEvent.on(section, 'click', L.DomEvent.preventDefault); + this.expand(); + setTimeout(() => { + L.DomEvent.off(section, 'click', L.DomEvent.preventDefault); + }); + } +}); + + +export var searchBar = function (options) { + return new SearchBar(options); + }; \ No newline at end of file diff --git a/src/mapml/index.js b/src/mapml/index.js index 938a47deb..b14a019b9 100644 --- a/src/mapml/index.js +++ b/src/mapml/index.js @@ -54,6 +54,7 @@ import { Util } from './utils/Util'; import { ReloadButton, reloadButton } from './control/ReloadButton'; import { FullscreenButton, fullscreenButton } from './control/FullscreenButton'; import {attributionControl} from "./control/AttributionControl"; +import { SearchBar, searchBar } from "./control/SearchControl"; import { Crosshair, crosshair } from "./layers/Crosshair"; import { Feature, feature } from "./features/feature"; import { FeatureRenderer, featureRenderer } from './features/featureRenderer'; @@ -659,6 +660,9 @@ M.fullscreenButton = fullscreenButton; M.attributionControl = attributionControl; +M.SearchBar = SearchBar; +M.searchBar = searchBar; + M.StaticTileLayer = StaticTileLayer; M.staticTileLayer = staticTileLayer; From 9dcae7c909e4d8be8ca61f8bf80a110f12986a88 Mon Sep 17 00:00:00 2001 From: AliyanH Date: Mon, 27 Mar 2023 16:40:12 -0400 Subject: [PATCH 2/5] Make search control a bit more stable and functioning --- index.html | 13 +++ src/layer.js | 5 +- src/mapml-viewer.js | 19 +++- src/mapml.css | 15 ++- src/mapml/control/SearchControl.js | 148 +++++++++++++++++++++++++++-- src/mapml/layers/MapMLLayer.js | 4 +- src/mapml/layers/TemplatedLayer.js | 6 +- test/e2e/core/touchDevice.test.js | 2 +- 8 files changed, 194 insertions(+), 18 deletions(-) diff --git a/index.html b/index.html index e98405f41..8dd91433b 100644 --- a/index.html +++ b/index.html @@ -75,6 +75,19 @@ A pleasing map of Canada + + + + + + + + + + diff --git a/src/layer.js b/src/layer.js index c2a7b3f9f..698174a3f 100644 --- a/src/layer.js +++ b/src/layer.js @@ -104,6 +104,8 @@ export class MapLayer extends HTMLElement { if (this._layerControl && !this.hidden) { this._layerControl.addOrUpdateOverlay(this._layer, this.label); } + // Update search control to update searchable layers + this._layer._map.options.mapEl._searchBar._updateSearchableLayers(); }, {once:true}); //listener stops listening after event occurs once //if map is already created then dispatch createmap event, allowing layer to be built if(this.parentNode._map)this.parentNode.dispatchEvent(new CustomEvent('createmap')); @@ -204,7 +206,8 @@ export class MapLayer extends HTMLElement { total++; layer._extent._mapExtents[i].removeAttribute("disabled"); layer._extent._mapExtents[i].disabled = false; - if(!(layer._extent._mapExtents[i].templatedLayer._templates[j].layer.isVisible)){ + if(!(layer._extent._mapExtents[i].templatedLayer._templates[j].layer && + layer._extent._mapExtents[i].templatedLayer._templates[j].layer.isVisible)){ count++; layer._extent._mapExtents[i].setAttribute("disabled", ""); layer._extent._mapExtents[i].disabled = true; diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 2696dced8..9787ae290 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -134,7 +134,7 @@ export class MapViewer extends HTMLElement { this._controlsList = new DOMTokenList( this.getAttribute("controlslist"), this, "controlslist", - ["noreload","nofullscreen","nozoom","nolayer"] + ["noreload","nofullscreen","nozoom","nolayer","nosearch"] ); // the dimension attributes win, if they're there. A map does not @@ -354,7 +354,9 @@ export class MapViewer extends HTMLElement { totalSize += 49; this._fullScreenControl = M.fullscreenButton().addTo(this._map); } - this._searchBar = M.searchBar().addTo(this._map); + if (!this._searchBar) { + this._searchBar = M.searchBar().addTo(this._map); + } } // Sets controls by hiding/unhiding them based on the map attribute @@ -373,12 +375,17 @@ export class MapViewer extends HTMLElement { this._setControlsVisibility("layercontrol",true); this._setControlsVisibility("reload",true); this._setControlsVisibility("zoom",true); + this._setControlsVisibility("search",true); } _showControls() { this._setControlsVisibility("fullscreen",false); this._setControlsVisibility("layercontrol",false); this._setControlsVisibility("reload",false); this._setControlsVisibility("zoom",false); + // show search control, if any layer is searchable + if (this._searchBar?.searchableLayers.length > 0) { + this._setControlsVisibility("search",false); + } // prune the controls shown if necessary // this logic could be embedded in _showControls @@ -399,6 +406,9 @@ export class MapViewer extends HTMLElement { case 'nozoom': this._setControlsVisibility("zoom",true); break; + case 'nosearch': + this._setControlsVisibility("search",true); + break; } }); } @@ -432,6 +442,11 @@ export class MapViewer extends HTMLElement { container = this._layerControl._container; } break; + case "search": + if (this._searchBar) { + container = this._searchBar._container; + } + break; } if (container) { if (hide) { diff --git a/src/mapml.css b/src/mapml.css index 3f747e39d..bf6e2a3e8 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -103,11 +103,17 @@ cursor: default; } -.leaflet-control-layers-toggle { +.leaflet-control-layers-toggle, +.leaflet-control-search-toggle { + display:block; width: 44px !important; height: 44px !important; } +.leaflet-control-layers-expanded .leaflet-control-search-toggle { + display:none; +} + .leaflet-bar a, .leaflet-control-layers, .mapml-reload-button { @@ -242,6 +248,13 @@ background-size: 34px; } +.leaflet-control .leaflet-control-search-toggle { + background-image: url("data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='40' height='40' viewBox='0 0 500.00001 500.00001' id='svg4162' version='1.1' inkscape:version='0.92.3 (2405546, 2018-03-11)' sodipodi:docname='Search_Icon.svg'%3E%3Cdefs id='defs4164'/%3E%3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='0.954' inkscape:cx='250' inkscape:cy='250' inkscape:document-units='px' inkscape:current-layer='layer1' showgrid='false' units='px' inkscape:window-width='1366' inkscape:window-height='706' inkscape:window-x='-8' inkscape:window-y='-8' inkscape:window-maximized='1'/%3E%3Cmetadata id='metadata4167'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/%3E%3Cdc:title/%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg inkscape:label='Layer 1' inkscape:groupmode='layer' id='layer1' transform='translate(0,-552.36216)'%3E%3Cg id='g1400' transform='translate(-4.3609793,-7.6704785)'%3E%3Cpath inkscape:connector-curvature='0' id='path4714' d='M 232.83952,614.96702 A 154.04816,154.04794 0 0 0 78.79153,769.01382 154.04816,154.04794 0 0 0 232.83952,923.06184 154.04816,154.04794 0 0 0 386.88751,769.01382 154.04816,154.04794 0 0 0 232.83952,614.96702 Z m 0,26.77613 A 129.95832,127.2707 0 0 1 362.79832,769.01382 129.95832,127.2707 0 0 1 232.83952,896.28449 129.95832,127.2707 0 0 1 102.88194,769.01382 129.95832,127.2707 0 0 1 232.83952,641.74315 Z' style='opacity:1;fill:%232b0000;fill-opacity:1;stroke:none;stroke-opacity:1'/%3E%3Crect ry='18.08342' rx='33.249443' transform='matrix(0.65316768,0.7572133,-0.60689051,0.79478545,0,0)' y='319.55432' x='794.8775' height='36.16684' width='173.02675' id='rect4721' style='opacity:1;fill:%232b0000;fill-opacity:1;stroke:none;stroke-opacity:1'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); + background-size: 34px; + background-repeat: no-repeat; + background-position: center; +} + /* Revert Leaflet styles that are causing misalignment. */ .leaflet-control-layers-selector { margin-top: revert; diff --git a/src/mapml/control/SearchControl.js b/src/mapml/control/SearchControl.js index 3791258e3..2f9f12ed2 100644 --- a/src/mapml/control/SearchControl.js +++ b/src/mapml/control/SearchControl.js @@ -10,6 +10,10 @@ export var SearchBar = L.Control.extend({ onAdd: function(map) { this._initLayout(); this._map = map; + L.DomEvent.on(this._container.getElementsByTagName("a")[0], 'keydown', this._focusSearchBar, this._container); + + // get a list of layers that are searchable + this.searchableLayers = []; return this._container; }, @@ -20,7 +24,6 @@ export var SearchBar = L.Control.extend({ // Expand the control container if collapsed. expand() { - console.log("expand called"); this._container.classList.add('leaflet-control-layers-expanded'); this._section.style.height = null; return this; @@ -29,8 +32,9 @@ export var SearchBar = L.Control.extend({ // @method collapse(): this // Collapse the control container if expanded. collapse() { - console.log("collapse called"); - this._container.classList.remove('leaflet-control-layers-expanded'); + if (this._suggestion.childElementCount === 0) { + this._container.classList.remove('leaflet-control-layers-expanded'); + } return this; }, @@ -56,7 +60,7 @@ export var SearchBar = L.Control.extend({ }, this); } - const link = this._layersLink = L.DomUtil.create('a', `${className}-toggle`, container); + const link = this._layersLink = L.DomUtil.create('a', `leaflet-control-search-toggle`, container); link.href = '#'; link.title = 'Layers'; link.setAttribute('role', 'button'); @@ -78,18 +82,144 @@ export var SearchBar = L.Control.extend({ this.expand(); } - // for testing - this._img = L.DomUtil.create('img', `${className}-img`, section); - this._img.src = './dist/images/layers-2x.png'; - this._img.style.width = '30px'; - this._input = L.DomUtil.create('input', `${className}-input`, section); + this._input.setAttribute("list", "suggestions"); // connect to datalist this._input.type = 'text'; this._input.size = '15'; + this._input.style.margin='10px'; + this._input.onkeyup = (e)=> { + if (e.code === 'Enter') { + this.search(); + } else { + this.suggest(); + } + }; + + this._createSuggestions(); container.appendChild(section); }, + search() { + let input = this._container.querySelector('.leaflet-control-layers-input'); + this._updateSearchableLayers(input.value); + + // TODO - search through all layers when multiple searchable layers present, + // currently hardcoded to only search the first searchable layer + fetch(this.searchableLayers[0], { + "headers": { + "accept": "text/mapml", + }, + "method": "GET", + "mode": "cors", + }) + .then((response) => { + if (response.ok) { + return response.text(); + } + throw new Error('Invalid Search Response'); + }) + .then((data)=> { + // TODO - work with data variable - mapml response or geojson + let l = document.createElement("layer-"); + l.src = this.searchableLayers[0]; + l.checked = true; + + this._map.options.mapEl.appendChild(l); + }) + .catch((error) => { + console.error("Error:", error); + }); + }, + + // suggest values when user is typing in the search bar + suggest() { + let val = this._input.value; + if (val.length >= 3) { + for (let layer of [... this._map.options.mapEl.layers]) { + if (layer._layer && layer._layer._templatedLayer.search) { + //layer.search = layer.search.replace('QUERY', input.value); + //let link = this.parseLink(layer._layer._templatedLayer._templates[0], val); + //this.searchableLayers.push(link); + let suggestionLink = layer._layer._templatedLayer._templates[0].linkEl.parentElement.querySelector("map-link[rel=searchSuggestion]"); + + if (suggestionLink) { + const query = suggestionLink.getAttribute("query"); + const desc = suggestionLink.getAttribute("desc"); + let tref = suggestionLink.getAttribute("tref"); + + // TODO - currently hardcoded to get first map-input, need to loop through all + let inpName = layer._layer._templatedLayer._templates[0].values[0].getAttribute("name"); + + tref = tref.replace('{' + inpName + '}', val); + fetch(tref) + .then((response) => response.json()) + .then((data)=> { + this.clearItems(); + for (const obj in data) { + this.addItem(data[obj][query], data[obj][desc]); + } + }); + } + } + } + } else { + this.clearItems(); + } + }, + + _updateSearchableLayers(val){ + this.searchableLayers = []; + for (let layer of [... this._map.options.mapEl.layers]) { + if (layer._layer && layer._layer._templatedLayer?.search) { + //layer.search = layer.search.replace('QUERY', input.value); + let link = this.parseLink(layer._layer._templatedLayer._templates[0], val); + this.searchableLayers.push(link); + } + } + if (this.searchableLayers.length === 0) { + this._map.options.mapEl._setControlsVisibility("search",true); + } else { + this._map.options.mapEl._setControlsVisibility("search",false); + } + }, + + _focusSearchBar(e) { + if (e.key === 'Enter') { + this.querySelector("input").focus(); + } + }, + + parseLink(template, val) { + let link = template.template; + let inpName = template.values[0].getAttribute("name"); + let inpType = template.values[0].getAttribute("type"); + //let inpVal = template.values[0].innerHTML; + link = link.replace('{' + inpName + '}',val); + return link; + }, + + _createSuggestions() { + this._suggestion = L.DomUtil.create( + 'datalist', + 'leaflet-searchbox-autocomplete', + this._container); + this._suggestion.id = "suggestions"; + this._items = []; + }, + + addItem(value, text) { + var listItem = L.DomUtil.create('option', 'leaflet-searchbox-autocomplete-item', this._suggestion); + listItem.innerHTML = text; + listItem.value = value; + this._items.push(listItem); + }, + + clearItems() { + this._suggestion.innerHTML = ''; + this._items = []; + }, + _expandSafely() { const section = this._section; L.DomEvent.on(section, 'click', L.DomEvent.preventDefault); diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 24c19cefd..473f3d1c4 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -875,7 +875,7 @@ export var MapMLLayer = L.Layer.extend({ function _initTemplateVars(serverExtent, metaExtent, projection, mapml, base, projectionMatch){ var templateVars = []; // set up the URL template and associated inputs (which yield variable values when processed) - var tlist = serverExtent.querySelectorAll('map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]'), + var tlist = serverExtent.querySelectorAll('map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query],map-link[rel=search]'), varNamesRe = (new RegExp('(?:\{)(.*?)(?:\})','g')), zoomInput = serverExtent.querySelector('map-input[type="zoom" i]'), includesZoom = false, extentFallback = {}; @@ -1051,7 +1051,7 @@ export var MapMLLayer = L.Layer.extend({ layer._extent._mapExtents = []; // stores all the map-extent elements in the layer layer._extent._templateVars = []; // stores all template variables coming from all extents for(let j = 0; j < serverExtent.length; j++){ - if (serverExtent[j].querySelector('map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query]') && + if (serverExtent[j].querySelector('map-link[rel=tile],map-link[rel=image],map-link[rel=features],map-link[rel=query],map-link[rel=search]') && serverExtent[j].hasAttribute("units")) { layer._extent._mapExtents.push(serverExtent[j]); projectionMatch = projectionMatch || selectedAlternate; diff --git a/src/mapml/layers/TemplatedLayer.js b/src/mapml/layers/TemplatedLayer.js index 623c3f36a..28661050c 100644 --- a/src/mapml/layers/TemplatedLayer.js +++ b/src/mapml/layers/TemplatedLayer.js @@ -28,6 +28,8 @@ export var TemplatedLayer = L.Layer.extend({ templates[i].extentBounds = inputData.bounds; templates[i].zoomBounds = inputData.zoomBounds; this._queries.push(L.extend(templates[i], this._setupQueryVars(templates[i]))); + } else if (templates[i].rel === 'search') { + this.search = true; } } }, @@ -207,7 +209,7 @@ export var TemplatedLayer = L.Layer.extend({ }, onAdd: function (map) { for (var i=0;i { }); test("Tap/Long press to show layer control", async () => { - const layerControl = await page.locator("div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div"); + const layerControl = await page.locator("div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div").first(); await layerControl.tap(); let className = await layerControl.evaluate( (el) => el.classList.contains('leaflet-control-layers-expanded') && el._isExpanded From 7b8c211531e8cdb9a18bb402cb61597e8628781a Mon Sep 17 00:00:00 2001 From: AliyanH Date: Fri, 31 Mar 2023 11:54:12 -0400 Subject: [PATCH 3/5] reposition and style search control --- src/mapml.css | 17 ++---- src/mapml/control/SearchControl.js | 93 ++++-------------------------- 2 files changed, 17 insertions(+), 93 deletions(-) diff --git a/src/mapml.css b/src/mapml.css index bf6e2a3e8..584df58ab 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -103,17 +103,11 @@ cursor: default; } -.leaflet-control-layers-toggle, -.leaflet-control-search-toggle { - display:block; +.leaflet-control-layers-toggle { width: 44px !important; height: 44px !important; } -.leaflet-control-layers-expanded .leaflet-control-search-toggle { - display:none; -} - .leaflet-bar a, .leaflet-control-layers, .mapml-reload-button { @@ -248,11 +242,10 @@ background-size: 34px; } -.leaflet-control .leaflet-control-search-toggle { - background-image: url("data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='40' height='40' viewBox='0 0 500.00001 500.00001' id='svg4162' version='1.1' inkscape:version='0.92.3 (2405546, 2018-03-11)' sodipodi:docname='Search_Icon.svg'%3E%3Cdefs id='defs4164'/%3E%3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='0.954' inkscape:cx='250' inkscape:cy='250' inkscape:document-units='px' inkscape:current-layer='layer1' showgrid='false' units='px' inkscape:window-width='1366' inkscape:window-height='706' inkscape:window-x='-8' inkscape:window-y='-8' inkscape:window-maximized='1'/%3E%3Cmetadata id='metadata4167'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/%3E%3Cdc:title/%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg inkscape:label='Layer 1' inkscape:groupmode='layer' id='layer1' transform='translate(0,-552.36216)'%3E%3Cg id='g1400' transform='translate(-4.3609793,-7.6704785)'%3E%3Cpath inkscape:connector-curvature='0' id='path4714' d='M 232.83952,614.96702 A 154.04816,154.04794 0 0 0 78.79153,769.01382 154.04816,154.04794 0 0 0 232.83952,923.06184 154.04816,154.04794 0 0 0 386.88751,769.01382 154.04816,154.04794 0 0 0 232.83952,614.96702 Z m 0,26.77613 A 129.95832,127.2707 0 0 1 362.79832,769.01382 129.95832,127.2707 0 0 1 232.83952,896.28449 129.95832,127.2707 0 0 1 102.88194,769.01382 129.95832,127.2707 0 0 1 232.83952,641.74315 Z' style='opacity:1;fill:%232b0000;fill-opacity:1;stroke:none;stroke-opacity:1'/%3E%3Crect ry='18.08342' rx='33.249443' transform='matrix(0.65316768,0.7572133,-0.60689051,0.79478545,0,0)' y='319.55432' x='794.8775' height='36.16684' width='173.02675' id='rect4721' style='opacity:1;fill:%232b0000;fill-opacity:1;stroke:none;stroke-opacity:1'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); - background-size: 34px; - background-repeat: no-repeat; - background-position: center; +.mapml-search-control { + position: absolute; + margin-left: 50px !important; + width: 201px; } /* Revert Leaflet styles that are causing misalignment. */ diff --git a/src/mapml/control/SearchControl.js b/src/mapml/control/SearchControl.js index 2f9f12ed2..6db68ab5d 100644 --- a/src/mapml/control/SearchControl.js +++ b/src/mapml/control/SearchControl.js @@ -1,16 +1,14 @@ export var SearchBar = L.Control.extend({ options: { - // @option collapsed: Boolean = true - // If `true`, the control will be collapsed into an icon and expanded on mouse hover, touch, or keyboard activation. - collapsed: true, - position: 'topright' + position: 'topleft' }, onAdd: function(map) { this._initLayout(); this._map = map; - L.DomEvent.on(this._container.getElementsByTagName("a")[0], 'keydown', this._focusSearchBar, this._container); + + L.DomEvent.on(this._button, 'click', this.search, this); // get a list of layers that are searchable this.searchableLayers = []; @@ -19,74 +17,18 @@ export var SearchBar = L.Control.extend({ }, onRemove: function(map) { - // Nothing to do here + L.DomEvent.off(this._button); }, - // Expand the control container if collapsed. - expand() { - this._container.classList.add('leaflet-control-layers-expanded'); - this._section.style.height = null; - return this; - }, - - // @method collapse(): this - // Collapse the control container if expanded. - collapse() { - if (this._suggestion.childElementCount === 0) { - this._container.classList.remove('leaflet-control-layers-expanded'); - } - return this; - }, - _initLayout() { - const className = 'leaflet-control-layers', + const className = 'mapml-search-control', container = this._container = L.DomUtil.create('div', className), - collapsed = this.options.collapsed; - - // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released - container.setAttribute('aria-haspopup', true); - - L.DomEvent.disableClickPropagation(container); - L.DomEvent.disableScrollPropagation(container); - - const section = this._section = L.DomUtil.create('div', `${className}-list`); - - if (collapsed) { - this._map.on('click', this.collapse, this); - - L.DomEvent.on(container, { - mouseenter: this._expandSafely, - mouseleave: this.collapse - }, this); - } - - const link = this._layersLink = L.DomUtil.create('a', `leaflet-control-search-toggle`, container); - link.href = '#'; - link.title = 'Layers'; - link.setAttribute('role', 'button'); - - L.DomEvent.on(link, { - keydown(e) { - if (e.code === 'Enter') { - this._expandSafely(); - } - }, - // Certain screen readers intercept the key event and instead send a click event - click(e) { - L.DomEvent.preventDefault(e); - this._expandSafely(); - } - }, this); - - if (!collapsed) { - this.expand(); - } + section = this._section = L.DomUtil.create('div', `${className}-section`); this._input = L.DomUtil.create('input', `${className}-input`, section); this._input.setAttribute("list", "suggestions"); // connect to datalist - this._input.type = 'text'; + this._input.type = 'search'; this._input.size = '15'; - this._input.style.margin='10px'; this._input.onkeyup = (e)=> { if (e.code === 'Enter') { this.search(); @@ -97,11 +39,15 @@ export var SearchBar = L.Control.extend({ this._createSuggestions(); + this._button = L.DomUtil.create('input', `${className}-button`, section); + this._button.type = 'button'; + this._button.value = 'Search'; + container.appendChild(section); }, search() { - let input = this._container.querySelector('.leaflet-control-layers-input'); + let input = this._input; this._updateSearchableLayers(input.value); // TODO - search through all layers when multiple searchable layers present, @@ -184,12 +130,6 @@ export var SearchBar = L.Control.extend({ } }, - _focusSearchBar(e) { - if (e.key === 'Enter') { - this.querySelector("input").focus(); - } - }, - parseLink(template, val) { let link = template.template; let inpName = template.values[0].getAttribute("name"); @@ -218,15 +158,6 @@ export var SearchBar = L.Control.extend({ clearItems() { this._suggestion.innerHTML = ''; this._items = []; - }, - - _expandSafely() { - const section = this._section; - L.DomEvent.on(section, 'click', L.DomEvent.preventDefault); - this.expand(); - setTimeout(() => { - L.DomEvent.off(section, 'click', L.DomEvent.preventDefault); - }); } }); From a66ebb527fae7895e447ce8e37388f15d41ed7a1 Mon Sep 17 00:00:00 2001 From: AliyanH Date: Mon, 3 Apr 2023 13:34:32 -0400 Subject: [PATCH 4/5] Hide searchbar on a mapml-viewer with no layers --- src/mapml-viewer.js | 5 ++++- test/e2e/api/domApi-mapml-viewer.test.js | 9 ++++++--- test/e2e/core/touchDevice.test.js | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js index 9787ae290..15dc37470 100644 --- a/src/mapml-viewer.js +++ b/src/mapml-viewer.js @@ -412,8 +412,11 @@ export class MapViewer extends HTMLElement { } }); } - if (this._layerControl && this._layerControl._layers.length === 0) { + if (this._layerControl?._layers.length === 0) { this._layerControl._container.setAttribute("hidden",""); + if (this._searchBar) { + this._setControlsVisibility("search",true); + } } } diff --git a/test/e2e/api/domApi-mapml-viewer.test.js b/test/e2e/api/domApi-mapml-viewer.test.js index b5547b91f..bbd1084a4 100644 --- a/test/e2e/api/domApi-mapml-viewer.test.js +++ b/test/e2e/api/domApi-mapml-viewer.test.js @@ -187,25 +187,28 @@ test.describe("mapml-viewer DOM API Tests", () => { await page.evaluateHandle((viewer) => viewer.setAttribute("controls", ""), viewerHandle); await page.evaluateHandle( (viewer) => document.body.appendChild(viewer), viewerHandle); - let leftControlCount = await page.$eval(".leaflet-top.leaflet-left", (div) => div.childElementCount); - expect(leftControlCount).toBe(2); - let zoomHidden = await page.$eval(".leaflet-top.leaflet-left > .leaflet-control-zoom", (div) => div.hidden); let reloadHidden = await page.$eval(".leaflet-top.leaflet-left > .mapml-reload-button", (div) => div.hidden); + let searchHidden = await page.$eval(".leaflet-top.leaflet-left > .mapml-search-control", (div) => div.hidden); expect(zoomHidden).toEqual(false); expect(reloadHidden).toEqual(false); + expect(searchHidden).toEqual(true); await page.evaluate( viewer => viewer.removeAttribute("controls"), viewerHandle); zoomHidden = await page.$eval(".leaflet-top.leaflet-left > .leaflet-control-zoom", (div) => div.hidden); reloadHidden = await page.$eval(".leaflet-top.leaflet-left > .mapml-reload-button", (div) => div.hidden); + searchHidden = await page.$eval(".leaflet-top.leaflet-left > .mapml-search-control", (div) => div.hidden); expect(zoomHidden).toEqual(true); expect(reloadHidden).toEqual(true); + expect(searchHidden).toEqual(true); await page.evaluate( viewer => viewer.setAttribute("controls",""), viewerHandle); zoomHidden = await page.$eval(".leaflet-top.leaflet-left > .leaflet-control-zoom", (div) => div.hidden); reloadHidden = await page.$eval(".leaflet-top.leaflet-left > .mapml-reload-button", (div) => div.hidden); + searchHidden = await page.$eval(".leaflet-top.leaflet-left > .mapml-search-control", (div) => div.hidden); expect(zoomHidden).toEqual(false); expect(reloadHidden).toEqual(false); + expect(searchHidden).toEqual(true); // remove map for next test await page.evaluateHandle(() => document.querySelector('mapml-viewer').remove()); diff --git a/test/e2e/core/touchDevice.test.js b/test/e2e/core/touchDevice.test.js index 7deb7deef..18e812bf6 100644 --- a/test/e2e/core/touchDevice.test.js +++ b/test/e2e/core/touchDevice.test.js @@ -19,7 +19,7 @@ test.describe("Playwright touch device tests", () => { }); test("Tap/Long press to show layer control", async () => { - const layerControl = await page.locator("div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div").first(); + const layerControl = await page.locator("div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div"); await layerControl.tap(); let className = await layerControl.evaluate( (el) => el.classList.contains('leaflet-control-layers-expanded') && el._isExpanded From 5fe1af60b7acdbab50169b83e06804be4bde87e9 Mon Sep 17 00:00:00 2001 From: AliyanH Date: Mon, 3 Apr 2023 17:18:01 -0400 Subject: [PATCH 5/5] Update Styling --- index.html | 4 ++-- src/mapml.css | 31 +++++++++++++++++++++++++++++- src/mapml/control/SearchControl.js | 3 ++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 8dd91433b..bbe9c706d 100644 --- a/index.html +++ b/index.html @@ -84,8 +84,8 @@ - diff --git a/src/mapml.css b/src/mapml.css index 584df58ab..d943041e5 100644 --- a/src/mapml.css +++ b/src/mapml.css @@ -244,9 +244,38 @@ .mapml-search-control { position: absolute; - margin-left: 50px !important; + margin-left: 55px !important; width: 201px; } +.mapml-search-control-section { + width: 180px; + display: grid; + grid-template-columns: 83% 1fr; +} +.mapml-search-control-section input[type=search] { + padding: 5px; + font-size: 16px; + border: 1px solid grey; + width: 100%; + border-radius: 5px; + grid-column: 1; + overflow: hidden; +} +.mapml-search-control-section input[type=button] { + width: 100%; + height: 30px; + border-radius: 5px; + border: 1px solid black; + grid-column: 2; + overflow: hidden; + background-image: url("data:image/svg+xml,%3Csvg xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns%23' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns%23' xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' width='40' height='40' viewBox='0 0 500.00001 500.00001' id='svg4162' version='1.1' inkscape:version='0.92.3 (2405546, 2018-03-11)' sodipodi:docname='Search_Icon.svg'%3E%3Cdefs id='defs4164'/%3E%3Csodipodi:namedview id='base' pagecolor='%23ffffff' bordercolor='%23666666' borderopacity='1.0' inkscape:pageopacity='0.0' inkscape:pageshadow='2' inkscape:zoom='0.954' inkscape:cx='250' inkscape:cy='250' inkscape:document-units='px' inkscape:current-layer='layer1' showgrid='false' units='px' inkscape:window-width='1366' inkscape:window-height='706' inkscape:window-x='-8' inkscape:window-y='-8' inkscape:window-maximized='1'/%3E%3Cmetadata id='metadata4167'%3E%3Crdf:RDF%3E%3Ccc:Work rdf:about=''%3E%3Cdc:format%3Eimage/svg+xml%3C/dc:format%3E%3Cdc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/%3E%3Cdc:title/%3E%3C/cc:Work%3E%3C/rdf:RDF%3E%3C/metadata%3E%3Cg inkscape:label='Layer 1' inkscape:groupmode='layer' id='layer1' transform='translate(0,-552.36216)'%3E%3Cg id='g1400' transform='translate(-4.3609793,-7.6704785)'%3E%3Cpath inkscape:connector-curvature='0' id='path4714' d='M 232.83952,614.96702 A 154.04816,154.04794 0 0 0 78.79153,769.01382 154.04816,154.04794 0 0 0 232.83952,923.06184 154.04816,154.04794 0 0 0 386.88751,769.01382 154.04816,154.04794 0 0 0 232.83952,614.96702 Z m 0,26.77613 A 129.95832,127.2707 0 0 1 362.79832,769.01382 129.95832,127.2707 0 0 1 232.83952,896.28449 129.95832,127.2707 0 0 1 102.88194,769.01382 129.95832,127.2707 0 0 1 232.83952,641.74315 Z' style='opacity:1;fill:%232b0000;fill-opacity:1;stroke:none;stroke-opacity:1'/%3E%3Crect ry='18.08342' rx='33.249443' transform='matrix(0.65316768,0.7572133,-0.60689051,0.79478545,0,0)' y='319.55432' x='794.8775' height='36.16684' width='173.02675' id='rect4721' style='opacity:1;fill:%232b0000;fill-opacity:1;stroke:none;stroke-opacity:1'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); + background-size: 27px; + background-repeat: no-repeat; + background-position: center; +} +.mapml-search-control input[type=button]:hover { + background-color: #fafafa; +} /* Revert Leaflet styles that are causing misalignment. */ .leaflet-control-layers-selector { diff --git a/src/mapml/control/SearchControl.js b/src/mapml/control/SearchControl.js index 6db68ab5d..1bc9a4f2e 100644 --- a/src/mapml/control/SearchControl.js +++ b/src/mapml/control/SearchControl.js @@ -25,6 +25,7 @@ export var SearchBar = L.Control.extend({ container = this._container = L.DomUtil.create('div', className), section = this._section = L.DomUtil.create('div', `${className}-section`); + // TODO - Create a label for the search input this._input = L.DomUtil.create('input', `${className}-input`, section); this._input.setAttribute("list", "suggestions"); // connect to datalist this._input.type = 'search'; @@ -41,7 +42,7 @@ export var SearchBar = L.Control.extend({ this._button = L.DomUtil.create('input', `${className}-button`, section); this._button.type = 'button'; - this._button.value = 'Search'; + this._button.title = 'Search'; container.appendChild(section); },