Files
EasyFlow/easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/createFloating.ts
陈子默 7e7c236c2a fix: 修复管理端前端 lint 与构建问题
- 收敛 easyflow-ui-admin 的 lint、格式和类型问题

- 修正 demo 页面与管理端前端构建失败点

- 验证 pnpm lint 与 pnpm build 均已通过
2026-04-05 21:39:13 +08:00

233 lines
5.6 KiB
TypeScript

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;
},
};
};