533 lines
17 KiB
JavaScript
533 lines
17 KiB
JavaScript
/* 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")
|
|
if (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
|