mirror of
https://github.com/jlengrand/leaflet-geosearch.git
synced 2026-03-10 08:31:26 +00:00
convert SearchControl to typescript
This commit is contained in:
@@ -197,6 +197,8 @@
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.leaflet-control-geosearch a.reset:hover {
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -17990,7 +17990,7 @@
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.6.0.tgz",
|
||||
"integrity": "sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==",
|
||||
"dev": true
|
||||
"optional": true
|
||||
},
|
||||
"leven": {
|
||||
"version": "3.1.0",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"scripts": {
|
||||
"clean": "rimraf ./dist",
|
||||
"build": "run-s clean \"run-p build:dist build:lib\"",
|
||||
"build:dist": "microbundle build --external none --format es,cjs,umd",
|
||||
"build:dist": "microbundle build --external leaflet --format es,cjs,umd",
|
||||
"build:lib": "tsc",
|
||||
"build:watch": "npm run build:dist -- --compress false --watch",
|
||||
"test": "jest",
|
||||
@@ -77,7 +77,6 @@
|
||||
"gatsby-plugin-react-leaflet": "^2.0.12",
|
||||
"jest": "^25.3.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"leaflet": "^1.6.0",
|
||||
"microbundle": "^0.12.0-next.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.0.4",
|
||||
@@ -88,5 +87,8 @@
|
||||
"ts-jest": "^25.3.1",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"dependencies": {}
|
||||
"dependencies": {},
|
||||
"optionalDependencies": {
|
||||
"leaflet": "^1.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
473
src/SearchControl.ts
Normal file
473
src/SearchControl.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
import L, { ControlPosition, FeatureGroup, MarkerOptions, Map } 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';
|
||||
import AbstractProvider, { SearchResult } from './providers/provider';
|
||||
|
||||
const defaultOptions: Omit<SearchControlOptions, 'provider'> = {
|
||||
position: 'topleft',
|
||||
style: 'button',
|
||||
showMarker: true,
|
||||
showPopup: false,
|
||||
popupFormat: ({ result }) => `${result.label}`,
|
||||
marker: {
|
||||
icon: L && L.Icon ? new L.Icon.Default() : undefined,
|
||||
draggable: false,
|
||||
},
|
||||
maxMarkers: 1,
|
||||
retainZoomLevel: false,
|
||||
animateZoom: true,
|
||||
searchLabel: 'Enter address',
|
||||
notFoundMessage: 'Sorry, that address could not be found.',
|
||||
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: '',
|
||||
},
|
||||
autoComplete: true,
|
||||
autoCompleteDelay: 250,
|
||||
autoClose: false,
|
||||
keepResult: false,
|
||||
};
|
||||
|
||||
const wasHandlerEnabled: { [key in MapHandler]?: boolean } = {};
|
||||
type MapHandler =
|
||||
| 'dragging'
|
||||
| 'touchZoom'
|
||||
| 'doubleClickZoom'
|
||||
| 'scrollWheelZoom'
|
||||
| 'boxZoom'
|
||||
| 'keyboard';
|
||||
|
||||
const mapHandlers: MapHandler[] = [
|
||||
'dragging',
|
||||
'touchZoom',
|
||||
'doubleClickZoom',
|
||||
'scrollWheelZoom',
|
||||
'boxZoom',
|
||||
'keyboard',
|
||||
];
|
||||
|
||||
const UNINITIALIZED_ERR =
|
||||
'Leaflet must be loaded before instantiating the GeoSearch control';
|
||||
|
||||
interface SearchControlOptions {
|
||||
provider: AbstractProvider;
|
||||
position: ControlPosition;
|
||||
style: 'button' | 'bar';
|
||||
|
||||
marker: MarkerOptions;
|
||||
maxMarkers: number;
|
||||
showMarker: boolean;
|
||||
showPopup: boolean;
|
||||
popupFormat<T = any>(args: {
|
||||
query: Selection;
|
||||
result: SearchResult<T>;
|
||||
}): string;
|
||||
|
||||
searchLabel: string;
|
||||
notFoundMessage: string;
|
||||
messageHideDelay: number;
|
||||
|
||||
animateZoom: boolean;
|
||||
zoomLevel: number;
|
||||
retainZoomLevel: boolean;
|
||||
|
||||
classNames: {
|
||||
container: string;
|
||||
button: string;
|
||||
resetButton: string;
|
||||
msgbox: string;
|
||||
form: string;
|
||||
input: string;
|
||||
};
|
||||
|
||||
autoComplete: boolean;
|
||||
autoCompleteDelay: number;
|
||||
autoClose: boolean;
|
||||
keepResult: boolean;
|
||||
}
|
||||
|
||||
export type SearchControlInputOptions = Partial<SearchControlOptions> & {
|
||||
provider: AbstractProvider;
|
||||
};
|
||||
|
||||
interface Selection {
|
||||
query: string;
|
||||
data?: SearchResult;
|
||||
}
|
||||
|
||||
interface SearchControl {
|
||||
options: Omit<SearchControlOptions, 'provider'> & {
|
||||
provider?: SearchControlOptions['provider'];
|
||||
};
|
||||
markers: FeatureGroup;
|
||||
handlersDisabled: boolean;
|
||||
searchElement: SearchElement;
|
||||
resultList: ResultList;
|
||||
classNames: SearchControlOptions['classNames'];
|
||||
container: HTMLDivElement;
|
||||
input: HTMLInputElement;
|
||||
map: Map;
|
||||
|
||||
// [key: string]: any;
|
||||
initialize(options: SearchControlOptions): void;
|
||||
onSubmit(result: Selection): void;
|
||||
onClick(event: Event): void;
|
||||
clearResults(event?: KeyboardEvent | null, force?: boolean): void;
|
||||
autoSearch(event: KeyboardEvent): void;
|
||||
selectResult(event: KeyboardEvent): void;
|
||||
disableHandlers(event: Event): void;
|
||||
restoreHandlers(event?: Event): void;
|
||||
showResult(result: SearchResult, query: Selection): void;
|
||||
addMarker(result: SearchResult, selection: Selection): void;
|
||||
centerMap(result: SearchResult): void;
|
||||
closeResults(): void;
|
||||
getZoom(): number;
|
||||
onAdd(map: Map): HTMLDivElement;
|
||||
onRemove(): SearchControl;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const Control: SearchControl = {
|
||||
options: defaultOptions,
|
||||
handlersDisabled: false,
|
||||
classNames: defaultOptions.classNames,
|
||||
|
||||
initialize(options: SearchControlInputOptions) {
|
||||
if (!L) {
|
||||
throw new Error(UNINITIALIZED_ERR);
|
||||
}
|
||||
|
||||
if (!options.provider) {
|
||||
throw new Error('Provider is missing from options');
|
||||
}
|
||||
|
||||
// merge given options with control defaults
|
||||
Object.assign(this.options, options);
|
||||
Object.assign(this.classNames, options.classNames);
|
||||
|
||||
this.markers = new L.FeatureGroup();
|
||||
this.classNames.container += ` geosearch-${this.options.style}`;
|
||||
|
||||
this.searchElement = new SearchElement({
|
||||
handleSubmit: (result) => this.onSubmit(result),
|
||||
});
|
||||
|
||||
const button = createElement<HTMLAnchorElement>(
|
||||
'a',
|
||||
this.classNames.button,
|
||||
this.searchElement.container,
|
||||
{
|
||||
title: this.options.searchLabel,
|
||||
href: '#',
|
||||
onClick: (e) => this.onClick(e),
|
||||
},
|
||||
);
|
||||
|
||||
const resetButton = createElement<HTMLAnchorElement>(
|
||||
'a',
|
||||
this.classNames.resetButton,
|
||||
this.searchElement.form,
|
||||
{
|
||||
text: 'X',
|
||||
href: '#',
|
||||
onClick: () => this.clearResults(null, true),
|
||||
},
|
||||
);
|
||||
|
||||
if (this.options.autoComplete) {
|
||||
this.resultList = new ResultList({
|
||||
handleClick: ({ result }) => {
|
||||
this.searchElement.input.value = result.label;
|
||||
this.onSubmit({ query: result.label, data: result });
|
||||
},
|
||||
});
|
||||
|
||||
this.searchElement.form.appendChild(this.resultList.container);
|
||||
|
||||
this.searchElement.input.addEventListener(
|
||||
'keyup',
|
||||
debounce(
|
||||
(e: KeyboardEvent) => this.autoSearch(e),
|
||||
this.options.autoCompleteDelay,
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
this.searchElement.input.addEventListener(
|
||||
'keydown',
|
||||
(e: KeyboardEvent) => this.selectResult(e),
|
||||
true,
|
||||
);
|
||||
|
||||
this.searchElement.input.addEventListener(
|
||||
'keydown',
|
||||
(e: KeyboardEvent) => this.clearResults(e, true),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
this.searchElement.form.addEventListener(
|
||||
'mouseenter',
|
||||
(e) => this.disableHandlers(e),
|
||||
true,
|
||||
);
|
||||
|
||||
this.searchElement.form.addEventListener(
|
||||
'mouseleave',
|
||||
() => this.restoreHandlers(),
|
||||
true,
|
||||
);
|
||||
|
||||
this.searchElement.form.addEventListener(
|
||||
'click',
|
||||
(e) => e.preventDefault(),
|
||||
false,
|
||||
);
|
||||
},
|
||||
|
||||
onAdd(map: 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<HTMLDivElement>(
|
||||
'div',
|
||||
'leaflet-control-geosearch bar',
|
||||
);
|
||||
|
||||
this.container.appendChild(this.searchElement.form);
|
||||
root!.appendChild(this.container);
|
||||
}
|
||||
|
||||
return this.searchElement.container;
|
||||
},
|
||||
|
||||
onRemove() {
|
||||
this.container?.remove();
|
||||
return this;
|
||||
},
|
||||
|
||||
onClick(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.container.classList.contains('active')) {
|
||||
removeClassName(this.container, 'active');
|
||||
this.clearResults();
|
||||
} else {
|
||||
addClassName(this.container, 'active');
|
||||
this.input.focus();
|
||||
}
|
||||
},
|
||||
|
||||
disableHandlers(event) {
|
||||
if (!this.searchElement.form.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapHandlers.forEach((handler) => {
|
||||
wasHandlerEnabled[handler] = this.map[handler]?.enabled();
|
||||
this.map[handler]?.disable();
|
||||
});
|
||||
},
|
||||
|
||||
restoreHandlers(event: Event) {
|
||||
if (event && !this.searchElement.form.includes(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapHandlers.forEach((handler) => {
|
||||
if (wasHandlerEnabled[handler]) {
|
||||
this.map[handler]?.enable();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
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: KeyboardEvent | null, 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 as HTMLInputElement).value;
|
||||
const { provider } = this.options;
|
||||
|
||||
if (query.length) {
|
||||
const results = await provider!.search({ query });
|
||||
this.resultList.render(results);
|
||||
} else {
|
||||
this.resultList.clear();
|
||||
}
|
||||
},
|
||||
|
||||
async onSubmit(query) {
|
||||
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 } = this.options;
|
||||
|
||||
// @ts-ignore
|
||||
const markers = Object.keys(this.markers._layers);
|
||||
if (markers.length >= this.options.maxMarkers) {
|
||||
// @ts-ignore
|
||||
this.markers.removeLayer(markers[0]);
|
||||
}
|
||||
|
||||
const marker = this.addMarker(result, query);
|
||||
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.restoreHandlers();
|
||||
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 = new L.LatLngBounds(result.bounds);
|
||||
const bounds = resultBounds.isValid()
|
||||
? resultBounds
|
||||
: this.markers.getBounds();
|
||||
|
||||
if (!retainZoomLevel && resultBounds.isValid()) {
|
||||
this.map.fitBounds(bounds, { animate: animateZoom });
|
||||
} else {
|
||||
this.map.setView(bounds.getCenter(), this.getZoom(), {
|
||||
animate: animateZoom,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getZoom(): number {
|
||||
const { retainZoomLevel, zoomLevel } = this.options;
|
||||
return retainZoomLevel ? this.map.getZoom() : zoomLevel;
|
||||
},
|
||||
};
|
||||
|
||||
export default function SearchControl(...options: any[]) {
|
||||
if (!L) {
|
||||
throw new Error(UNINITIALIZED_ERR);
|
||||
}
|
||||
|
||||
const LControl = L.Control.extend(Control);
|
||||
return new LControl(...options);
|
||||
}
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
stopPropagation,
|
||||
replaceClassName,
|
||||
} from './domUtils';
|
||||
|
||||
import { ESCAPE_KEY, ENTER_KEY } from './constants';
|
||||
|
||||
interface SearchElementProps {
|
||||
searchLabel?: string;
|
||||
handleSubmit: () => void;
|
||||
handleSubmit: (args: { query: string }) => void;
|
||||
classNames?: {
|
||||
container?: string;
|
||||
form?: string;
|
||||
@@ -52,10 +53,10 @@ export default class SearchElement {
|
||||
type: 'text',
|
||||
placeholder: searchLabel || 'search',
|
||||
onInput: this.onInput,
|
||||
onKeyUp: this.onKeyUp,
|
||||
onKeyPress: this.onKeyPress,
|
||||
onFocus: this.onFocus,
|
||||
onBlur: this.onBlur,
|
||||
onKeyUp: (e) => this.onKeyUp(e),
|
||||
onKeyPress: (e) => this.onKeyPress(e),
|
||||
onFocus: () => this.onFocus(),
|
||||
onBlur: () => this.onBlur(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import LeafletControl from '../leafletControl';
|
||||
import SearchControl from '../SearchControl';
|
||||
import randomId from '../../test/randomId';
|
||||
|
||||
function createMapInstance(options, id = randomId()) {
|
||||
@@ -24,7 +24,7 @@ test('Can add geosearch control to leaflet', () => {
|
||||
const { div, map } = createMapInstance();
|
||||
|
||||
const provider = { search: jest.fn() };
|
||||
const control = new LeafletControl({
|
||||
const control = new SearchControl({
|
||||
provider,
|
||||
}).addTo(map);
|
||||
|
||||
@@ -35,7 +35,7 @@ test('It toggles the active class when the search button is clicked', () => {
|
||||
const { map } = createMapInstance();
|
||||
|
||||
const provider = { search: jest.fn() };
|
||||
const control = new LeafletControl({
|
||||
const control = new SearchControl({
|
||||
provider,
|
||||
}).addTo(map);
|
||||
|
||||
@@ -57,7 +57,7 @@ test('Shows result on submit', async () => {
|
||||
|
||||
const provider = { search: jest.fn(async () => result) };
|
||||
|
||||
const control = new LeafletControl({
|
||||
const control = new SearchControl({
|
||||
provider,
|
||||
}).addTo(map);
|
||||
|
||||
@@ -76,7 +76,7 @@ test('Change view on result', () => {
|
||||
|
||||
map.setView = jest.fn();
|
||||
|
||||
const control = new LeafletControl({}).addTo(map);
|
||||
const control = new SearchControl({}).addTo(map);
|
||||
|
||||
control.showResult({ x: 50, y: 0 }, { query: 'none' });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SearchElement from '../searchElement';
|
||||
import SearchElement from '../SearchElement';
|
||||
|
||||
test('Can localize texts', () => {
|
||||
const searchLabel = 'Lookup address';
|
||||
@@ -17,6 +17,10 @@ export function createElement<T extends HTMLElement = HTMLElement>(
|
||||
? key.substr(2).toLowerCase()
|
||||
: key) as keyof HTMLElementEventMap;
|
||||
el.addEventListener(type, attributes[key] as () => void);
|
||||
} else if (key === 'html') {
|
||||
el.innerHTML = attributes[key] as string;
|
||||
} else if (key === 'text') {
|
||||
el.innerText = attributes[key] as string;
|
||||
} else {
|
||||
el.setAttribute(key, attributes[key] as string);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as GeoSearchControl } from './leafletControl';
|
||||
export { default as SearchElement } from './searchElement';
|
||||
export { default as GeoSearchControl } from './SearchControl';
|
||||
export { default as SearchControl } from './SearchControl';
|
||||
export { default as SearchElement } from './SearchElement';
|
||||
|
||||
export { default as BingProvider } from './providers/bingProvider';
|
||||
export { default as EsriProvider } from './providers/esriProvider';
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
import GeoSearchElement 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}`,
|
||||
marker: {
|
||||
icon: new L.Icon.Default(),
|
||||
draggable: false,
|
||||
},
|
||||
maxMarkers: 1,
|
||||
retainZoomLevel: false,
|
||||
animateZoom: true,
|
||||
searchLabel: 'Enter address',
|
||||
notFoundMessage: 'Sorry, that address could not be found.',
|
||||
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: '',
|
||||
},
|
||||
autoComplete: true,
|
||||
autoCompleteDelay: 250,
|
||||
autoClose: false,
|
||||
keepResult: false,
|
||||
});
|
||||
|
||||
const wasHandlerEnabled = {};
|
||||
const mapHandlers = [
|
||||
'dragging',
|
||||
'touchZoom',
|
||||
'doubleClickZoom',
|
||||
'scrollWheelZoom',
|
||||
'boxZoom',
|
||||
'keyboard',
|
||||
];
|
||||
|
||||
const Control = {
|
||||
initialize(options) {
|
||||
this.markers = new L.FeatureGroup();
|
||||
this.handlersDisabled = false;
|
||||
|
||||
this.options = {
|
||||
...defaultOptions(),
|
||||
...options,
|
||||
};
|
||||
|
||||
const {
|
||||
style,
|
||||
classNames,
|
||||
searchLabel,
|
||||
autoComplete,
|
||||
autoCompleteDelay,
|
||||
} = this.options;
|
||||
if (style !== 'button') {
|
||||
this.options.classNames.container += ` ${options.style}`;
|
||||
}
|
||||
|
||||
this.searchElement = new GeoSearchElement({
|
||||
...this.options,
|
||||
handleSubmit: (query) => this.onSubmit(query),
|
||||
});
|
||||
|
||||
const { container, form, input } = this.searchElement.elements;
|
||||
|
||||
const button = createElement('a', classNames.button, container);
|
||||
button.title = searchLabel;
|
||||
button.href = '#';
|
||||
|
||||
button.addEventListener(
|
||||
'click',
|
||||
(e) => {
|
||||
this.onClick(e);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const resetButton = createElement('a', classNames.resetButton, form);
|
||||
resetButton.innerHTML = 'X';
|
||||
button.href = '#';
|
||||
resetButton.addEventListener(
|
||||
'click',
|
||||
() => {
|
||||
this.clearResults(null, true);
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
if (autoComplete) {
|
||||
this.resultList = new ResultList({
|
||||
handleClick: ({ result }) => {
|
||||
input.value = result.label;
|
||||
this.onSubmit({ query: result.label, data: result });
|
||||
},
|
||||
});
|
||||
|
||||
form.appendChild(this.resultList.elements.container);
|
||||
|
||||
input.addEventListener(
|
||||
'keyup',
|
||||
debounce((e) => this.autoSearch(e), autoCompleteDelay),
|
||||
true,
|
||||
);
|
||||
input.addEventListener('keydown', (e) => this.selectResult(e), true);
|
||||
input.addEventListener(
|
||||
'keydown',
|
||||
(e) => this.clearResults(e, true),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
form.addEventListener('mouseenter', (e) => this.disableHandlers(e), true);
|
||||
form.addEventListener('mouseleave', (e) => this.restoreHandlers(e), true);
|
||||
form.addEventListener('click', (e) => e.preventDefault(), false);
|
||||
|
||||
this.elements = { button, resetButton };
|
||||
},
|
||||
|
||||
onAdd(map) {
|
||||
const { showMarker, style } = this.options;
|
||||
|
||||
this.map = map;
|
||||
if (showMarker) {
|
||||
this.markers.addTo(map);
|
||||
}
|
||||
|
||||
if (style === 'bar') {
|
||||
const { form } = this.searchElement.elements;
|
||||
const root = map
|
||||
.getContainer()
|
||||
.querySelector('.leaflet-control-container');
|
||||
|
||||
const container = createElement('div', 'leaflet-control-geosearch bar');
|
||||
container.appendChild(form);
|
||||
root.appendChild(container);
|
||||
this.elements.container = container;
|
||||
}
|
||||
|
||||
return this.searchElement.elements.container;
|
||||
},
|
||||
|
||||
onRemove() {
|
||||
const { container } = this.elements;
|
||||
if (container) {
|
||||
container.remove();
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
onClick(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const { container, input } = this.searchElement.elements;
|
||||
|
||||
if (container.classList.contains('active')) {
|
||||
removeClassName(container, 'active');
|
||||
this.clearResults();
|
||||
} else {
|
||||
addClassName(container, 'active');
|
||||
input.focus();
|
||||
}
|
||||
},
|
||||
|
||||
disableHandlers(e) {
|
||||
const { form } = this.searchElement.elements;
|
||||
|
||||
if (this.handlersDisabled || (e && e.target !== form)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlersDisabled = true;
|
||||
mapHandlers.forEach((handler) => {
|
||||
if (this.map[handler]) {
|
||||
wasHandlerEnabled[handler] = this.map[handler].enabled();
|
||||
this.map[handler].disable();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
restoreHandlers(e) {
|
||||
const { form } = this.searchElement.elements;
|
||||
|
||||
if (!this.handlersDisabled || (e && e.target !== form)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlersDisabled = false;
|
||||
mapHandlers.forEach((handler) => {
|
||||
if (wasHandlerEnabled[handler]) {
|
||||
this.map[handler].enable();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
selectResult(event) {
|
||||
if (
|
||||
[ENTER_KEY, ARROW_DOWN_KEY, ARROW_UP_KEY].indexOf(event.keyCode) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const { input } = this.searchElement.elements;
|
||||
|
||||
const list = this.resultList;
|
||||
|
||||
if (event.keyCode === ENTER_KEY) {
|
||||
const item = list.select(list.selected);
|
||||
this.onSubmit({ query: input.value, data: item });
|
||||
return;
|
||||
}
|
||||
|
||||
const max = list.count() - 1;
|
||||
if (max < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const next =
|
||||
event.code === 'ArrowDown' ? ~~list.selected + 1 : ~~list.selected - 1;
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const idx = next < 0 ? max : next > max ? 0 : next;
|
||||
|
||||
const item = list.select(idx);
|
||||
input.value = item.label;
|
||||
},
|
||||
|
||||
clearResults(event, force = false) {
|
||||
if (event && event.keyCode !== ESCAPE_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { input } = this.searchElement.elements;
|
||||
const { keepResult, autoComplete } = this.options;
|
||||
|
||||
if (force || !keepResult) {
|
||||
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) {
|
||||
const results = await provider.search({ query });
|
||||
this.resultList.render(results);
|
||||
} else {
|
||||
this.resultList.clear();
|
||||
}
|
||||
},
|
||||
|
||||
async onSubmit(query) {
|
||||
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 } = this.options;
|
||||
|
||||
const markers = Object.keys(this.markers._layers);
|
||||
if (markers.length >= this.options.maxMarkers) {
|
||||
this.markers.removeLayer(markers[0]);
|
||||
}
|
||||
|
||||
const marker = this.addMarker(result, query);
|
||||
this.centerMap(result);
|
||||
|
||||
this.map.fireEvent('geosearch/showlocation', {
|
||||
location: result,
|
||||
marker,
|
||||
});
|
||||
|
||||
if (autoClose) {
|
||||
this.closeResults();
|
||||
}
|
||||
},
|
||||
|
||||
closeResults() {
|
||||
const { container } = this.searchElement.elements;
|
||||
|
||||
if (container.classList.contains('active')) {
|
||||
removeClassName(container, 'active');
|
||||
}
|
||||
|
||||
this.restoreHandlers();
|
||||
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 = new L.LatLngBounds(result.bounds);
|
||||
const bounds = resultBounds.isValid()
|
||||
? resultBounds
|
||||
: this.markers.getBounds();
|
||||
|
||||
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 LeafletControl(...options) {
|
||||
if (!L || !L.Control || !L.Control.extend) {
|
||||
throw new Error(
|
||||
'Leaflet must be loaded before instantiating the GeoSearch control',
|
||||
);
|
||||
}
|
||||
|
||||
const LControl = L.Control.extend(Control);
|
||||
return new LControl(...options);
|
||||
}
|
||||
@@ -49,8 +49,10 @@ export interface Provider<TRequestResult, TRawResult> {
|
||||
search(options: SearchArgument): Promise<SearchResult<TRawResult>[]>;
|
||||
}
|
||||
|
||||
export default abstract class AbstractProvider<TRequestResult, TRawResult>
|
||||
implements Provider<TRequestResult, TRawResult> {
|
||||
export default abstract class AbstractProvider<
|
||||
TRequestResult = any,
|
||||
TRawResult = any
|
||||
> implements Provider<TRequestResult, TRawResult> {
|
||||
options: ProviderOptions;
|
||||
|
||||
constructor(options: ProviderOptions = {}) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createElement, addClassName, removeClassName, cx } from './domUtils';
|
||||
import { SearchResult } from './providers/provider';
|
||||
|
||||
interface ResultListProps {
|
||||
handleClick: () => void;
|
||||
handleClick: (args: { result: SearchResult }) => void;
|
||||
classNames?: {
|
||||
container?: string;
|
||||
item?: string;
|
||||
@@ -76,6 +76,7 @@ export default class ResultList {
|
||||
if (typeof this.handleClick !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target as HTMLDivElement;
|
||||
if (
|
||||
!target ||
|
||||
|
||||
Reference in New Issue
Block a user