142 lines
4.1 KiB
JavaScript
142 lines
4.1 KiB
JavaScript
|
|
// 在线名单徽章即时提示工具,负责动态徽章的悬浮气泡定位和显示。
|
||
|
|
|
||
|
|
const TOOLTIP_SELECTOR = ".user-badge-icon[data-instant-tooltip]";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 绑定在线名单徽章的即时悬浮提示。
|
||
|
|
* Vite 模块脚本通常晚于 Blade 经典脚本执行,这里由模块加载后主动绑定,避免依赖 Blade 调用时序。
|
||
|
|
*
|
||
|
|
* @param {HTMLElement|null} tooltip 提示气泡容器
|
||
|
|
* @param {Document|HTMLElement} root 事件委托根节点
|
||
|
|
* @returns {{hide: () => void, destroy: () => void}|null}
|
||
|
|
*/
|
||
|
|
export function bindInstantHoverTooltip(
|
||
|
|
tooltip = document.getElementById("chat-hover-tooltip"),
|
||
|
|
root = document,
|
||
|
|
) {
|
||
|
|
if (!tooltip || tooltip.dataset.instantTooltipBound === "1") {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
tooltip.dataset.instantTooltipBound = "1";
|
||
|
|
let activeTrigger = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 从事件目标解析可显示提示的徽章节点。
|
||
|
|
*
|
||
|
|
* @param {EventTarget|null} target 事件目标
|
||
|
|
* @returns {HTMLElement|null}
|
||
|
|
*/
|
||
|
|
const resolveTrigger = (target) => {
|
||
|
|
if (!(target instanceof Element)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return target.closest(TOOLTIP_SELECTOR);
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 把提示气泡定位到徽章旁边,同时避免超出视口边缘。
|
||
|
|
*
|
||
|
|
* @param {HTMLElement|null} trigger 当前悬浮徽章
|
||
|
|
* @returns {void}
|
||
|
|
*/
|
||
|
|
const position = (trigger) => {
|
||
|
|
if (!trigger) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const offset = 10;
|
||
|
|
const rect = trigger.getBoundingClientRect();
|
||
|
|
const tooltipWidth = tooltip.offsetWidth;
|
||
|
|
const tooltipHeight = tooltip.offsetHeight;
|
||
|
|
const fitsRight = rect.right + offset + tooltipWidth <= window.innerWidth - 8;
|
||
|
|
const side = fitsRight ? "right" : "left";
|
||
|
|
const nextLeft = fitsRight ? rect.right + offset : Math.max(8, rect.left - tooltipWidth - offset);
|
||
|
|
const nextTop = Math.min(
|
||
|
|
Math.max(8, rect.top + (rect.height - tooltipHeight) / 2),
|
||
|
|
window.innerHeight - tooltipHeight - 8,
|
||
|
|
);
|
||
|
|
|
||
|
|
tooltip.dataset.side = side;
|
||
|
|
tooltip.style.left = `${nextLeft}px`;
|
||
|
|
tooltip.style.top = `${nextTop}px`;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 显示当前徽章对应的提示文字。
|
||
|
|
*
|
||
|
|
* @param {HTMLElement|null} trigger 当前悬浮徽章
|
||
|
|
* @returns {void}
|
||
|
|
*/
|
||
|
|
const show = (trigger) => {
|
||
|
|
const tooltipText = trigger?.dataset.instantTooltip || "";
|
||
|
|
|
||
|
|
if (!trigger || !tooltipText) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
activeTrigger = trigger;
|
||
|
|
tooltip.textContent = tooltipText;
|
||
|
|
tooltip.style.display = "block";
|
||
|
|
position(trigger);
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 隐藏提示气泡并清理方向状态。
|
||
|
|
*
|
||
|
|
* @returns {void}
|
||
|
|
*/
|
||
|
|
const hide = () => {
|
||
|
|
activeTrigger = null;
|
||
|
|
tooltip.style.display = "none";
|
||
|
|
tooltip.textContent = "";
|
||
|
|
delete tooltip.dataset.side;
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleMouseOver = (event) => {
|
||
|
|
const trigger = resolveTrigger(event.target);
|
||
|
|
|
||
|
|
if (trigger) {
|
||
|
|
show(trigger);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleMouseOut = (event) => {
|
||
|
|
const trigger = resolveTrigger(event.target);
|
||
|
|
|
||
|
|
if (!trigger || trigger !== activeTrigger) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (event.relatedTarget && trigger.contains(event.relatedTarget)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
hide();
|
||
|
|
};
|
||
|
|
|
||
|
|
const reposition = () => {
|
||
|
|
if (activeTrigger) {
|
||
|
|
position(activeTrigger);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
root.addEventListener("mouseover", handleMouseOver);
|
||
|
|
root.addEventListener("mouseout", handleMouseOut);
|
||
|
|
window.addEventListener("scroll", reposition, true);
|
||
|
|
window.addEventListener("resize", reposition);
|
||
|
|
|
||
|
|
return {
|
||
|
|
hide,
|
||
|
|
destroy() {
|
||
|
|
root.removeEventListener("mouseover", handleMouseOver);
|
||
|
|
root.removeEventListener("mouseout", handleMouseOut);
|
||
|
|
window.removeEventListener("scroll", reposition, true);
|
||
|
|
window.removeEventListener("resize", reposition);
|
||
|
|
delete tooltip.dataset.instantTooltipBound;
|
||
|
|
hide();
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|