202 lines
6.0 KiB
TypeScript
202 lines
6.0 KiB
TypeScript
/*
|
|
* https://github.com/morethanwords/tweb
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
*/
|
|
|
|
import fastSmoothScroll from '../fastSmoothScroll';
|
|
import cancelEvent from './cancelEvent';
|
|
import {attachClickEvent} from './clickEvent';
|
|
import findUpAsChild from './findUpAsChild';
|
|
import findUpClassName from './findUpClassName';
|
|
|
|
type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight';
|
|
const HANDLE_EVENT = 'keydown';
|
|
const ACTIVE_CLASS_NAME = 'active';
|
|
|
|
const AXIS_Y_KEYS: ArrowKey[] = ['ArrowUp', 'ArrowDown'];
|
|
const AXIS_X_KEYS: ArrowKey[] = ['ArrowLeft', 'ArrowRight'];
|
|
|
|
export type ListNavigationOptions = {
|
|
list: HTMLElement,
|
|
type: 'xy' | 'x' | 'y',
|
|
onSelect: (target: Element) => void | boolean | Promise<boolean>,
|
|
once: boolean,
|
|
waitForKey?: string[]
|
|
};
|
|
|
|
export default function attachListNavigation({list, type, onSelect, once, waitForKey}: ListNavigationOptions) {
|
|
let waitForKeySet = waitForKey?.length ? new Set(waitForKey) : undefined;
|
|
const keyNames = new Set(type === 'xy' ? AXIS_Y_KEYS.concat(AXIS_X_KEYS) : (type === 'x' ? AXIS_X_KEYS : AXIS_Y_KEYS));
|
|
|
|
let target: Element;
|
|
const getCurrentTarget = () => {
|
|
return target || list.querySelector('.' + ACTIVE_CLASS_NAME) || list.firstElementChild;
|
|
};
|
|
|
|
const setCurrentTarget = (_target: Element, scrollTo: boolean) => {
|
|
if(target === _target) {
|
|
return;
|
|
}
|
|
|
|
let hadTarget = false;
|
|
if(target) {
|
|
hadTarget = true;
|
|
target.classList.remove(ACTIVE_CLASS_NAME);
|
|
}
|
|
|
|
target = _target;
|
|
if(!target) return;
|
|
target.classList.add(ACTIVE_CLASS_NAME);
|
|
|
|
if(hadTarget && scrollable && scrollTo) {
|
|
fastSmoothScroll({
|
|
container: scrollable,
|
|
element: target as HTMLElement,
|
|
position: 'center',
|
|
forceDuration: 100,
|
|
axis: type === 'x' ? 'x' : 'y'
|
|
});
|
|
}
|
|
};
|
|
|
|
const getNextTargetX = (currentTarget: Element, isNext: boolean) => {
|
|
let nextTarget: Element;
|
|
if(isNext) nextTarget = currentTarget.nextElementSibling || list.firstElementChild;
|
|
else nextTarget = currentTarget.previousElementSibling || list.lastElementChild;
|
|
|
|
return nextTarget;
|
|
};
|
|
|
|
const getNextTargetY = (currentTarget: Element, isNext: boolean) => {
|
|
const property = isNext ? 'nextElementSibling' : 'previousElementSibling';
|
|
const endProperty = isNext ? 'firstElementChild' : 'lastElementChild';
|
|
const currentRect = currentTarget.getBoundingClientRect();
|
|
|
|
let nextTarget = currentTarget[property] || list[endProperty];
|
|
while(nextTarget !== currentTarget) {
|
|
const targetRect = nextTarget.getBoundingClientRect();
|
|
if(targetRect.x === currentRect.x && targetRect.y !== currentRect.y) {
|
|
break;
|
|
}
|
|
|
|
nextTarget = nextTarget[property] || list[endProperty];
|
|
}
|
|
|
|
return nextTarget;
|
|
};
|
|
|
|
let handleArrowKey: (currentTarget: Element, key: ArrowKey) => Element;
|
|
if(type === 'xy') { // flex-direction: row; flex-wrap: wrap;
|
|
handleArrowKey = (currentTarget, key) => {
|
|
if(key === 'ArrowUp' || key === 'ArrowDown') return getNextTargetY(currentTarget, key === 'ArrowDown');
|
|
else return getNextTargetX(currentTarget, key === 'ArrowRight');
|
|
};
|
|
} else { // flex-direction: row | column;
|
|
handleArrowKey = (currentTarget, key) => getNextTargetX(currentTarget, key === 'ArrowRight' || key === 'ArrowDown');
|
|
}
|
|
|
|
let onKeyDown = (e: KeyboardEvent) => {
|
|
const key = e.key;
|
|
if(!keyNames.has(key as any)) {
|
|
if(key === 'Enter' || (type !== 'xy' && key === 'Tab')) {
|
|
cancelEvent(e);
|
|
fireSelect(getCurrentTarget());
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
cancelEvent(e);
|
|
|
|
if(list.childElementCount > 1) {
|
|
let currentTarget = getCurrentTarget();
|
|
currentTarget = handleArrowKey(currentTarget, key as any);
|
|
setCurrentTarget(currentTarget, true);
|
|
}
|
|
};
|
|
|
|
const scrollable = findUpClassName(list, 'scrollable');
|
|
list.classList.add('navigable-list');
|
|
|
|
const onMouseMove = (e: MouseEvent) => {
|
|
const target = findUpAsChild(e.target as HTMLElement, list) as HTMLElement;
|
|
if(!target) {
|
|
return;
|
|
}
|
|
|
|
setCurrentTarget(target, false);
|
|
};
|
|
|
|
const onClick = (e: Event) => {
|
|
cancelEvent(e); // cancel keyboard closening
|
|
|
|
const target = findUpAsChild(e.target as HTMLElement, list) as HTMLElement;
|
|
if(!target) {
|
|
return;
|
|
}
|
|
|
|
setCurrentTarget(target, false);
|
|
fireSelect(getCurrentTarget());
|
|
};
|
|
|
|
const fireSelect = async(target: Element) => {
|
|
const canContinue = await onSelect(target);
|
|
if(canContinue !== undefined ? !canContinue : once) {
|
|
detach();
|
|
}
|
|
};
|
|
|
|
let attached = false, detachClickEvent: () => void;
|
|
const attach = () => {
|
|
if(attached) return;
|
|
attached = true;
|
|
// const input = document.activeElement as HTMLElement;
|
|
// input.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
|
|
document.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
|
|
list.addEventListener('mousemove', onMouseMove, {passive: true});
|
|
detachClickEvent = attachClickEvent(list, onClick);
|
|
};
|
|
|
|
const detach = () => {
|
|
if(!attached) return;
|
|
attached = false;
|
|
// input.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
|
|
document.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
|
|
list.removeEventListener('mousemove', onMouseMove);
|
|
detachClickEvent();
|
|
detachClickEvent = undefined;
|
|
};
|
|
|
|
const resetTarget = () => {
|
|
if(waitForKeySet) return;
|
|
setCurrentTarget(list.firstElementChild, false);
|
|
};
|
|
|
|
if(waitForKeySet) {
|
|
const _onKeyDown = onKeyDown;
|
|
onKeyDown = (e) => {
|
|
if(waitForKeySet.has(e.key)) {
|
|
cancelEvent(e);
|
|
|
|
document.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
|
|
onKeyDown = _onKeyDown;
|
|
document.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
|
|
|
|
waitForKeySet = undefined;
|
|
resetTarget();
|
|
}
|
|
};
|
|
} else {
|
|
resetTarget();
|
|
}
|
|
|
|
attach();
|
|
|
|
return {
|
|
attach,
|
|
detach,
|
|
resetTarget
|
|
};
|
|
}
|