ca1213c32f
Fix chat date blinking Fix displaying sent messages to new dialog Scroll to date bubble if message is bigger than viewport Fix releasing keyboard by inline helper Fix clearing self user Fix displaying sent public poll Update contacts counter in dialogs placeholder Improve multiselect animation Disable lottie icon animations if they're disabled Fix changing mtproto transport during authorization
198 lines
6.0 KiB
TypeScript
198 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, detachClickEvent } 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 default function attachListNavigation({list, type, onSelect, once, waitForKey}: {
|
|
list: HTMLElement,
|
|
type: 'xy' | 'x' | 'y',
|
|
onSelect: (target: Element) => void | boolean,
|
|
once: boolean,
|
|
waitForKey?: string
|
|
}) {
|
|
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, list) as HTMLElement;
|
|
if(!target) {
|
|
return;
|
|
}
|
|
|
|
setCurrentTarget(target, false);
|
|
};
|
|
|
|
const onClick = (e: Event) => {
|
|
cancelEvent(e); // cancel keyboard closening
|
|
|
|
const target = findUpAsChild(e.target, list) as HTMLElement;
|
|
if(!target) {
|
|
return;
|
|
}
|
|
|
|
setCurrentTarget(target, false);
|
|
fireSelect(getCurrentTarget());
|
|
};
|
|
|
|
const fireSelect = (target: Element) => {
|
|
const canContinue = onSelect(target);
|
|
if(canContinue !== undefined ? !canContinue : once) {
|
|
detach();
|
|
}
|
|
};
|
|
|
|
let attached = false;
|
|
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});
|
|
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(list, onClick);
|
|
};
|
|
|
|
const resetTarget = () => {
|
|
if(waitForKey) return;
|
|
setCurrentTarget(list.firstElementChild, false);
|
|
};
|
|
|
|
if(waitForKey) {
|
|
const _onKeyDown = onKeyDown;
|
|
onKeyDown = (e) => {
|
|
if(e.key === waitForKey) {
|
|
cancelEvent(e);
|
|
|
|
document.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
|
|
onKeyDown = _onKeyDown;
|
|
document.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
|
|
|
|
waitForKey = undefined;
|
|
resetTarget();
|
|
}
|
|
};
|
|
} else {
|
|
resetTarget();
|
|
}
|
|
|
|
attach();
|
|
|
|
return {
|
|
attach,
|
|
detach,
|
|
resetTarget
|
|
};
|
|
}
|