leaflet-geosearch
Version:
Adds support for address lookup (a.k.a. geocoding / geosearching) to Leaflet.
292 lines • 10.6 kB
JavaScript
import * as L from 'leaflet';
import SearchElement from './SearchElement';
import ResultList from './resultList';
import debounce from './lib/debounce';
import { createElement, addClassName, removeClassName, } from './domUtils';
import { ENTER_KEY, SPECIAL_KEYS, ARROW_UP_KEY, ARROW_DOWN_KEY, ESCAPE_KEY, } from './constants';
const defaultOptions = {
position: 'topleft',
style: 'button',
showMarker: true,
showPopup: false,
popupFormat: ({ result }) => `${result.label}`,
resultFormat: ({ result }) => `${result.label}`,
marker: {
icon: L && L.Icon ? new L.Icon.Default() : undefined,
draggable: false,
},
maxMarkers: 1,
maxSuggestions: 5,
retainZoomLevel: false,
animateZoom: true,
searchLabel: 'Enter address',
clearSearchLabel: 'Clear search',
notFoundMessage: '',
messageHideDelay: 3000,
zoomLevel: 18,
classNames: {
container: 'leaflet-bar leaflet-control leaflet-control-geosearch',
button: 'leaflet-bar-part leaflet-bar-part-single',
resetButton: 'reset',
msgbox: 'leaflet-bar message',
form: '',
input: '',
resultlist: '',
item: '',
notfound: 'leaflet-bar-notfound',
},
autoComplete: true,
autoCompleteDelay: 250,
autoClose: false,
keepResult: false,
updateMap: true,
resetButton: '×',
};
const UNINITIALIZED_ERR = 'Leaflet must be loaded before instantiating the GeoSearch control';
// @ts-ignore
const Control = {
options: { ...defaultOptions },
classNames: { ...defaultOptions.classNames },
initialize(options) {
if (!L) {
throw new Error(UNINITIALIZED_ERR);
}
if (!options.provider) {
throw new Error('Provider is missing from options');
}
// merge given options with control defaults
this.options = { ...defaultOptions, ...options };
this.classNames = { ...this.classNames, ...options.classNames };
this.markers = new L.FeatureGroup();
this.classNames.container += ` leaflet-geosearch-${this.options.style}`;
this.searchElement = new SearchElement({
searchLabel: this.options.searchLabel,
classNames: {
container: this.classNames.container,
form: this.classNames.form,
input: this.classNames.input,
},
handleSubmit: (result) => this.onSubmit(result),
});
this.button = createElement('a', this.classNames.button, this.searchElement.container, {
title: this.options.searchLabel,
'aria-label': this.options.searchLabel,
href: '#',
onClick: (e) => this.onClick(e),
});
L.DomEvent.disableClickPropagation(this.button);
this.resetButton = createElement('button', this.classNames.resetButton, this.searchElement.form, {
text: this.options.resetButton,
'aria-label': this.options.clearSearchLabel,
onClick: () => {
if (this.searchElement.input.value === '') {
this.close();
}
else {
this.clearResults(null, true);
}
},
});
L.DomEvent.disableClickPropagation(this.resetButton);
if (this.options.autoComplete) {
this.resultList = new ResultList({
handleClick: ({ result }) => {
this.searchElement.input.value = result.label;
this.onSubmit({ query: result.label, data: result });
},
classNames: {
resultlist: this.classNames.resultlist,
item: this.classNames.item,
notfound: this.classNames.notfound,
},
notFoundMessage: this.options.notFoundMessage,
});
this.searchElement.form.appendChild(this.resultList.container);
this.searchElement.input.addEventListener('keyup', debounce((e) => this.autoSearch(e), this.options.autoCompleteDelay), true);
this.searchElement.input.addEventListener('keydown', (e) => this.selectResult(e), true);
this.searchElement.input.addEventListener('keydown', (e) => this.clearResults(e, true), true);
}
this.searchElement.form.addEventListener('click', (e) => {
e.preventDefault();
}, false);
},
onAdd(map) {
const { showMarker, style } = this.options;
this.map = map;
if (showMarker) {
this.markers.addTo(map);
}
if (style === 'bar') {
const root = map
.getContainer()
.querySelector('.leaflet-control-container');
this.container = createElement('div', 'leaflet-control-geosearch leaflet-geosearch-bar');
this.container.appendChild(this.searchElement.form);
root.appendChild(this.container);
}
L.DomEvent.disableClickPropagation(this.searchElement.form);
return this.searchElement.container;
},
onRemove() {
this.container?.remove();
return this;
},
open() {
const { container, input } = this.searchElement;
addClassName(container, 'active');
input.focus();
},
close() {
const { container } = this.searchElement;
removeClassName(container, 'active');
this.clearResults();
},
onClick(event) {
event.preventDefault();
event.stopPropagation();
const { container } = this.searchElement;
if (container.classList.contains('active')) {
this.close();
}
else {
this.open();
}
},
selectResult(event) {
if ([ENTER_KEY, ARROW_DOWN_KEY, ARROW_UP_KEY].indexOf(event.keyCode) === -1) {
return;
}
event.preventDefault();
if (event.keyCode === ENTER_KEY) {
const item = this.resultList.select(this.resultList.selected);
this.onSubmit({ query: this.searchElement.input.value, data: item });
return;
}
const max = this.resultList.count() - 1;
if (max < 0) {
return;
}
const { selected } = this.resultList;
const next = event.keyCode === ARROW_DOWN_KEY ? selected + 1 : selected - 1;
const idx = next < 0 ? max : next > max ? 0 : next;
const item = this.resultList.select(idx);
this.searchElement.input.value = item.label;
},
clearResults(event, force = false) {
if (event && event.keyCode !== ESCAPE_KEY) {
return;
}
const { keepResult, autoComplete } = this.options;
if (force || !keepResult) {
this.searchElement.input.value = '';
this.markers.clearLayers();
}
if (autoComplete) {
this.resultList.clear();
}
},
async autoSearch(event) {
if (SPECIAL_KEYS.indexOf(event.keyCode) > -1) {
return;
}
const query = event.target.value;
const { provider } = this.options;
if (query.length) {
let results = await provider.search({ query });
results = results.slice(0, this.options.maxSuggestions);
this.resultList.render(results, this.options.resultFormat);
}
else {
this.resultList.clear();
}
},
async onSubmit(query) {
this.resultList.clear();
const { provider } = this.options;
const results = await provider.search(query);
if (results && results.length > 0) {
this.showResult(results[0], query);
}
},
showResult(result, query) {
const { autoClose, updateMap } = this.options;
const markers = this.markers.getLayers();
if (markers.length >= this.options.maxMarkers) {
this.markers.removeLayer(markers[0]);
}
const marker = this.addMarker(result, query);
if (updateMap) {
this.centerMap(result);
}
this.map.fireEvent('geosearch/showlocation', {
location: result,
marker,
});
if (autoClose) {
this.closeResults();
}
},
closeResults() {
const { container } = this.searchElement;
if (container.classList.contains('active')) {
removeClassName(container, 'active');
}
this.clearResults();
},
addMarker(result, query) {
const { marker: options, showPopup, popupFormat } = this.options;
const marker = new L.Marker([result.y, result.x], options);
let popupLabel = result.label;
if (typeof popupFormat === 'function') {
popupLabel = popupFormat({ query, result });
}
marker.bindPopup(popupLabel);
this.markers.addLayer(marker);
if (showPopup) {
marker.openPopup();
}
if (options.draggable) {
marker.on('dragend', (args) => {
this.map.fireEvent('geosearch/marker/dragend', {
location: marker.getLatLng(),
event: args,
});
});
}
return marker;
},
centerMap(result) {
const { retainZoomLevel, animateZoom } = this.options;
const resultBounds = result.bounds
? new L.LatLngBounds(result.bounds)
: new L.LatLng(result.y, result.x).toBounds(10);
const bounds = resultBounds.isValid()
? resultBounds
: this.markers.getBounds();
if (!retainZoomLevel && resultBounds.isValid() && !result.bounds) {
this.map.setView(bounds.getCenter(), this.getZoom(), {
animate: animateZoom,
});
}
else if (!retainZoomLevel && resultBounds.isValid()) {
this.map.fitBounds(bounds, { animate: animateZoom });
}
else {
this.map.setView(bounds.getCenter(), this.getZoom(), {
animate: animateZoom,
});
}
},
getZoom() {
const { retainZoomLevel, zoomLevel } = this.options;
return retainZoomLevel ? this.map.getZoom() : zoomLevel;
},
};
export default function SearchControl(...options) {
if (!L) {
throw new Error(UNINITIALIZED_ERR);
}
const LControl = L.Control.extend(Control);
return new LControl(...options);
}
//# sourceMappingURL=SearchControl.js.map