import { arrow, computePosition, type FlipOptions, offset, type OffsetOptions, type Placement, shift, type ShiftOptions, size, } from '@floating-ui/dom'; export type FloatingOptions = { trigger: string | HTMLElement; triggerEvent?: string[]; floatContent: string | HTMLElement; placement?: Placement; offsetOptions?: OffsetOptions; flipOptions?: FlipOptions; shiftOptions?: ShiftOptions; interactive?: boolean; showArrow?: boolean; onShow?: () => void; onHide?: () => void; syncWidth?: boolean; syncWidthMode?: 'min' | 'equal'; }; export type FloatingInstance = { destroy: () => void; hide: () => void; isVisible: () => boolean; }; export const createFloating = ({ trigger, triggerEvent, floatContent, placement = 'bottom', offsetOptions, flipOptions, shiftOptions, interactive, showArrow, onShow, onHide, syncWidth = false, syncWidthMode = 'min', }: FloatingOptions): FloatingInstance => { if (typeof trigger === 'string') { const triggerEl = document.querySelector(trigger); if (!triggerEl) { throw new Error( "element not found by document.querySelector('" + trigger + "')", ); } else { trigger = triggerEl as HTMLElement; } } let floating: HTMLElement; if (typeof floatContent === 'string') { const floatContentEl = document.querySelector(floatContent); if (!floatContentEl) { throw new Error( "element not found by document.querySelector('" + floatContent + "')", ); } else { floating = floatContentEl as HTMLElement; } } else { floating = floatContent as HTMLElement; } let arrowElement: HTMLElement; if (showArrow) { arrowElement = document.createElement('div'); arrowElement.style.position = 'absolute'; arrowElement.style.backgroundColor = '#222'; arrowElement.style.width = '8px'; arrowElement.style.height = '8px'; arrowElement.style.transform = 'rotate(45deg)'; arrowElement.style.display = 'none'; floating.firstElementChild!.before(arrowElement); } function updatePosition() { computePosition(trigger as Element, floating, { placement: placement, middleware: [ offset(offsetOptions), // 手动偏移配置 // flip(flipOptions), // 注释掉自动翻转,强制向下弹出,避免遮挡顶部工具栏 shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野) ...(showArrow ? [arrow({ element: arrowElement })] : []), ...(syncWidth ? [ size({ apply({ rects, elements }) { if (syncWidthMode === 'equal') { Object.assign(elements.floating.style, { width: `${rects.reference.width}px`, minWidth: `${rects.reference.width}px`, }); } else { Object.assign(elements.floating.style, { width: '', minWidth: `${rects.reference.width}px`, }); } }, }), ] : []), ], }).then(({ x, y, placement, middlewareData }) => { Object.assign(floating.style, { left: `${x}px`, top: `${y}px`, position: 'absolute', }); if (showArrow) { const { x: arrowX, y: arrowY } = middlewareData.arrow as { x: number; y: number; }; const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right', }[placement.split('-')[0]] as string; Object.assign(arrowElement.style, { zIndex: -1, left: arrowX != null ? `${arrowX}px` : '', top: arrowY != null ? `${arrowY}px` : '', right: '', bottom: '', [staticSide]: '2px', }); } }); } let visible = false; function showTooltip() { floating.style.display = 'block'; floating.style.visibility = 'visible'; floating.style.position = 'absolute'; if (showArrow) { arrowElement.style.display = 'block'; } visible = true; updatePosition(); onShow?.(); } function hideTooltip() { floating.style.display = 'none'; if (showArrow) { arrowElement.style.display = 'none'; } visible = false; onHide?.(); } function onTrigger(event: any) { if (!visible) { showTooltip(); } else { hideTooltip(); } } function hideTooltipCompute(event: any) { if ( floating.contains(event.target as Node) || (trigger as Node).contains(event.target as Node) ) { return; } hideTooltip(); } if (!triggerEvent || triggerEvent.length == 0) { if (interactive) { triggerEvent = ['click']; } else { triggerEvent = ['mouseenter', 'focus']; } } triggerEvent.forEach((event) => { (trigger as HTMLElement).addEventListener(event, onTrigger); }); if (interactive) { document.addEventListener('click', hideTooltipCompute); } else { ['mouseleave', 'blur'].forEach((event) => { trigger.addEventListener(event, hideTooltip); }); } return { destroy() { triggerEvent.forEach((event) => { (trigger as HTMLElement).removeEventListener(event, onTrigger); }); if (interactive) { document.removeEventListener('click', hideTooltipCompute); } else { ['mouseleave', 'blur'].forEach((event) => { trigger.removeEventListener(event, hideTooltip); }); } }, hide() { hideTooltip(); }, isVisible() { return visible; }, }; };