194 lines
5.3 KiB
TypeScript
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;
|
|
}
|
|
};
|
|
};
|