Files
EasyFlow/easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/createFloating.ts

194 lines
5.3 KiB
TypeScript

import {
arrow,
computePosition,
flip,
type FlipOptions,
offset,
type OffsetOptions,
type Placement,
shift,
type ShiftOptions
} 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;
};
export type FloatingInstance = {
destroy: () => void;
hide: () => void;
isVisible: () => boolean;
};
export const createFloating = ({
trigger,
triggerEvent,
floatContent,
placement = 'bottom',
offsetOptions,
flipOptions,
shiftOptions,
interactive,
showArrow
}: 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 })] : [])
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`
});
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 = 'block';
floating.style.position = 'absolute';
if (showArrow) {
arrowElement.style.display = 'block';
}
visible = true;
updatePosition();
}
function hideTooltip() {
floating.style.display = 'none';
if (showArrow) {
arrowElement.style.display = 'none';
}
visible = false;
}
function onTrigger(event: any) {
event.stopPropagation();
if (!visible) {
showTooltip();
} else {
hideTooltip();
}
}
function hideTooltipCompute(event: any) {
if (floating.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;
}
};
};