252 lines
7.2 KiB
TypeScript
252 lines
7.2 KiB
TypeScript
/*
|
|
* https://github.com/morethanwords/tweb
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
*/
|
|
|
|
import {attachClickEvent} from './dom/clickEvent';
|
|
import findUpAsChild from './dom/findUpAsChild';
|
|
import EventListenerBase from './eventListenerBase';
|
|
import ListenerSetter from './listenerSetter';
|
|
import IS_TOUCH_SUPPORTED from '../environment/touchSupport';
|
|
import safeAssign from './object/safeAssign';
|
|
import appNavigationController, {NavigationItem} from '../components/appNavigationController';
|
|
import findUpClassName from './dom/findUpClassName';
|
|
import rootScope from '../lib/rootScope';
|
|
import liteMode from './liteMode';
|
|
|
|
const KEEP_OPEN = false;
|
|
const TOGGLE_TIMEOUT = 200;
|
|
const ANIMATION_DURATION = 200;
|
|
|
|
export type IgnoreMouseOutType = 'click' | 'menu' | 'popup';
|
|
type DropdownHoverTimeoutType = 'toggle' | 'done';
|
|
|
|
export default class DropdownHover extends EventListenerBase<{
|
|
open: () => Promise<any> | void,
|
|
openAfterLayout: () => void,
|
|
opened: () => any,
|
|
close: () => any,
|
|
closed: () => any
|
|
}> {
|
|
protected element: HTMLElement;
|
|
protected forceClose: boolean;
|
|
protected inited: boolean;
|
|
protected ignoreMouseOut: Set<IgnoreMouseOutType>;
|
|
protected ignoreButtons: Set<HTMLElement>;
|
|
protected navigationItem: NavigationItem;
|
|
protected ignoreOutClickClassName: string;
|
|
protected timeouts: {[type in DropdownHoverTimeoutType]?: number};
|
|
protected detachClickEvent: () => void;
|
|
|
|
constructor(options: {
|
|
element: DropdownHover['element'],
|
|
ignoreOutClickClassName?: string
|
|
}) {
|
|
super(false);
|
|
safeAssign(this, options);
|
|
this.forceClose = false;
|
|
this.inited = false;
|
|
this.ignoreMouseOut = new Set();
|
|
this.ignoreButtons = new Set();
|
|
this.timeouts = {};
|
|
}
|
|
|
|
public attachButtonListener(
|
|
button: HTMLElement,
|
|
listenerSetter: ListenerSetter
|
|
) {
|
|
let firstTime = true;
|
|
if(IS_TOUCH_SUPPORTED) {
|
|
attachClickEvent(button, () => {
|
|
if(firstTime) {
|
|
firstTime = false;
|
|
this.toggle(true);
|
|
} else {
|
|
this.toggle();
|
|
}
|
|
}, {listenerSetter});
|
|
} else {
|
|
listenerSetter.add(button)('mouseover', (e) => {
|
|
if(firstTime) {
|
|
listenerSetter.add(button)('mouseout', (e) => {
|
|
this.clearTimeout('toggle');
|
|
this.onMouseOut(e);
|
|
});
|
|
firstTime = false;
|
|
}
|
|
|
|
this.setTimeout('toggle', () => {
|
|
this.toggle(true);
|
|
}, TOGGLE_TIMEOUT);
|
|
});
|
|
|
|
attachClickEvent(button, () => {
|
|
const type: IgnoreMouseOutType = 'click';
|
|
const ignore = !this.ignoreMouseOut.has(type);
|
|
|
|
if(ignore && !this.ignoreMouseOut.size) {
|
|
this.ignoreButtons.add(button);
|
|
setTimeout(() => {
|
|
this.detachClickEvent = attachClickEvent(window, this.onClickOut, {capture: true});
|
|
}, 0);
|
|
}
|
|
|
|
this.setIgnoreMouseOut(type, ignore);
|
|
this.toggle(ignore);
|
|
}, {listenerSetter});
|
|
}
|
|
}
|
|
|
|
protected onClickOut = (e: MouseEvent) => {
|
|
const target = e.target as HTMLElement;
|
|
if(
|
|
!findUpAsChild(target, this.element) &&
|
|
!Array.from(this.ignoreButtons).some((button) => findUpAsChild(target, button) || target === button) &&
|
|
this.ignoreMouseOut.size <= 1 &&
|
|
(!this.ignoreOutClickClassName || !findUpClassName(target, this.ignoreOutClickClassName))
|
|
) {
|
|
this.toggle(false);
|
|
}
|
|
};
|
|
|
|
protected onMouseOut = (e: MouseEvent) => {
|
|
if(KEEP_OPEN || !this.isActive()) return;
|
|
this.clearTimeout('toggle');
|
|
|
|
if(this.ignoreMouseOut.size) {
|
|
return;
|
|
}
|
|
|
|
const toElement = (e as any).toElement as HTMLElement;
|
|
if(toElement && findUpAsChild(toElement, this.element)) {
|
|
return;
|
|
}
|
|
|
|
this.setTimeout('toggle', () => {
|
|
this.toggle(false);
|
|
}, TOGGLE_TIMEOUT);
|
|
};
|
|
|
|
protected clearTimeout(type: DropdownHoverTimeoutType) {
|
|
if(this.timeouts[type] !== undefined) {
|
|
clearTimeout(this.timeouts[type]);
|
|
delete this.timeouts[type];
|
|
}
|
|
}
|
|
|
|
protected setTimeout(type: DropdownHoverTimeoutType, cb: () => void, timeout: number) {
|
|
this.clearTimeout(type);
|
|
this.timeouts[type] = window.setTimeout(() => {
|
|
this.clearTimeout(type);
|
|
cb();
|
|
}, timeout);
|
|
}
|
|
|
|
public init() {
|
|
if(!IS_TOUCH_SUPPORTED) {
|
|
this.element.onmouseout = this.onMouseOut;
|
|
this.element.onmouseover = (e) => {
|
|
if(this.forceClose) {
|
|
return;
|
|
}
|
|
|
|
// console.log('onmouseover element');
|
|
this.clearTimeout('toggle');
|
|
};
|
|
}
|
|
}
|
|
|
|
public toggle = async(enable?: boolean) => {
|
|
// if(!this.element) return;
|
|
const willBeActive = (!!this.element.style.display && enable === undefined) || enable;
|
|
if(this.init) {
|
|
if(willBeActive) {
|
|
this.init();
|
|
this.init = null;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(willBeActive === this.isActive()) {
|
|
return;
|
|
}
|
|
|
|
const delay = IS_TOUCH_SUPPORTED || !liteMode.isAvailable('animations') ? 0 : ANIMATION_DURATION;
|
|
if((this.element.style.display && enable === undefined) || enable) {
|
|
const res = this.dispatchResultableEvent('open');
|
|
await Promise.all(res);
|
|
|
|
this.element.style.display = '';
|
|
void this.element.offsetLeft; // reflow
|
|
this.element.classList.add('active');
|
|
|
|
this.dispatchEvent('openAfterLayout');
|
|
|
|
appNavigationController.pushItem(this.navigationItem = {
|
|
type: 'dropdown',
|
|
onPop: () => {
|
|
this.toggle(false);
|
|
}
|
|
});
|
|
|
|
this.clearTimeout('toggle');
|
|
this.setTimeout('done', () => {
|
|
this.forceClose = false;
|
|
this.dispatchEvent('opened');
|
|
}, delay);
|
|
|
|
// ! can't use together with resizeObserver
|
|
/* if(isTouchSupported) {
|
|
const height = this.element.scrollHeight + appImManager.chat.input.inputContainer.scrollHeight - 10;
|
|
console.log('[ESG]: toggle: enable height', height);
|
|
appImManager.chat.bubbles.scrollable.scrollTop += height;
|
|
} */
|
|
|
|
/* if(touchSupport) {
|
|
this.restoreScroll();
|
|
} */
|
|
} else {
|
|
this.dispatchEvent('close');
|
|
this.ignoreMouseOut.clear();
|
|
this.ignoreButtons.clear();
|
|
|
|
this.element.classList.remove('active');
|
|
|
|
appNavigationController.removeItem(this.navigationItem);
|
|
this.detachClickEvent?.();
|
|
this.detachClickEvent = undefined;
|
|
|
|
this.clearTimeout('toggle');
|
|
this.setTimeout('done', () => {
|
|
this.element.style.display = 'none';
|
|
this.forceClose = false;
|
|
this.dispatchEvent('closed');
|
|
}, delay);
|
|
|
|
/* if(isTouchSupported) {
|
|
const scrollHeight = this.container.scrollHeight;
|
|
if(scrollHeight) {
|
|
const height = this.container.scrollHeight + appImManager.chat.input.inputContainer.scrollHeight - 10;
|
|
appImManager.chat.bubbles.scrollable.scrollTop -= height;
|
|
}
|
|
} */
|
|
|
|
/* if(touchSupport) {
|
|
this.restoreScroll();
|
|
} */
|
|
}
|
|
|
|
// animationIntersector.checkAnimations(false, EMOTICONSSTICKERGROUP);
|
|
};
|
|
|
|
public isActive() {
|
|
return this.element.classList.contains('active');
|
|
}
|
|
|
|
public setIgnoreMouseOut(type: IgnoreMouseOutType, ignore: boolean) {
|
|
ignore ? this.ignoreMouseOut.add(type) : this.ignoreMouseOut.delete(type);
|
|
}
|
|
}
|