MediaWiki:Gadget-UnihanTooltips.js:修订间差异
外观
小无编辑摘要 |
小 // Edit via Wikiplus |
||
| (未显示同一用户的1个中间版本) | |||
| 第1行: | 第1行: | ||
/* | /* | ||
本小工具可以將[[Template:僻字]]的提示由原來的title提示改為元素式彈出提示,使觸控式裝置可以觀看有關提示 | 本小工具可以將[[Template:僻字]]的提示由原來的title提示改為元素式彈出提示,使觸控式裝置可以觀看有關提示 | ||
适配说明:兼容MediaWiki 1. | 适配说明:兼容MediaWiki 1.43+,修复模块依赖错误,移除jquery.ui.animate依赖,优化动画逻辑 | ||
*/ | */ | ||
// 显式传入jQuery和mediaWiki,避免$冲突 | // 显式传入jQuery和mediaWiki,避免$冲突 | ||
(function($, mw) { | (function($, mw) { | ||
"use strict"; | "use strict"; | ||
/*===== | /*===== 初始化检查:控制工具是否加载 =====*/ | ||
// 获取URL参数"UTdontload"(0=清除不加载状态,数字=设置不加载状态) | // 获取URL参数"UTdontload"(0=清除不加载状态,数字=设置不加载状态) | ||
const dontLoadParam = mw.util.getParamValue("UTdontload"); | const dontLoadParam = mw.util.getParamValue("UTdontload"); | ||
if (dontLoadParam !== null) { | if (dontLoadParam !== null) { | ||
const paramVal = parseInt(dontLoadParam, 10); | const paramVal = parseInt(dontLoadParam, 10); | ||
if (!isNaN(paramVal)) { | if (!isNaN(paramVal)) { | ||
paramVal === 0 ? mw.storage.remove("UTdontload") : mw.storage.set("UTdontload", "1"); | |||
} | } | ||
} | } | ||
// 若本地存储存在"不加载" | // 若本地存储存在"不加载"状态,直接终止 | ||
if (mw.storage.get("UTdontload") === "1") { | if (mw.storage.get("UTdontload") === "1") { | ||
return; | return; | ||
} | } | ||
/*===== | /*===== 主初始化:命名空间与容器 =====*/ | ||
const canonicalNamespace = mw.config.get("wgCanonicalNamespace"); | const canonicalNamespace = mw.config.get("wgCanonicalNamespace"); | ||
const allowedNamespaces = ["", "Project", "Help"]; | const allowedNamespaces = ["", "Project", "Help"]; // 主/项目/帮助命名空间 | ||
if (!allowedNamespaces.includes(canonicalNamespace)) { | if (!allowedNamespaces.includes(canonicalNamespace)) { | ||
return; | return; | ||
} | } | ||
// | // 获取内容容器(优先mw-content-text,兼容旧版本) | ||
const $bodyContent = $("#mw-content-text").length ? $("#mw-content-text") : $("body"); | const $bodyContent = $("#mw-content-text").length ? $("#mw-content-text") : $("body"); | ||
const bodyContent = $bodyContent[0]; | const bodyContent = $bodyContent[0]; | ||
// | /*===== 设备类型检测 =====*/ | ||
const isTouchscreen = window.matchMedia("(hover: none), (pointer: coarse)").matches || | const isTouchscreen = window.matchMedia("(hover: none), (pointer: coarse)").matches || | ||
mw.config.get("wgDisplayResolution") === "mobile" || | mw.config.get("wgDisplayResolution") === "mobile" || | ||
"ontouchstart" in document.documentElement; | "ontouchstart" in document.documentElement; | ||
const hoverDelay = isTouchscreen ? 0 : 200; // | const hoverDelay = isTouchscreen ? 0 : 200; // 触摸设备无延迟 | ||
/*===== | /*===== 工具函数:弹窗核心逻辑 =====*/ | ||
/** | /** | ||
* | * 初始化元素数据(存储到元素data中) | ||
*/ | */ | ||
function initElementData(element) { | function initElementData(element) { | ||
let tooltipData = $(element).data("tooltipData"); | let tooltipData = $(element).data("tooltipData"); | ||
if (!tooltipData) { | if (!tooltipData) { | ||
const originalTitle = element.title || ""; | const originalTitle = element.title || ""; | ||
element.title = ""; | element.title = ""; // 清空原生title提示 | ||
$(element).addClass("UHTarget"); // 添加高亮类 | |||
$(element).addClass("UHTarget"); | |||
tooltipData = { | tooltipData = { | ||
originalTitle: originalTitle, | originalTitle: originalTitle, | ||
tooltipNode: null, | tooltipNode: null, | ||
hideTimer: null, | hideTimer: null, | ||
showTimer: null | showTimer: null | ||
}; | }; | ||
$(element).data("tooltipData", tooltipData); | $(element).data("tooltipData", tooltipData); | ||
| 第73行: | 第60行: | ||
/** | /** | ||
* | * 创建弹窗内容(支持多行文本分割) | ||
*/ | */ | ||
function createContentElement(originalTitle) { | function createContentElement(originalTitle) { | ||
const $contentContainer = $("<div>"); | const $contentContainer = $("<div>"); | ||
originalTitle.split("\n").forEach(line => { | originalTitle.split("\n").forEach(line => { | ||
const trimmedLine = line.trim(); | const trimmedLine = line.trim(); | ||
| 第90行: | 第74行: | ||
/** | /** | ||
* | * 创建弹窗DOM结构(包含内容区和箭头) | ||
*/ | */ | ||
function createTooltipNode(originalTitle) { | function createTooltipNode(originalTitle) { | ||
const tooltipNode = document.createElement("ul"); | const tooltipNode = document.createElement("ul"); | ||
tooltipNode.className = "unihantooltip"; | tooltipNode.className = "unihantooltip"; | ||
// | // 内容区(包含提示文本和设置图标) | ||
const $contentLi = $("<li>"); | const $contentLi = $("<li>"); | ||
const contentElement = createContentElement(originalTitle); | const contentElement = createContentElement(originalTitle); | ||
const $settingsIcon = $("<div>") | const $settingsIcon = $("<div>") | ||
.addClass("UHsettings") | .addClass("UHsettings") | ||
.attr("title", "设置") | .attr("title", "设置") | ||
.on("click", (e) => { | .on("click", (e) => { | ||
e.stopPropagation(); // | e.stopPropagation(); | ||
// 示例:设置功能入口 | |||
alert("僻字提示设置(可扩展)"); | |||
}); | }); | ||
// | // 组装内容区和箭头 | ||
$contentLi.append(contentElement).append($settingsIcon); | $contentLi.append(contentElement).append($settingsIcon); | ||
const $arrowLi = $("<li>"); | const $arrowLi = $("<li>"); | ||
$(tooltipNode).append($contentLi).append($arrowLi); | $(tooltipNode).append($contentLi).append($arrowLi); | ||
// 非触摸设备:绑定弹窗悬停事件 | // 非触摸设备:绑定弹窗悬停事件 | ||
if (!isTouchscreen) { | if (!isTouchscreen) { | ||
$(tooltipNode) | const tooltipData = { hideTimer: null, showTimer: null }; | ||
$(tooltipNode).data("tooltipData", tooltipData) | |||
.on("mouseenter", function() { | .on("mouseenter", function() { | ||
showTooltip($(this).data("tooltipData"), this); | |||
}) | }) | ||
.on("mouseleave", function() { | .on("mouseleave", function() { | ||
hideTooltip($(this).data("tooltipData"), this); | |||
}); | }); | ||
} | } | ||
| 第137行: | 第113行: | ||
/** | /** | ||
* | * 弹窗定位(避免超出视口,精准对齐) | ||
*/ | */ | ||
function positionTooltip(element, tooltipNode) { | function positionTooltip(element, tooltipNode) { | ||
| 第145行: | 第119行: | ||
const $tooltip = $(tooltipNode); | const $tooltip = $(tooltipNode); | ||
// | // 离线计算尺寸(避免布局抖动) | ||
$tooltip.css({ | $tooltip.css({ visibility: "hidden", display: "block" }).appendTo(bodyContent); | ||
// | // 元素与视口信息 | ||
const elementOffset = $element.offset(); | const elementOffset = $element.offset(); | ||
const elementWidth = $element.outerWidth(); | const elementWidth = $element.outerWidth(); | ||
const elementHeight = $element.outerHeight(); | const elementHeight = $element.outerHeight(); | ||
const tooltipWidth = $tooltip.outerWidth(); | const tooltipWidth = $tooltip.outerWidth(); | ||
const contentHeight = $tooltip.find("li:first-child").outerHeight(); | const contentHeight = $tooltip.find("li:first-child").outerHeight(); | ||
const arrowHeight = 12; // | const arrowHeight = 12; // 与CSS箭头高度一致 | ||
const viewport = { | const viewport = { | ||
top: $(window).scrollTop(), | top: $(window).scrollTop(), | ||
| 第169行: | 第136行: | ||
}; | }; | ||
/ | // 垂直定位(优先上方,不足则翻转) | ||
let top = elementOffset.top - contentHeight - arrowHeight; | let top = elementOffset.top - contentHeight - arrowHeight; | ||
const needFlip = top < viewport.top || (elementOffset.top + contentHeight + arrowHeight) > viewport.bottom; | const needFlip = top < viewport.top || (elementOffset.top + contentHeight + arrowHeight) > viewport.bottom; | ||
$tooltip.toggleClass("UHflipped", needFlip); | $tooltip.toggleClass("UHflipped", needFlip); | ||
if (needFlip) { | if (needFlip) { | ||
top = elementOffset.top + elementHeight + arrowHeight - | top = elementOffset.top + elementHeight + arrowHeight - 2; // 适配CSS翻转间距 | ||
} | } | ||
top = Math.max(viewport.top, Math.min(top, viewport.bottom - contentHeight)); | top = Math.max(viewport.top, Math.min(top, viewport.bottom - contentHeight)); | ||
/ | // 水平定位(居中对齐,留边距) | ||
let left = elementOffset.left + (elementWidth - tooltipWidth) / 2; | let left = elementOffset.left + (elementWidth - tooltipWidth) / 2; | ||
left = Math.max(viewport.left + 10, Math.min(left, viewport.right - tooltipWidth - 10)); | left = Math.max(viewport.left + 10, Math.min(left, viewport.right - tooltipWidth - 10)); | ||
/ | // 箭头定位(指向元素中心) | ||
const arrowOffset = elementOffset.left - left + elementWidth / 2 - 7; | const arrowOffset = elementOffset.left - left + elementWidth / 2 - 7; | ||
$tooltip.find("li:last-child").css("margin- | $tooltip.find("li:last-child").css("margin-inline-start", `${arrowOffset}px`); | ||
/ | // 应用定位并显示 | ||
$tooltip.css({ | $tooltip.css({ top: `${top}px`, left: `${left}px`, visibility: "visible", display: "" }); | ||
} | } | ||
/** | /** | ||
* | * 显示弹窗(依赖CSS过渡,无JS动画依赖) | ||
*/ | */ | ||
function showTooltip(tooltipData, tooltipNode) { | function showTooltip(tooltipData, tooltipNode) { | ||
const $tooltip = $(tooltipNode); | |||
// 确保弹窗已插入DOM | // 确保弹窗已插入DOM | ||
if (!tooltipNode.parentNode || tooltipNode.parentNode.nodeType === 11) { | if (!tooltipNode.parentNode || tooltipNode.parentNode.nodeType === 11) { | ||
bodyContent.appendChild(tooltipNode); | bodyContent.appendChild(tooltipNode); | ||
} | } | ||
// | // 添加可见类(触发CSS渲染) | ||
$( | $tooltip.addClass("is-visible"); | ||
// 直接设置透明度(CSS过渡自动处理动画) | |||
$tooltip.stop().css("opacity", 1); | |||
// 清除隐藏定时器 | // 清除隐藏定时器 | ||
if (tooltipData.hideTimer) { | if (tooltipData.hideTimer) { | ||
| 第219行: | 第178行: | ||
/** | /** | ||
* | * 隐藏弹窗(依赖CSS过渡,无JS动画依赖) | ||
*/ | */ | ||
function hideTooltip(tooltipData, tooltipNode) { | function hideTooltip(tooltipData, tooltipNode) { | ||
const $tooltip = $(tooltipNode); | |||
// 清除显示定时器 | // 清除显示定时器 | ||
if (tooltipData.showTimer) { | if (tooltipData.showTimer) { | ||
| 第229行: | 第187行: | ||
tooltipData.showTimer = null; | tooltipData.showTimer = null; | ||
} | } | ||
// | // 延迟隐藏(防误触) | ||
tooltipData.hideTimer = setTimeout(() => { | tooltipData.hideTimer = setTimeout(() => { | ||
// | // 触发淡出(CSS过渡处理) | ||
$( | $tooltip.stop().css("opacity", 0); | ||
// 过渡结束后清理 | |||
setTimeout(() => { | |||
$tooltip.removeClass("is-visible"); | |||
if (bodyContent.contains(tooltipNode)) { | if (bodyContent.contains(tooltipNode)) { | ||
bodyContent.removeChild(tooltipNode); | bodyContent.removeChild(tooltipNode); | ||
} | } | ||
}); | }, 150); // 与CSS transition时长一致 | ||
}, isTouchscreen ? 16 : 100); | }, isTouchscreen ? 16 : 100); | ||
} | } | ||
/** | /** | ||
* | * 触摸设备:点击外部关闭弹窗 | ||
*/ | */ | ||
function setupClickOutsideHandler(tooltipData, tooltipNode, element) { | function setupClickOutsideHandler(tooltipData, tooltipNode, element) { | ||
const handler = function(e) { | const handler = function(e) { | ||
if (!tooltipNode.contains(e.target) && e.target !== element) { | if (!tooltipNode.contains(e.target) && e.target !== element) { | ||
hideTooltip(tooltipData, tooltipNode); | hideTooltip(tooltipData, tooltipNode); | ||
$(document).off("click touchstart", handler); // 移除事件避免泄漏 | |||
$(document).off("click touchstart", handler); | if (e.type === "touchstart") e.preventDefault(); // 防点击穿透 | ||
if (e.type === "touchstart") | |||
} | } | ||
}; | }; | ||
$(document).on("click touchstart", handler); | $(document).on("click touchstart", handler); | ||
} | } | ||
/** | /** | ||
* | * 触发弹窗显示 | ||
*/ | */ | ||
function triggerTooltip(element, tooltipData) { | function triggerTooltip(element, tooltipData) { | ||
if (!tooltipData.originalTitle.trim()) return; | |||
if (!tooltipData.originalTitle.trim()) | |||
// | // 创建或更新弹窗 | ||
if (!tooltipData.tooltipNode) { | if (!tooltipData.tooltipNode) { | ||
tooltipData.tooltipNode = createTooltipNode(tooltipData.originalTitle); | tooltipData.tooltipNode = createTooltipNode(tooltipData.originalTitle); | ||
} else { | } else { | ||
const $contentContainer = $(tooltipData.tooltipNode).find("li:first-child > div"); | const $contentContainer = $(tooltipData.tooltipNode).find("li:first-child > div"); | ||
$contentContainer.replaceWith(createContentElement(tooltipData.originalTitle)); | $contentContainer.replaceWith(createContentElement(tooltipData.originalTitle)); | ||
} | } | ||
// | // 定位并显示 | ||
positionTooltip(element, tooltipData.tooltipNode); | positionTooltip(element, tooltipData.tooltipNode); | ||
showTooltip(tooltipData, tooltipData.tooltipNode); | showTooltip(tooltipData, tooltipData.tooltipNode); | ||
// | // 触摸设备绑定外部点击关闭 | ||
if (isTouchscreen) { | if (isTouchscreen) { | ||
setupClickOutsideHandler(tooltipData, tooltipData.tooltipNode, element); | setupClickOutsideHandler(tooltipData, tooltipData.tooltipNode, element); | ||
| 第293行: | 第239行: | ||
} | } | ||
/*===== | /*===== 事件委托:支持动态元素 =====*/ | ||
if (isTouchscreen) { | if (isTouchscreen) { | ||
// 触摸设备:点击触发 | |||
$bodyContent.on("click", ".inline-unihan", function(e) { | $bodyContent.on("click", ".inline-unihan", function(e) { | ||
e.preventDefault(); | e.preventDefault(); | ||
const element = this; | const element = this; | ||
const tooltipData = initElementData(element); | const tooltipData = initElementData(element); | ||
// 切换显示/隐藏状态 | |||
// | |||
if (tooltipData.tooltipNode && bodyContent.contains(tooltipData.tooltipNode)) { | if (tooltipData.tooltipNode && bodyContent.contains(tooltipData.tooltipNode)) { | ||
hideTooltip(tooltipData, tooltipData.tooltipNode); | hideTooltip(tooltipData, tooltipData.tooltipNode); | ||
| 第309行: | 第254行: | ||
}); | }); | ||
} else { | } else { | ||
// | // 非触摸设备:鼠标悬停触发 | ||
$bodyContent.on("mouseenter", ".inline-unihan", function() { | $bodyContent.on("mouseenter", ".inline-unihan", function() { | ||
const element = this; | const element = this; | ||
const tooltipData = initElementData(element); | const tooltipData = initElementData(element); | ||
tooltipData.showTimer = setTimeout(() => { | tooltipData.showTimer = setTimeout(() => { | ||
triggerTooltip(element, tooltipData); | triggerTooltip(element, tooltipData); | ||
| 第321行: | 第264行: | ||
const element = this; | const element = this; | ||
const tooltipData = initElementData(element); | const tooltipData = initElementData(element); | ||
if (tooltipData.tooltipNode) { | if (tooltipData.tooltipNode) { | ||
hideTooltip(tooltipData, tooltipData.tooltipNode); | hideTooltip(tooltipData, tooltipData.tooltipNode); | ||
| 第328行: | 第269行: | ||
}); | }); | ||
} | } | ||
/*===== 窗口滚动时重新定位弹窗 =====*/ | |||
$(window).on("scroll", function() { | |||
$(".inline-unihan").each(function() { | |||
const tooltipData = $(this).data("tooltipData"); | |||
if (tooltipData && tooltipData.tooltipNode && bodyContent.contains(tooltipData.tooltipNode)) { | |||
positionTooltip(this, tooltipData.tooltipNode); | |||
} | |||
}); | |||
}); | |||
})(jQuery, mediaWiki); | })(jQuery, mediaWiki); | ||
2025年11月11日 (二) 12:45的最新版本
/*
本小工具可以將[[Template:僻字]]的提示由原來的title提示改為元素式彈出提示,使觸控式裝置可以觀看有關提示
适配说明:兼容MediaWiki 1.43+,修复模块依赖错误,移除jquery.ui.animate依赖,优化动画逻辑
*/
// 显式传入jQuery和mediaWiki,避免$冲突
(function($, mw) {
"use strict";
/*===== 初始化检查:控制工具是否加载 =====*/
// 获取URL参数"UTdontload"(0=清除不加载状态,数字=设置不加载状态)
const dontLoadParam = mw.util.getParamValue("UTdontload");
if (dontLoadParam !== null) {
const paramVal = parseInt(dontLoadParam, 10);
if (!isNaN(paramVal)) {
paramVal === 0 ? mw.storage.remove("UTdontload") : mw.storage.set("UTdontload", "1");
}
}
// 若本地存储存在"不加载"状态,直接终止
if (mw.storage.get("UTdontload") === "1") {
return;
}
/*===== 主初始化:命名空间与容器 =====*/
const canonicalNamespace = mw.config.get("wgCanonicalNamespace");
const allowedNamespaces = ["", "Project", "Help"]; // 主/项目/帮助命名空间
if (!allowedNamespaces.includes(canonicalNamespace)) {
return;
}
// 获取内容容器(优先mw-content-text,兼容旧版本)
const $bodyContent = $("#mw-content-text").length ? $("#mw-content-text") : $("body");
const bodyContent = $bodyContent[0];
/*===== 设备类型检测 =====*/
const isTouchscreen = window.matchMedia("(hover: none), (pointer: coarse)").matches ||
mw.config.get("wgDisplayResolution") === "mobile" ||
"ontouchstart" in document.documentElement;
const hoverDelay = isTouchscreen ? 0 : 200; // 触摸设备无延迟
/*===== 工具函数:弹窗核心逻辑 =====*/
/**
* 初始化元素数据(存储到元素data中)
*/
function initElementData(element) {
let tooltipData = $(element).data("tooltipData");
if (!tooltipData) {
const originalTitle = element.title || "";
element.title = ""; // 清空原生title提示
$(element).addClass("UHTarget"); // 添加高亮类
tooltipData = {
originalTitle: originalTitle,
tooltipNode: null,
hideTimer: null,
showTimer: null
};
$(element).data("tooltipData", tooltipData);
}
return tooltipData;
}
/**
* 创建弹窗内容(支持多行文本分割)
*/
function createContentElement(originalTitle) {
const $contentContainer = $("<div>");
originalTitle.split("\n").forEach(line => {
const trimmedLine = line.trim();
if (trimmedLine) {
$contentContainer.append($("<p>").text(trimmedLine));
}
});
return $contentContainer[0];
}
/**
* 创建弹窗DOM结构(包含内容区和箭头)
*/
function createTooltipNode(originalTitle) {
const tooltipNode = document.createElement("ul");
tooltipNode.className = "unihantooltip";
// 内容区(包含提示文本和设置图标)
const $contentLi = $("<li>");
const contentElement = createContentElement(originalTitle);
const $settingsIcon = $("<div>")
.addClass("UHsettings")
.attr("title", "设置")
.on("click", (e) => {
e.stopPropagation();
// 示例:设置功能入口
alert("僻字提示设置(可扩展)");
});
// 组装内容区和箭头
$contentLi.append(contentElement).append($settingsIcon);
const $arrowLi = $("<li>");
$(tooltipNode).append($contentLi).append($arrowLi);
// 非触摸设备:绑定弹窗悬停事件
if (!isTouchscreen) {
const tooltipData = { hideTimer: null, showTimer: null };
$(tooltipNode).data("tooltipData", tooltipData)
.on("mouseenter", function() {
showTooltip($(this).data("tooltipData"), this);
})
.on("mouseleave", function() {
hideTooltip($(this).data("tooltipData"), this);
});
}
return tooltipNode;
}
/**
* 弹窗定位(避免超出视口,精准对齐)
*/
function positionTooltip(element, tooltipNode) {
const $element = $(element);
const $tooltip = $(tooltipNode);
// 离线计算尺寸(避免布局抖动)
$tooltip.css({ visibility: "hidden", display: "block" }).appendTo(bodyContent);
// 元素与视口信息
const elementOffset = $element.offset();
const elementWidth = $element.outerWidth();
const elementHeight = $element.outerHeight();
const tooltipWidth = $tooltip.outerWidth();
const contentHeight = $tooltip.find("li:first-child").outerHeight();
const arrowHeight = 12; // 与CSS箭头高度一致
const viewport = {
top: $(window).scrollTop(),
bottom: $(window).scrollTop() + $(window).height(),
left: 0,
right: $(window).width()
};
// 垂直定位(优先上方,不足则翻转)
let top = elementOffset.top - contentHeight - arrowHeight;
const needFlip = top < viewport.top || (elementOffset.top + contentHeight + arrowHeight) > viewport.bottom;
$tooltip.toggleClass("UHflipped", needFlip);
if (needFlip) {
top = elementOffset.top + elementHeight + arrowHeight - 2; // 适配CSS翻转间距
}
top = Math.max(viewport.top, Math.min(top, viewport.bottom - contentHeight));
// 水平定位(居中对齐,留边距)
let left = elementOffset.left + (elementWidth - tooltipWidth) / 2;
left = Math.max(viewport.left + 10, Math.min(left, viewport.right - tooltipWidth - 10));
// 箭头定位(指向元素中心)
const arrowOffset = elementOffset.left - left + elementWidth / 2 - 7;
$tooltip.find("li:last-child").css("margin-inline-start", `${arrowOffset}px`);
// 应用定位并显示
$tooltip.css({ top: `${top}px`, left: `${left}px`, visibility: "visible", display: "" });
}
/**
* 显示弹窗(依赖CSS过渡,无JS动画依赖)
*/
function showTooltip(tooltipData, tooltipNode) {
const $tooltip = $(tooltipNode);
// 确保弹窗已插入DOM
if (!tooltipNode.parentNode || tooltipNode.parentNode.nodeType === 11) {
bodyContent.appendChild(tooltipNode);
}
// 添加可见类(触发CSS渲染)
$tooltip.addClass("is-visible");
// 直接设置透明度(CSS过渡自动处理动画)
$tooltip.stop().css("opacity", 1);
// 清除隐藏定时器
if (tooltipData.hideTimer) {
clearTimeout(tooltipData.hideTimer);
tooltipData.hideTimer = null;
}
}
/**
* 隐藏弹窗(依赖CSS过渡,无JS动画依赖)
*/
function hideTooltip(tooltipData, tooltipNode) {
const $tooltip = $(tooltipNode);
// 清除显示定时器
if (tooltipData.showTimer) {
clearTimeout(tooltipData.showTimer);
tooltipData.showTimer = null;
}
// 延迟隐藏(防误触)
tooltipData.hideTimer = setTimeout(() => {
// 触发淡出(CSS过渡处理)
$tooltip.stop().css("opacity", 0);
// 过渡结束后清理
setTimeout(() => {
$tooltip.removeClass("is-visible");
if (bodyContent.contains(tooltipNode)) {
bodyContent.removeChild(tooltipNode);
}
}, 150); // 与CSS transition时长一致
}, isTouchscreen ? 16 : 100);
}
/**
* 触摸设备:点击外部关闭弹窗
*/
function setupClickOutsideHandler(tooltipData, tooltipNode, element) {
const handler = function(e) {
if (!tooltipNode.contains(e.target) && e.target !== element) {
hideTooltip(tooltipData, tooltipNode);
$(document).off("click touchstart", handler); // 移除事件避免泄漏
if (e.type === "touchstart") e.preventDefault(); // 防点击穿透
}
};
$(document).on("click touchstart", handler);
}
/**
* 触发弹窗显示
*/
function triggerTooltip(element, tooltipData) {
if (!tooltipData.originalTitle.trim()) return;
// 创建或更新弹窗
if (!tooltipData.tooltipNode) {
tooltipData.tooltipNode = createTooltipNode(tooltipData.originalTitle);
} else {
const $contentContainer = $(tooltipData.tooltipNode).find("li:first-child > div");
$contentContainer.replaceWith(createContentElement(tooltipData.originalTitle));
}
// 定位并显示
positionTooltip(element, tooltipData.tooltipNode);
showTooltip(tooltipData, tooltipData.tooltipNode);
// 触摸设备绑定外部点击关闭
if (isTouchscreen) {
setupClickOutsideHandler(tooltipData, tooltipData.tooltipNode, element);
}
}
/*===== 事件委托:支持动态元素 =====*/
if (isTouchscreen) {
// 触摸设备:点击触发
$bodyContent.on("click", ".inline-unihan", function(e) {
e.preventDefault();
const element = this;
const tooltipData = initElementData(element);
// 切换显示/隐藏状态
if (tooltipData.tooltipNode && bodyContent.contains(tooltipData.tooltipNode)) {
hideTooltip(tooltipData, tooltipData.tooltipNode);
} else {
triggerTooltip(element, tooltipData);
}
});
} else {
// 非触摸设备:鼠标悬停触发
$bodyContent.on("mouseenter", ".inline-unihan", function() {
const element = this;
const tooltipData = initElementData(element);
tooltipData.showTimer = setTimeout(() => {
triggerTooltip(element, tooltipData);
}, hoverDelay);
}).on("mouseleave", ".inline-unihan", function() {
const element = this;
const tooltipData = initElementData(element);
if (tooltipData.tooltipNode) {
hideTooltip(tooltipData, tooltipData.tooltipNode);
}
});
}
/*===== 窗口滚动时重新定位弹窗 =====*/
$(window).on("scroll", function() {
$(".inline-unihan").each(function() {
const tooltipData = $(this).data("tooltipData");
if (tooltipData && tooltipData.tooltipNode && bodyContent.contains(tooltipData.tooltipNode)) {
positionTooltip(this, tooltipData.tooltipNode);
}
});
});
})(jQuery, mediaWiki);