mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 02:26:21 +08:00
160 lines
3.4 KiB
TypeScript
160 lines
3.4 KiB
TypeScript
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();
|
|
},
|
|
};
|
|
}
|