import { RefObject } from 'react'; import { useState, useEffect, useCallback } from '../lib/teact/teact'; import { IAnchorPosition } from '../types'; import { IS_TOUCH_ENV, IS_SINGLE_COLUMN_LAYOUT } from '../util/environment'; const LONG_TAP_DURATION_MS = 150; const SELECTION_ANIMATION_DURATION_MS = 200; let contextMenuCounter = 0; function checkIsDisabledForMobile() { return IS_SINGLE_COLUMN_LAYOUT && window.document.body.classList.contains('enable-symbol-menu-transforms'); } export default ( elementRef: RefObject, isMenuDisabled?: boolean, shouldDisableOnLink?: boolean, shouldDisableOnLongTap?: boolean, ) => { const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); const [contextMenuPosition, setContextMenuPosition] = useState(undefined); const handleBeforeContextMenu = useCallback((e: React.MouseEvent) => { if (!isMenuDisabled && e.button === 2) { document.body.classList.add('no-selection'); } }, [isMenuDisabled]); const handleContextMenu = useCallback((e: React.MouseEvent) => { document.body.classList.remove('no-selection'); if (isMenuDisabled || (shouldDisableOnLink && (e.target as HTMLElement).matches('a.text-entity-link[href]'))) { return; } e.preventDefault(); if (contextMenuPosition) { return; } document.body.classList.remove('no-selection'); if (contextMenuCounter === 0) { document.body.classList.add('has-context-menu'); } contextMenuCounter++; setIsContextMenuOpen(true); setContextMenuPosition({ x: e.clientX, y: e.clientY }); }, [isMenuDisabled, shouldDisableOnLink, contextMenuPosition]); const handleContextMenuClose = useCallback(() => { setIsContextMenuOpen(false); }, []); const handleContextMenuHide = useCallback(() => { setContextMenuPosition(undefined); document.body.classList.remove('no-selection'); setTimeout(() => { contextMenuCounter--; if (contextMenuCounter === 0) { document.body.classList.remove('has-context-menu'); } }, SELECTION_ANIMATION_DURATION_MS); }, []); // Support context menu on touch-devices useEffect(() => { if (isMenuDisabled || !IS_TOUCH_ENV || shouldDisableOnLongTap) { return undefined; } const element = elementRef.current; if (!element) { return undefined; } let timer: number | undefined; const clearLongPressTimer = () => { if (timer) { clearTimeout(timer); timer = undefined; } }; const emulateContextMenuEvent = (originalEvent: TouchEvent) => { clearLongPressTimer(); const { clientX, clientY, target } = originalEvent.touches[0]; if (contextMenuPosition || (shouldDisableOnLink && (target as HTMLElement).matches('a.text-entity-link[href]'))) { return; } // temporarily intercept and clear the next click element.addEventListener('touchend', function cancelClickOnce(e) { element.removeEventListener('touchend', cancelClickOnce, true); e.stopImmediatePropagation(); e.preventDefault(); e.stopPropagation(); }, true); document.body.classList.add('no-selection'); setIsContextMenuOpen(true); setContextMenuPosition({ x: clientX, y: clientY }); }; const startLongPressTimer = (e: TouchEvent) => { if (isMenuDisabled || checkIsDisabledForMobile()) { return; } clearLongPressTimer(); timer = window.setTimeout(() => emulateContextMenuEvent(e), LONG_TAP_DURATION_MS); }; // @perf Consider event delegation element.addEventListener('touchstart', startLongPressTimer, { passive: true }); element.addEventListener('touchcancel', clearLongPressTimer, true); element.addEventListener('touchend', clearLongPressTimer, true); element.addEventListener('touchmove', clearLongPressTimer, { passive: true }); return () => { clearLongPressTimer(); element.removeEventListener('touchstart', startLongPressTimer); element.removeEventListener('touchcancel', clearLongPressTimer, true); element.removeEventListener('touchend', clearLongPressTimer, true); element.removeEventListener('touchmove', clearLongPressTimer); }; }, [contextMenuPosition, isMenuDisabled, shouldDisableOnLongTap, elementRef, shouldDisableOnLink]); return { isContextMenuOpen, contextMenuPosition, handleBeforeContextMenu, handleContextMenu, handleContextMenuClose, handleContextMenuHide, }; };