MediaWiki:Gadget-UnihanTooltips.js:修订间差异
外观
小无编辑摘要 |
小 // Edit via Wikiplus |
||
| 第2行: | 第2行: | ||
本小工具可以將[[Template:僻字]]的提示由原來的title提示改為元素式彈出提示,使觸控式裝置可以觀看有關提示 | 本小工具可以將[[Template:僻字]]的提示由原來的title提示改為元素式彈出提示,使觸控式裝置可以觀看有關提示 | ||
适配说明:兼容MediaWiki 1.43版本,弃用过时API,支持动态元素,优化触摸体验,修复布局抖动 | 适配说明:兼容MediaWiki 1.43版本,弃用过时API,支持动态元素,优化触摸体验,修复布局抖动 | ||
新增优化:配合CSS的is-visible类控制渲染性能,优化箭头定位逻辑,适配深色模式 | |||
*/ | */ | ||
// 显式传入jQuery和mediaWiki,避免$冲突 | // 显式传入jQuery和mediaWiki,避免$冲突 | ||
| 第110行: | 第111行: | ||
e.stopPropagation(); // 阻止事件冒泡,避免关闭弹窗 | e.stopPropagation(); // 阻止事件冒泡,避免关闭弹窗 | ||
// 可扩展设置逻辑:如弹窗样式、显示开关等 | // 可扩展设置逻辑:如弹窗样式、显示开关等 | ||
alert("设置功能可在此处扩展"); | |||
}); | }); | ||
| 第120行: | 第122行: | ||
$(tooltipNode).append($contentLi).append($arrowLi); | $(tooltipNode).append($contentLi).append($arrowLi); | ||
// | // 非触摸设备:绑定弹窗悬停事件(存储tooltipData到弹窗节点) | ||
if (!isTouchscreen) { | if (!isTouchscreen) { | ||
// 存储tooltipData到弹窗节点,供悬停事件使用 | |||
const tooltipData = { | |||
hideTimer: null, | |||
showTimer: null | |||
}; | |||
$(tooltipNode).data("tooltipData", tooltipData); | |||
$(tooltipNode) | $(tooltipNode) | ||
.on("mouseenter", function() { | .on("mouseenter", function() { | ||
| 第177行: | 第186行: | ||
// 调整垂直位置(适配翻转状态和视口边界) | // 调整垂直位置(适配翻转状态和视口边界) | ||
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)); | ||
| 第186行: | 第195行: | ||
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)); | ||
/*===== | /*===== 箭头定位:精准指向目标元素中心(适配CSS逻辑属性) =====*/ | ||
const arrowOffset = elementOffset.left - left + elementWidth / 2 - 7; // 7px= | const arrowOffset = elementOffset.left - left + elementWidth / 2 - 7; // 7px=箭头水平偏移补偿 | ||
$tooltip.find("li:last-child").css("margin- | $tooltip.find("li:last-child").css("margin-inline-start", `${arrowOffset}px`); // 使用逻辑属性适配多语言 | ||
/*===== 应用定位并恢复显示状态 =====*/ | /*===== 应用定位并恢复显示状态 =====*/ | ||
| 第200行: | 第209行: | ||
/** | /** | ||
* | * 显示弹窗(添加is-visible类,清除隐藏定时器,执行淡入动画) | ||
* @param {Object} tooltipData - 弹窗数据对象 | * @param {Object} tooltipData - 弹窗数据对象 | ||
* @param {HTMLElement} tooltipNode - 弹窗DOM节点 | * @param {HTMLElement} tooltipNode - 弹窗DOM节点 | ||
*/ | */ | ||
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); | ||
} | } | ||
// | // 添加is-visible类(启用CSS渲染优化) | ||
$( | $tooltip.addClass("is-visible"); | ||
// 停止动画排队,淡入显示(CSS已设置transition,可简化动画) | |||
$tooltip.stop().css("opacity", 1); | |||
// 清除隐藏定时器 | // 清除隐藏定时器 | ||
if (tooltipData.hideTimer) { | if (tooltipData.hideTimer) { | ||
| 第219行: | 第231行: | ||
/** | /** | ||
* | * 隐藏弹窗(移除is-visible类,延迟执行,执行淡出动画后移除) | ||
* @param {Object} tooltipData - 弹窗数据对象 | * @param {Object} tooltipData - 弹窗数据对象 | ||
* @param {HTMLElement} tooltipNode - 弹窗DOM节点 | * @param {HTMLElement} tooltipNode - 弹窗DOM节点 | ||
*/ | */ | ||
function hideTooltip(tooltipData, tooltipNode) { | function hideTooltip(tooltipData, tooltipNode) { | ||
const $tooltip = $(tooltipNode); | |||
// 清除显示定时器 | // 清除显示定时器 | ||
if (tooltipData.showTimer) { | if (tooltipData.showTimer) { | ||
| 第231行: | 第244行: | ||
// 延迟隐藏(避免误触) | // 延迟隐藏(避免误触) | ||
tooltipData.hideTimer = setTimeout(() => { | tooltipData.hideTimer = setTimeout(() => { | ||
// | // 移除is-visible类(禁用CSS渲染) | ||
$( | $tooltip.removeClass("is-visible"); | ||
// 淡出动画后移除弹窗(利用CSS transition) | |||
$tooltip.stop().css("opacity", 0); | |||
setTimeout(() => { | |||
if (bodyContent.contains(tooltipNode)) { | if (bodyContent.contains(tooltipNode)) { | ||
bodyContent.removeChild(tooltipNode); | bodyContent.removeChild(tooltipNode); | ||
} | } | ||
}); | }, 150); // 与CSS transition时长保持一致 | ||
}, isTouchscreen ? 16 : 100); | }, isTouchscreen ? 16 : 100); | ||
} | } | ||
| 第253行: | 第269行: | ||
// 移除事件绑定,避免内存泄漏 | // 移除事件绑定,避免内存泄漏 | ||
$(document).off("click touchstart", handler); | $(document).off("click touchstart", handler); | ||
// | // 阻止touchstart默认行为,防止点击穿透(适配移动设备) | ||
if (e.type === "touchstart") { | if (e.type === "touchstart") { | ||
e.preventDefault(); | e.preventDefault(); | ||
| 第328行: | 第344行: | ||
}); | }); | ||
} | } | ||
/*===== 窗口滚动时重新定位弹窗(优化体验) =====*/ | |||
$(window).on("scroll", function() { | |||
// 遍历所有带tooltipData的元素,重新定位显示中的弹窗 | |||
$(".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:36的版本
/*
本小工具可以將[[Template:僻字]]的提示由原來的title提示改為元素式彈出提示,使觸控式裝置可以觀看有關提示
适配说明:兼容MediaWiki 1.43版本,弃用过时API,支持动态元素,优化触摸体验,修复布局抖动
新增优化:配合CSS的is-visible类控制渲染性能,优化箭头定位逻辑,适配深色模式
*/
// 显式传入jQuery和mediaWiki,避免$冲突
(function($, mw) {
"use strict"; // 启用严格模式,减少语法错误
/*===== 初始化检查:控制工具是否加载(替换Cookie为本地存储) =====*/
// 获取URL参数"UTdontload"(0=清除不加载状态,数字=设置不加载状态)
const dontLoadParam = mw.util.getParamValue("UTdontload");
// 处理参数:设置/清除"不加载"状态(使用mw.storage,1.43推荐)
if (dontLoadParam !== null) {
const paramVal = parseInt(dontLoadParam, 10);
if (!isNaN(paramVal)) {
// 参数为0:清除本地存储的不加载状态
if (paramVal === 0) {
mw.storage.remove("UTdontload");
} else {
// 参数为非0数字:设置不加载状态(永久有效,除非手动清除)
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;
}
// 获取内容容器(1.43推荐直接选择器获取,兼容性更好)
const $bodyContent = $("#mw-content-text").length ? $("#mw-content-text") : $("body");
const bodyContent = $bodyContent[0]; // 转为原生DOM对象
// 设备类型检测(适配1.43触摸设备判断逻辑)
const isTouchscreen = window.matchMedia("(hover: none), (pointer: coarse)").matches ||
mw.config.get("wgDisplayResolution") === "mobile" ||
"ontouchstart" in document.documentElement;
const hoverDelay = isTouchscreen ? 0 : 200; // 悬停延迟:触摸设备无延迟
/*===== 工具函数:弹窗创建/更新/定位/控制 =====*/
/**
* 初始化单个僻字元素的数据(存储到元素data中,避免全局污染)
* @param {HTMLElement} element - .inline-unihan元素
* @returns {Object} 弹窗相关数据
*/
function initElementData(element) {
let tooltipData = $(element).data("tooltipData");
if (!tooltipData) {
// 保存原始title,清空默认提示
const originalTitle = element.title || "";
element.title = "";
// 标记目标元素(用于CSS样式)
$(element).addClass("UHTarget");
// 初始化弹窗数据(存储到元素data中)
tooltipData = {
originalTitle: originalTitle,
tooltipNode: null, // 弹窗DOM节点
hideTimer: null, // 隐藏定时器
showTimer: null // 显示定时器
};
$(element).data("tooltipData", tooltipData);
}
return tooltipData;
}
/**
* 创建/更新弹窗内容(支持多段落,适配原始title换行)
* @param {string} originalTitle - 原始title文本
* @returns {HTMLElement} 内容容器DOM
*/
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结构(包含内容区、设置图标、箭头)
* @param {string} originalTitle - 原始title文本
* @returns {HTMLElement} 弹窗DOM节点
*/
function createTooltipNode(originalTitle) {
// 弹窗容器(ul标签,用于样式控制)
const tooltipNode = document.createElement("ul");
tooltipNode.className = "unihantooltip";
// 内容区li(包含提示内容和设置图标)
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);
// 箭头li(用于指向目标元素)
const $arrowLi = $("<li>");
// 组装弹窗
$(tooltipNode).append($contentLi).append($arrowLi);
// 非触摸设备:绑定弹窗悬停事件(存储tooltipData到弹窗节点)
if (!isTouchscreen) {
// 存储tooltipData到弹窗节点,供悬停事件使用
const tooltipData = {
hideTimer: null,
showTimer: null
};
$(tooltipNode).data("tooltipData", tooltipData);
$(tooltipNode)
.on("mouseenter", function() {
const tooltipData = $(this).data("tooltipData");
showTooltip(tooltipData, this);
})
.on("mouseleave", function() {
const tooltipData = $(this).data("tooltipData");
hideTooltip(tooltipData, this);
});
}
return tooltipNode;
}
/**
* 弹窗定位(精准对齐目标元素,避免超出视口,无布局抖动)
* @param {HTMLElement} element - 目标僻字元素
* @param {HTMLElement} tooltipNode - 弹窗DOM节点
*/
function positionTooltip(element, tooltipNode) {
const $element = $(element);
const $tooltip = $(tooltipNode);
// 离线获取尺寸:避免布局抖动(visibility:hidden不触发重排)
$tooltip.css({
visibility: "hidden",
display: "block",
position: "absolute"
}).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;
// 左右留10px安全边距
left = Math.max(viewport.left + 10, Math.min(left, viewport.right - tooltipWidth - 10));
/*===== 箭头定位:精准指向目标元素中心(适配CSS逻辑属性) =====*/
const arrowOffset = elementOffset.left - left + elementWidth / 2 - 7; // 7px=箭头水平偏移补偿
$tooltip.find("li:last-child").css("margin-inline-start", `${arrowOffset}px`); // 使用逻辑属性适配多语言
/*===== 应用定位并恢复显示状态 =====*/
$tooltip.css({
top: `${top}px`,
left: `${left}px`,
visibility: "visible",
display: ""
});
}
/**
* 显示弹窗(添加is-visible类,清除隐藏定时器,执行淡入动画)
* @param {Object} tooltipData - 弹窗数据对象
* @param {HTMLElement} tooltipNode - 弹窗DOM节点
*/
function showTooltip(tooltipData, tooltipNode) {
const $tooltip = $(tooltipNode);
// 确保弹窗已插入DOM
if (!tooltipNode.parentNode || tooltipNode.parentNode.nodeType === 11) {
bodyContent.appendChild(tooltipNode);
}
// 添加is-visible类(启用CSS渲染优化)
$tooltip.addClass("is-visible");
// 停止动画排队,淡入显示(CSS已设置transition,可简化动画)
$tooltip.stop().css("opacity", 1);
// 清除隐藏定时器
if (tooltipData.hideTimer) {
clearTimeout(tooltipData.hideTimer);
tooltipData.hideTimer = null;
}
}
/**
* 隐藏弹窗(移除is-visible类,延迟执行,执行淡出动画后移除)
* @param {Object} tooltipData - 弹窗数据对象
* @param {HTMLElement} tooltipNode - 弹窗DOM节点
*/
function hideTooltip(tooltipData, tooltipNode) {
const $tooltip = $(tooltipNode);
// 清除显示定时器
if (tooltipData.showTimer) {
clearTimeout(tooltipData.showTimer);
tooltipData.showTimer = null;
}
// 延迟隐藏(避免误触)
tooltipData.hideTimer = setTimeout(() => {
// 移除is-visible类(禁用CSS渲染)
$tooltip.removeClass("is-visible");
// 淡出动画后移除弹窗(利用CSS transition)
$tooltip.stop().css("opacity", 0);
setTimeout(() => {
if (bodyContent.contains(tooltipNode)) {
bodyContent.removeChild(tooltipNode);
}
}, 150); // 与CSS transition时长保持一致
}, isTouchscreen ? 16 : 100);
}
/**
* 触摸设备:点击弹窗外区域关闭弹窗(防点击穿透)
* @param {Object} tooltipData - 弹窗数据对象
* @param {HTMLElement} tooltipNode - 弹窗DOM节点
* @param {HTMLElement} element - 目标僻字元素
*/
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);
// 阻止touchstart默认行为,防止点击穿透(适配移动设备)
if (e.type === "touchstart") {
e.preventDefault();
}
}
};
// 绑定文档点击/触摸事件
$(document).on("click touchstart", handler);
}
/**
* 触发弹窗显示(根据设备类型处理)
* @param {HTMLElement} element - 目标僻字元素
* @param {Object} tooltipData - 弹窗数据对象
*/
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);
}
}
/*===== 事件委托:支持动态加载的僻字元素(1.43关键优化) =====*/
// 触摸设备:绑定点击事件
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() {
// 遍历所有带tooltipData的元素,重新定位显示中的弹窗
$(".inline-unihan").each(function() {
const tooltipData = $(this).data("tooltipData");
if (tooltipData && tooltipData.tooltipNode && bodyContent.contains(tooltipData.tooltipNode)) {
positionTooltip(this, tooltipData.tooltipNode);
}
});
});
})(jQuery, mediaWiki);