// Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { RefObject } from 'react'; import * as log from '../logging/log'; export type HandlerType = (target: Node) => boolean; export type HandlersType = { name: string; handleClick: HandlerType; handlePointerDown: HandlerType; }; export type ContainerElementType = Node | RefObject | null | undefined; // TODO(indutny): DESKTOP-4177 // A stack of handlers. Handlers are executed from the top to the bottom const fakeHandlers = new Array(); export type HandleOutsideClickOptionsType = Readonly<{ name: string; containerElements: ReadonlyArray; }>; function handleGlobalPointerDown(event: MouseEvent) { for (const handlers of fakeHandlers) { // continue even if handled, so that we can detect if the click was inside handlers.handlePointerDown(event.target as Node); } } function handleGlobalClick(event: MouseEvent) { for (const handlers of fakeHandlers.slice().reverse()) { const handled = handlers.handleClick(event.target as Node); if (handled) { log.info(`handleOutsideClick: ${handlers.name} handled click`); break; } } } const eventOptions = { capture: true }; export const handleOutsideClick = ( handler: HandlerType, { name, containerElements }: HandleOutsideClickOptionsType ): (() => void) => { function isInside(target: Node) { return containerElements.some(elem => { if (!elem) { return false; } if (elem instanceof Node) { return elem.contains(target); } return elem.current?.contains(target); }); } let startedInside = false; function handlePointerDown(target: Node) { startedInside = isInside(target); return false; } function handleClick(target: Node) { const endedInside = isInside(target); // Clicked inside of one of container elements - stop processing if (startedInside || endedInside) { return true; } // Stop processing if requested by handler function return handler(target); } const fakeHandler = { name, handleClick, handlePointerDown, }; fakeHandlers.push(fakeHandler); if (fakeHandlers.length === 1) { document.addEventListener( 'pointerdown', handleGlobalPointerDown, eventOptions ); document.addEventListener('click', handleGlobalClick, eventOptions); } return () => { const index = fakeHandlers.indexOf(fakeHandler); fakeHandlers.splice(index, 1); if (fakeHandlers.length === 0) { document.removeEventListener( 'pointerdown', handleGlobalPointerDown, eventOptions ); document.removeEventListener('click', handleGlobalClick, eventOptions); } }; };