import type { Disposable } from '@blocksuite/global/utils'; import { autoPlacement, autoUpdate, computePosition, offset, type Rect, shift, size, } from '@floating-ui/dom'; export function listenClickAway( element: HTMLElement, onClickAway: () => void ): Disposable { const callback = (event: MouseEvent) => { const inside = event.composedPath().includes(element); if (!inside) { onClickAway(); } }; document.addEventListener('click', callback); return { dispose: () => { document.removeEventListener('click', callback); }, }; } type Display = 'show' | 'hidden'; const ATTR_SHOW = 'data-show'; /** * Using attribute 'data-show' to control popper visibility. * * ```css * selector { * display: none; * } * selector[data-show] { * display: block; * } * ``` */ export function createButtonPopper( reference: HTMLElement, popperElement: HTMLElement, stateUpdated: (state: { display: Display }) => void = () => { /** DEFAULT EMPTY FUNCTION */ }, { mainAxis, crossAxis, rootBoundary, ignoreShift, }: { mainAxis?: number; crossAxis?: number; rootBoundary?: Rect | (() => Rect | undefined); ignoreShift?: boolean; } = {} ) { let display: Display = 'hidden'; let cleanup: (() => void) | void; const originMaxHeight = window.getComputedStyle(popperElement).maxHeight; function compute() { const overflowOptions = { rootBoundary: typeof rootBoundary === 'function' ? rootBoundary() : rootBoundary, }; computePosition(reference, popperElement, { middleware: [ offset({ mainAxis: mainAxis ?? 14, crossAxis: crossAxis ?? 0, }), autoPlacement({ allowedPlacements: ['top', 'bottom'], ...overflowOptions, }), shift(overflowOptions), size({ ...overflowOptions, apply({ availableHeight }) { popperElement.style.maxHeight = originMaxHeight ? `min(${originMaxHeight}, ${availableHeight}px)` : `${availableHeight}px`; }, }), ], }) .then(({ x, y, middlewareData: data }) => { if (!ignoreShift) { x += data.shift?.x ?? 0; y += data.shift?.y ?? 0; } Object.assign(popperElement.style, { position: 'absolute', zIndex: 1, left: `${x}px`, top: `${y}px`, }); }) .catch(console.error); } const show = (force = false) => { const displayed = display === 'show'; if (displayed && !force) return; if (!displayed) { popperElement.setAttribute(ATTR_SHOW, ''); display = 'show'; stateUpdated({ display }); } cleanup?.(); cleanup = autoUpdate(reference, popperElement, compute, { animationFrame: true, }); }; const hide = () => { if (display === 'hidden') return; popperElement.removeAttribute(ATTR_SHOW); display = 'hidden'; stateUpdated({ display }); cleanup?.(); }; const toggle = () => { if (popperElement.hasAttribute(ATTR_SHOW)) { hide(); } else { show(); } }; const clickAway = listenClickAway(reference, () => hide()); return { get state() { return display; }, show, hide, toggle, dispose: () => { cleanup?.(); clickAway.dispose(); }, }; }