/* Copyright (c) 2017 Jean-Marc VIGLINO, released under the CeCILL-B license (French BSD license) (http://www.cecill.info/licences/Licence_CeCILL-B_V1-en.txt). */ import ol_control_Control from 'ol/control/Control.js' import {unByKey as ol_Observable_unByKey} from 'ol/Observable.js' import ol_ext_element from '../util/element.js' /** * Search Control. * This is the base class for search controls. You can use it for simple custom search or as base to new class. * @see ol_control_SearchFeature * @see ol_control_SearchPhoton * * @constructor * @extends {ol_control_Control} * @fires select * @fires change:input * @param {Object=} options * @param {string} options.className control class name * @param {Element | string | undefined} options.target Specify a target if you want the control to be rendered outside of the map's viewport. * @param {string | undefined} options.title Title to use for the search button tooltip, default "Search" * @param {string | undefined} options.reverseTitle Title to use for the reverse geocoding button tooltip, default "Click on the map..." * @param {string | undefined} options.placeholder placeholder, default "Search..." * @param {boolean | undefined} options.reverse enable reverse geocoding, default false * @param {string | undefined} options.inputLabel label for the input, default none * @param {string | undefined} options.collapsed search is collapsed on start, default true * @param {string | undefined} options.noCollapse prevent collapsing on input blur, default false * @param {number | undefined} options.typing a delay on each typing to start searching (ms) use -1 to prevent autocompletion, default 300. * @param {integer | undefined} options.minLength minimum length to start searching, default 1 * @param {integer | undefined} options.maxItems maximum number of items to display in the autocomplete list, default 10 * @param {integer | undefined} options.maxHistory maximum number of items to display in history. Set -1 if you don't want history, default maxItems * @param {function} options.getTitle a function that takes a feature and return the name to display in the index. * @param {function} options.autocomplete a function that take a search string and callback function to send an array * @param {function} options.onselect a function called when a search is selected * @param {boolean} options.centerOnSelect center map on search, default false * @param {number|boolean} options.zoomOnSelect center map on search and zoom to value if zoom < value, default false */ var ol_control_Search = class olcontrolSearch extends ol_control_Control { constructor(options) { options = options || {}; var classNames = (options.className || '') + ' ol-search' + (options.target ? '' : ' ol-unselectable ol-control'); var element = ol_ext_element.create('DIV', { className: classNames }); super({ element: element, target: options.target }); var self = this; if (options.typing == undefined) { options.typing = 300; } // Class name for history this._classname = options.className || 'search'; if (options.collapsed !== false) element.classList.add('ol-collapsed'); if (!options.target) { this.button = document.createElement('BUTTON'); this.button.setAttribute('type', 'button'); this.button.setAttribute('title', options.title || options.label || 'Search'); this.button.addEventListener('click', function () { element.classList.toggle('ol-collapsed'); if (!element.classList.contains('ol-collapsed')) { element.querySelector('input.search').focus(); var listElements = element.querySelectorAll('li'); for (var i = 0; i < listElements.length; i++) { listElements[i].classList.remove('select'); } // Display history if (!input.value) { self.drawList_(); } } }); element.appendChild(this.button); } // Input label if (options.inputLabel) { var label = document.createElement("LABEL"); label.innerText = options.inputLabel; element.appendChild(label); } // Search input var tout, cur = ""; var input = this._input = document.createElement("INPUT"); input.setAttribute("type", "search"); input.setAttribute("class", "search"); input.setAttribute("autocomplete", "off"); input.setAttribute("placeholder", options.placeholder || "Search..."); input.addEventListener("change", function (e) { self.dispatchEvent({ type: "change:input", input: e, value: input.value }); }); var doSearch = function (e) { // console.log(e.type+" "+e.key)' var li = element.querySelector("ul.autocomplete li.select"); var val = input.value; // move up/down if (e.key == 'ArrowDown' || e.key == 'ArrowUp' || e.key == 'Down' || e.key == 'Up') { if (li) { var newli = (/Down/.test(e.key)) ? li.nextElementSibling : li.previousElementSibling; if (newli && !newli.classList.contains('copy')) { li.classList.remove("select"); newli.classList.add("select"); input.value = newli.innerText; } } else { li = element.querySelector("ul.autocomplete li") li.classList.add("select"); input.value = li.innerText; } } // Clear input else if (e.type == 'input') { if (!val) { setTimeout(function () { self.drawList_(); }, 200); } if (li) { input.value = val = ''; li.classList.remove("select"); } } // Select in the list else if (li && (e.type === "search" || e.key === "Enter")) { if (element.classList.contains("ol-control")) { input.blur(); } li.classList.remove("select"); cur = val; self._handleSelect(self._list[li.getAttribute("data-search")]); } // Search / autocomplete else if ((e.type === "search" || e.key === 'Enter') || (cur != val && options.typing >= 0)) { // current search cur = val; if (cur) { // prevent searching on each typing if (tout) clearTimeout(tout); var minLength = self.get("minLength"); tout = setTimeout(function () { if (cur.length >= minLength) { var s = self.autocomplete(cur, function (auto) { self.drawList_(auto); }); if (s) self.drawList_(s); } else self.drawList_(); }, options.typing); } else { self.drawList_(); } } // Clear list selection else { li = element.querySelector("ul.autocomplete li"); if (li) li.classList.remove('select'); } }; input.addEventListener("keyup", doSearch); input.addEventListener("search", doSearch); input.addEventListener("cut", doSearch); input.addEventListener("paste", doSearch); input.addEventListener("input", doSearch); if (!options.noCollapse) { input.addEventListener('blur', function () { setTimeout(function () { if (input !== document.activeElement) { element.classList.add('ol-collapsed'); this.set('reverse', false); element.classList.remove('ol-revers'); } }.bind(this), 200); }.bind(this)); input.addEventListener('focus', function () { if (!this.get('reverse')) { element.classList.remove('ol-collapsed'); element.classList.remove('ol-revers'); } }.bind(this)); input.addEventListener('keydown', function() { this.set('reverse', false); element.classList.remove('ol-collapsed'); element.classList.remove('ol-revers'); }.bind(this)) } element.appendChild(input); // Reverse geocode if (options.reverse) { var reverse = ol_ext_element.create('BUTTON', { type: 'button', class: 'ol-revers', title: options.reverseTitle || 'click on the map', on: { focus: function () { if (!this.get('reverse')) { this.set('reverse', !this.get('reverse')); input.focus(); element.classList.add('ol-revers'); } else { this.set('reverse', false); } }.bind(this) } }); element.appendChild(reverse); } // Autocomplete list var ul = document.createElement('UL'); ul.classList.add('autocomplete'); element.appendChild(ul); if (typeof (options.getTitle) == 'function') this.getTitle = options.getTitle; if (typeof (options.autocomplete) == 'function') this.autocomplete = options.autocomplete; // Options this.set('copy', options.copy); this.set('minLength', options.minLength || 1); this.set('maxItems', options.maxItems || 10); this.set('maxHistory', options.maxHistory || options.maxItems || 10); // Select if (options.onselect) this.on('select', options.onselect); // Center on select if (options.centerOnSelect) { this.on('select', function (e) { var map = this.getMap(); if (map) { map.getView().setCenter(e.coordinate); } }.bind(this)); } // Zoom on select if (options.zoomOnSelect) { this.on('select', function (e) { var map = this.getMap(); if (map) { map.getView().setCenter(e.coordinate); if (map.getView().getZoom() < options.zoomOnSelect) map.getView().setZoom(options.zoomOnSelect); } }.bind(this)); } // History this.restoreHistory(); this.drawList_(); } /** * Remove the control from its current map and attach it to the new map. * Subclasses may set up event handlers to get notified about changes to * the map here. * @param {ol.Map} map Map. * @api stable */ setMap(map) { if (this._listener) ol_Observable_unByKey(this._listener); this._listener = null; super.setMap(map); if (map) { this._listener = map.on('click', this._handleClick.bind(this)); } } /** Collapse the search * @param {boolean} [b=true] * @api */ collapse(b) { if (b === false) this.element.classList.remove('ol-collapsed'); else this.element.classList.add('ol-collapsed'); } /** Get the input field * @return {Element} * @api */ getInputField() { return this._input; } /** Returns the text to be displayed in the menu * @param {any} f feature to be displayed * @return {string} the text to be displayed in the index, default f.name * @api */ getTitle(f) { return f.name || "No title"; } /** Returns title as text * @param {any} f feature to be displayed * @return {string} * @api */ _getTitleTxt(f) { return ol_ext_element.create('DIV', { html: this.getTitle(f) }).innerText; } /** Force search to refresh */ search() { var search = this.element.querySelector("input.search"); this._triggerCustomEvent('search', search); } /** Reverse geocode * @param {Object} event * @param {ol.coordinate} event.coordinate * @private */ _handleClick(e) { if (this.get('reverse')) { document.activeElement.blur(); this.reverseGeocode(e.coordinate); } } /** Reverse geocode * @param {ol.coordinate} coord * @param {function | undefined} cback a callback function, default trigger a select event * @api */ reverseGeocode( /*coord, cback*/) { // this._handleSelect(f); } /** Trigger custom event on elemebt * @param {*} eventName * @param {*} element * @private */ _triggerCustomEvent(eventName, element) { ol_ext_element.dispatchEvent(eventName, element); } /** Set the input value in the form (for initialisation purpose) * @param {string} value * @param {boolean} search to start a search * @api */ setInput(value, search) { var input = this.element.querySelector("input.search"); input.value = value; if (search) this._triggerCustomEvent("keyup", input); } /** A line has been clicked in the menu > dispatch event * @param {any} f the feature, as passed in the autocomplete * @param {boolean} reverse true if reverse geocode * @param {ol.coordinate} coord * @param {*} options options passed to the event * @api */ select(f, reverse, coord, options) { var event = { type: "select", search: f, reverse: !!reverse, coordinate: coord }; if (options) { for (var i in options) { event[i] = options[i]; } } this.dispatchEvent(event); } /** * Save history and select * @param {*} f * @param {boolean} reverse true if reverse geocode * @param {*} options options send in the event * @private */ _handleSelect(f, reverse, options) { if (!f) return; // Save input in history var hist = this.get('history'); // Prevent error on stringify var i; try { var fstr = JSON.stringify(f); for (i = hist.length - 1; i >= 0; i--) { if (!hist[i] || JSON.stringify(hist[i]) === fstr) { hist.splice(i, 1); } } } catch (e) { for (i = hist.length - 1; i >= 0; i--) { if (hist[i] === f) { hist.splice(i, 1); } } } hist.unshift(f); var size = Math.max(0, this.get('maxHistory') || 10) || 0; while (hist.length > size) { hist.pop(); } this.saveHistory(); // Select feature this.select(f, reverse, null, options); if (reverse) { this.setInput(this._getTitleTxt(f)); this.drawList_(); setTimeout(function () { this.collapse(false); }.bind(this), 300); } } /** Save history (in the localstorage) */ saveHistory() { try { if (this.get('maxHistory') >= 0) { localStorage["ol@search-" + this._classname] = JSON.stringify(this.get('history')); } else { localStorage.removeItem("ol@search-" + this._classname); } } catch (e) { console.warn('Failed to access localStorage...'); } } /** Restore history (from the localstorage) */ restoreHistory() { if (this._history[this._classname]) { this.set('history', this._history[this._classname]); } else { try { this._history[this._classname] = JSON.parse(localStorage["ol@search-" + this._classname]); this.set('history', this._history[this._classname]); } catch (e) { this.set('history', []); } } } /** * Remove previous history */ clearHistory() { this.set('history', []); this.saveHistory(); this.drawList_(); } /** * Get history table */ getHistory() { return this.get('history'); } /** Autocomplete function * @param {string} s search string * @param {function} cback a callback function that takes an array to display in the autocomplete field (for asynchronous search) * @return {Array|false} an array of search solutions or false if the array is send with the cback argument (asnchronous) * @api */ autocomplete(s, cback) { cback([]); return false; // or just return []; } /** Draw the list * @param {Array} auto an array of search result * @private */ drawList_(auto) { var self = this; var ul = this.element.querySelector("ul.autocomplete"); ul.innerHTML = ''; this._list = []; if (!auto) { var input = this.element.querySelector("input.search"); var value = input.value; if (!value) { auto = this.get('history'); } else { return; } ul.setAttribute('class', 'autocomplete history'); } else { ul.setAttribute('class', 'autocomplete'); } var li, max = Math.min(self.get("maxItems"), auto.length); for (var i = 0; i < max; i++) { if (auto[i]) { if (!i || !self.equalFeatures(auto[i], auto[i - 1])) { li = document.createElement("LI"); li.setAttribute("data-search", this._list.length); this._list.push(auto[i]); li.addEventListener("click", function (e) { self._handleSelect(self._list[e.currentTarget.getAttribute("data-search")]); }); var title = self.getTitle(auto[i]); if (title instanceof Element) li.appendChild(title); else li.innerHTML = title; ul.appendChild(li); } } } if (max && this.get("copy")) { li = document.createElement("LI"); li.classList.add("copy"); li.innerHTML = this.get("copy"); ul.appendChild(li); } } /** Test if 2 features are equal * @param {any} f1 * @param {any} f2 * @return {boolean} */ equalFeatures( /* f1, f2 */) { return false; } } /** Current history */ ol_control_Search.prototype._history = {}; export default ol_control_Search