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:
Stephan Meijer
2020-04-14 16:20:55 +02:00
parent 88694884d1
commit affe9c9b8d
10 changed files with 261 additions and 202 deletions

View File

@@ -12,6 +12,7 @@ module.exports = {
},
rules: {
'@typescript-eslint/ban-ts-ignore': 0,
'@typescript-eslint/no-explicit-any': 'off',
curly: ['error', 'all'],
},
};

View File

@@ -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",

View File

@@ -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];

View File

@@ -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
View 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);
}

View File

@@ -6,7 +6,7 @@ export interface LatLng {
lng: number;
}
export interface SearchResult<TRawResult> {
export interface SearchResult<TRawResult = any> {
x: number;
y: number;
label: string;

View File

@@ -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
View 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] });
};
}

View File

@@ -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
View 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;
}
}