mirror of
https://github.com/jlengrand/leaflet-geosearch.git
synced 2026-03-10 08:31:26 +00:00
convert elements and libs to typescript
also extend the dom utils to provide support for setting eventListeners along with the attributes.
This commit is contained in:
@@ -12,6 +12,7 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-ignore': 0,
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
curly: ['error', 'all'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
"docz:serve": "docz build && docz serve",
|
||||
"docs:update": "npm run lib:build && npm run docz:build && git checkout gh-pages && find .docz/dist -name '*.js.map' -delete && cp -r .docz/dist/* . && git add . && git commit -m \"update docs\" && git checkout -"
|
||||
},
|
||||
"mangle": {
|
||||
"regex": "^_"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
|
||||
@@ -5,11 +5,4 @@ export const ARROW_UP_KEY = 38;
|
||||
export const ARROW_LEFT_KEY = 37;
|
||||
export const ARROW_RIGHT_KEY = 39;
|
||||
|
||||
export const SPECIAL_KEYS = [
|
||||
ENTER_KEY,
|
||||
ESCAPE_KEY,
|
||||
ARROW_DOWN_KEY,
|
||||
ARROW_UP_KEY,
|
||||
ARROW_LEFT_KEY,
|
||||
ARROW_RIGHT_KEY,
|
||||
];
|
||||
export const SPECIAL_KEYS = [ENTER_KEY, ESCAPE_KEY, ARROW_DOWN_KEY, ARROW_UP_KEY, ARROW_LEFT_KEY, ARROW_RIGHT_KEY];
|
||||
@@ -1,42 +0,0 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export const createElement = (element, classNames = '', parent = null, attributes = {}) => {
|
||||
const el = document.createElement(element);
|
||||
el.className = classNames;
|
||||
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
el.setAttribute(key, attributes[key]);
|
||||
});
|
||||
|
||||
if (parent) {
|
||||
parent.appendChild(el);
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
export const createScriptElement = (url, cb) => {
|
||||
const script = createElement('script', null, document.body);
|
||||
script.setAttribute('type', 'text/javascript');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
window[cb] = (json) => {
|
||||
script.remove();
|
||||
delete window[cb];
|
||||
resolve(json);
|
||||
};
|
||||
|
||||
script.setAttribute('src', url);
|
||||
});
|
||||
};
|
||||
|
||||
export const addClassName = (element, className) => {
|
||||
if (element && !element.classList.contains(className)) {
|
||||
element.classList.add(className);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeClassName = (element, className) => {
|
||||
if (element && element.classList.contains(className)) {
|
||||
element.classList.remove(className);
|
||||
}
|
||||
};
|
||||
83
src/domUtils.ts
Normal file
83
src/domUtils.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
export function createElement<T extends HTMLElement = HTMLElement>(
|
||||
element: string,
|
||||
className: string | null = '',
|
||||
parent?: Element | null,
|
||||
attributes: { [key: string]: string | ((event: any) => void) } = {},
|
||||
): T {
|
||||
const el = document.createElement(element) as T;
|
||||
|
||||
if (className) {
|
||||
el.className = className;
|
||||
}
|
||||
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
if (typeof attributes[key] === 'function') {
|
||||
// IE doesn't support startsWith
|
||||
const type = (key.indexOf('on') === 0 ? key.substr(2).toLowerCase() : key) as keyof HTMLElementEventMap;
|
||||
el.addEventListener(type, attributes[key] as () => void);
|
||||
} else {
|
||||
el.setAttribute(key, attributes[key] as string);
|
||||
}
|
||||
});
|
||||
|
||||
if (parent) {
|
||||
parent.appendChild(el);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
export function stopPropagation(event: Event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
export function createScriptElement<T = object>(url: string, cb: string): Promise<T> {
|
||||
const script = createElement('script', null, document.body);
|
||||
script.setAttribute('type', 'text/javascript');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
(window as any)[cb] = (json: T): void => {
|
||||
script.remove();
|
||||
delete (window as any)[cb];
|
||||
resolve(json);
|
||||
};
|
||||
|
||||
script.setAttribute('src', url);
|
||||
});
|
||||
}
|
||||
|
||||
export const cx = (...classNames: (string | undefined)[]): string => classNames.filter(Boolean).join(' ').trim();
|
||||
|
||||
export function addClassName(element: Element, className: string | string[]): void {
|
||||
if (!element || !element.classList) {
|
||||
return;
|
||||
}
|
||||
|
||||
// IE doesn't support adding multiple classes at once :(
|
||||
const classNames = Array.isArray(className) ? className : [className];
|
||||
classNames.forEach((name) => {
|
||||
if (!element.classList.contains(name)) {
|
||||
element.classList.add(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function removeClassName(element: Element, className: string | string[]): void {
|
||||
if (!element || !element.classList) {
|
||||
return;
|
||||
}
|
||||
|
||||
// IE doesn't support removing multiple classes at once :(
|
||||
const classNames = Array.isArray(className) ? className : [className];
|
||||
classNames.forEach((name) => {
|
||||
if (element.classList.contains(name)) {
|
||||
element.classList.remove(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function replaceClassName(element: Element, find: string, replace: string): void {
|
||||
removeClassName(element, find);
|
||||
addClassName(element, replace);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export interface LatLng {
|
||||
lng: number;
|
||||
}
|
||||
|
||||
export interface SearchResult<TRawResult> {
|
||||
export interface SearchResult<TRawResult = any> {
|
||||
x: number;
|
||||
y: number;
|
||||
label: string;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { createElement, addClassName, removeClassName } from './domUtils';
|
||||
|
||||
const cx = (...classnames) => classnames.join(' ').trim();
|
||||
|
||||
export default class ResultList {
|
||||
constructor({ handleClick = () => {}, classNames = {} } = {}) {
|
||||
this.props = { handleClick, classNames };
|
||||
this.selected = -1;
|
||||
this.results = [];
|
||||
|
||||
const container = createElement('div', cx('results', classNames.container));
|
||||
const resultItem = createElement('div', cx(classNames.item));
|
||||
|
||||
container.addEventListener('click', this.onClick, true);
|
||||
this.elements = { container, resultItem };
|
||||
}
|
||||
|
||||
render(results = []) {
|
||||
const { container, resultItem } = this.elements;
|
||||
this.clear();
|
||||
|
||||
results.forEach((result, idx) => {
|
||||
const child = resultItem.cloneNode(true);
|
||||
child.setAttribute('data-key', idx);
|
||||
child.innerHTML = result.label;
|
||||
container.appendChild(child);
|
||||
});
|
||||
|
||||
if (results.length > 0) {
|
||||
addClassName(container, 'active');
|
||||
}
|
||||
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
select(index) {
|
||||
const { container } = this.elements;
|
||||
|
||||
// eslint-disable-next-line no-confusing-arrow
|
||||
Array.from(container.children).forEach((child, idx) => (idx === index)
|
||||
? addClassName(child, 'active')
|
||||
: removeClassName(child, 'active'));
|
||||
|
||||
this.selected = index;
|
||||
return this.results[index];
|
||||
}
|
||||
|
||||
count() {
|
||||
return this.results ? this.results.length : 0;
|
||||
}
|
||||
|
||||
clear() {
|
||||
const { container } = this.elements;
|
||||
this.selected = -1;
|
||||
|
||||
while (container.lastChild) {
|
||||
container.removeChild(container.lastChild);
|
||||
}
|
||||
|
||||
removeClassName(container, 'active');
|
||||
}
|
||||
|
||||
onClick = ({ target } = {}) => {
|
||||
const { handleClick } = this.props;
|
||||
const { container } = this.elements;
|
||||
|
||||
if (target.parentNode !== container || !target.hasAttribute('data-key')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = target.getAttribute('data-key');
|
||||
const result = this.results[idx];
|
||||
handleClick({ result });
|
||||
};
|
||||
}
|
||||
82
src/resultList.ts
Normal file
82
src/resultList.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { createElement, addClassName, removeClassName, cx } from './domUtils';
|
||||
import { SearchResult } from './providers/provider';
|
||||
|
||||
interface ResultListProps {
|
||||
handleClick: () => void;
|
||||
classNames?: {
|
||||
container?: string;
|
||||
item?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default class ResultList {
|
||||
handleClick?: (args: { result: SearchResult }) => void;
|
||||
selected = -1;
|
||||
results: SearchResult[] = [];
|
||||
|
||||
container: HTMLDivElement;
|
||||
resultItem: HTMLDivElement;
|
||||
|
||||
constructor({ handleClick, classNames = {} }: ResultListProps) {
|
||||
this.handleClick = handleClick;
|
||||
|
||||
this.container = createElement<HTMLDivElement>('div', cx('results', classNames.container));
|
||||
this.container.addEventListener('click', this.onClick, true);
|
||||
|
||||
this.resultItem = createElement<HTMLDivElement>('div', cx(classNames.item));
|
||||
}
|
||||
|
||||
render(results: SearchResult[] = []): void {
|
||||
this.clear();
|
||||
|
||||
results.forEach((result, idx) => {
|
||||
const child = this.resultItem.cloneNode(true) as HTMLDivElement;
|
||||
child.setAttribute('data-key', `${idx}`);
|
||||
child.innerHTML = result.label;
|
||||
this.container.appendChild(child);
|
||||
});
|
||||
|
||||
if (results.length > 0) {
|
||||
addClassName(this.container, 'active');
|
||||
}
|
||||
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
select(index: number): SearchResult {
|
||||
// eslint-disable-next-line no-confusing-arrow
|
||||
Array.from(this.container.children).forEach((child, idx) =>
|
||||
idx === index ? addClassName(child, 'active') : removeClassName(child, 'active'),
|
||||
);
|
||||
|
||||
this.selected = index;
|
||||
return this.results[index];
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.results ? this.results.length : 0;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.selected = -1;
|
||||
|
||||
while (this.container.lastChild) {
|
||||
this.container.removeChild(this.container.lastChild);
|
||||
}
|
||||
|
||||
removeClassName(this.container, 'active');
|
||||
}
|
||||
|
||||
onClick = (event: Event): void => {
|
||||
if (typeof this.handleClick !== 'function') {
|
||||
return;
|
||||
}
|
||||
const target = event.target as HTMLDivElement;
|
||||
if (!target || !this.container.contains(target) || !target.hasAttribute('data-key')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = Number(target.getAttribute('data-key'));
|
||||
this.handleClick({ result: this.results[idx] });
|
||||
};
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { createElement, addClassName, removeClassName } from './domUtils';
|
||||
import { ESCAPE_KEY, ENTER_KEY } from './constants';
|
||||
|
||||
export default class SearchElement {
|
||||
constructor({ handleSubmit = () => {}, searchLabel = 'search', classNames = {} } = {}) {
|
||||
const container = createElement('div', ['geosearch', classNames.container].join(' '));
|
||||
const form = createElement('form', ['', classNames.form].join(' '), container, { autocomplete: 'none' });
|
||||
const input = createElement('input', ['glass', classNames.input].join(' '), form);
|
||||
|
||||
input.type = 'text';
|
||||
input.placeholder = searchLabel;
|
||||
|
||||
input.addEventListener('input', (e) => { this.onInput(e); }, false);
|
||||
input.addEventListener('keyup', (e) => { this.onKeyUp(e); }, false);
|
||||
input.addEventListener('keypress', (e) => { this.onKeyPress(e); }, false);
|
||||
input.addEventListener('focus', (e) => { this.onFocus(e); }, false);
|
||||
input.addEventListener('blur', (e) => { this.onBlur(e); }, false);
|
||||
|
||||
this.elements = { container, form, input };
|
||||
this.handleSubmit = handleSubmit;
|
||||
}
|
||||
|
||||
onFocus() {
|
||||
addClassName(this.elements.form, 'active');
|
||||
}
|
||||
|
||||
onBlur() {
|
||||
removeClassName(this.elements.form, 'active');
|
||||
}
|
||||
|
||||
async onSubmit(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const { input, container } = this.elements;
|
||||
removeClassName(container, 'error');
|
||||
addClassName(container, 'pending');
|
||||
|
||||
await this.handleSubmit({ query: input.value });
|
||||
removeClassName(container, 'pending');
|
||||
}
|
||||
|
||||
onInput() {
|
||||
const { container } = this.elements;
|
||||
|
||||
if (this.hasError) {
|
||||
removeClassName(container, 'error');
|
||||
this.hasError = false;
|
||||
}
|
||||
}
|
||||
|
||||
onKeyUp(event) {
|
||||
const { container, input } = this.elements;
|
||||
|
||||
if (event.keyCode === ESCAPE_KEY) {
|
||||
removeClassName(container, 'pending');
|
||||
removeClassName(container, 'active');
|
||||
|
||||
input.value = '';
|
||||
|
||||
document.body.focus();
|
||||
document.body.blur();
|
||||
}
|
||||
}
|
||||
|
||||
onKeyPress(event) {
|
||||
if (event.keyCode === ENTER_KEY) {
|
||||
this.onSubmit(event);
|
||||
}
|
||||
}
|
||||
|
||||
setQuery(query) {
|
||||
const { input } = this.elements;
|
||||
input.value = query;
|
||||
}
|
||||
}
|
||||
90
src/searchElement.ts
Normal file
90
src/searchElement.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createElement, addClassName, removeClassName, cx, stopPropagation, replaceClassName } from './domUtils';
|
||||
import { ESCAPE_KEY, ENTER_KEY } from './constants';
|
||||
|
||||
interface SearchElementProps {
|
||||
searchLabel?: string;
|
||||
handleSubmit: () => void;
|
||||
classNames?: {
|
||||
container?: string;
|
||||
form?: string;
|
||||
input?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default class SearchElement {
|
||||
container: HTMLDivElement;
|
||||
form: HTMLFormElement;
|
||||
input: HTMLInputElement;
|
||||
handleSubmit: (args: { query: string }) => void;
|
||||
hasError = false;
|
||||
|
||||
constructor({ handleSubmit, searchLabel, classNames = {} }: SearchElementProps) {
|
||||
this.container = createElement<HTMLDivElement>('div', cx('geosearch', classNames.container));
|
||||
|
||||
this.form = createElement<HTMLFormElement>('form', ['', classNames.form].join(' '), this.container, {
|
||||
autocomplete: 'none',
|
||||
});
|
||||
|
||||
this.input = createElement<HTMLInputElement>('input', ['glass', classNames.input].join(' '), this.form, {
|
||||
type: 'text',
|
||||
placeholder: searchLabel || 'search',
|
||||
onInput: this.onInput,
|
||||
onKeyUp: this.onKeyUp,
|
||||
onKeyPress: this.onKeyPress,
|
||||
onFocus: this.onFocus,
|
||||
onBlur: this.onBlur,
|
||||
});
|
||||
|
||||
this.handleSubmit = handleSubmit;
|
||||
}
|
||||
|
||||
onFocus(): void {
|
||||
addClassName(this.form, 'active');
|
||||
}
|
||||
|
||||
onBlur(): void {
|
||||
removeClassName(this.form, 'active');
|
||||
}
|
||||
|
||||
async onSubmit(event: Event): Promise<void> {
|
||||
stopPropagation(event);
|
||||
replaceClassName(this.container, 'error', 'pending');
|
||||
|
||||
await this.handleSubmit({ query: this.input.value });
|
||||
removeClassName(this.container, 'pending');
|
||||
}
|
||||
|
||||
onInput(): void {
|
||||
if (!this.hasError) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeClassName(this.container, 'error');
|
||||
this.hasError = false;
|
||||
}
|
||||
|
||||
onKeyUp(event: KeyboardEvent): void {
|
||||
if (event.keyCode !== ESCAPE_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeClassName(this.container, ['pending', 'active']);
|
||||
|
||||
this.input.value = '';
|
||||
|
||||
document.body.focus();
|
||||
document.body.blur();
|
||||
}
|
||||
|
||||
onKeyPress(event: KeyboardEvent): void {
|
||||
if (event.keyCode !== ENTER_KEY) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onSubmit(event);
|
||||
}
|
||||
|
||||
setQuery(query: string): void {
|
||||
this.input.value = query;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user