/* * 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 | void, openAfterLayout: () => void, opened: () => any, close: () => any, closed: () => any }> { protected element: HTMLElement; protected forceClose: boolean; protected inited: boolean; protected ignoreMouseOut: Set; protected ignoreButtons: Set; 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); } }