跳转到内容

MediaWiki:Gadget-UnihanTooltips.js:修订间差异

勤求古训,博采众方
无编辑摘要
无编辑摘要
第1行: 第1行:
(function() {
/*
     /*===== 初始化检查 =====*/
  本小工具可以將[[Template:僻字]]的提示由原來的title提示改為元素式彈出提示,使觸控式裝置可以觀看有關提示
     // Cookie 检查(保持原始逻辑)
  适配说明:兼容MediaWiki 1.43版本,弃用过时API,支持动态元素,优化触摸体验,修复布局抖动
     const dontLoad = mw.util.getParamValue("UTdontload");
*/
     if (dontLoad && !isNaN(dontLoad)) {
// 显式传入jQuery和mediaWiki,避免$冲突
        mw.cookie.set("UTdontload", "1", { path: "/", expires: parseInt(dontLoad) });
(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;
     }
     }
    if (mw.cookie.get("UTdontload") === "1") return;


     /*===== 主逻辑 =====*/
     /*===== 主初始化:获取配置与容器 =====*/
     (function() {
     // 获取当前页面命名空间(仅在主/项目/帮助命名空间加载)
        // 容器和目标命名空间检查
    const canonicalNamespace = mw.config.get("wgCanonicalNamespace");
        const bodyContent = mw.util.$content || document.body;
    const allowedNamespaces = ["", "Project", "Help"];
        const canonicalNamespace = mw.config.get('wgCanonicalNamespace');
    if (!allowedNamespaces.includes(canonicalNamespace)) {
        if (!['', 'Project', 'Help'].includes(canonicalNamespace)) return;
        return;
    }


        // 触摸设备检测(优化后的逻辑)
    // 获取内容容器(1.43推荐直接选择器获取,兼容性更好)
        const isTouchscreen = window.matchMedia('(hover: none), (pointer: coarse)').matches ||
    const $bodyContent = $("#mw-content-text").length ? $("#mw-content-text") : $("body");
                            mw.config.get('wgDisplayResolution') === 'mobile' ||
    const bodyContent = $bodyContent[0]; // 转为原生DOM对象
                            'ontouchstart' in document.documentElement;
        const hoverDelay = isTouchscreen ? 0 : 200;


        /*===== 工具提示核心逻辑 =====*/
    // 设备类型检测(适配1.43触摸设备判断逻辑)
        $(".inline-unihan").each(function() {
    const isTouchscreen = window.matchMedia("(hover: none), (pointer: coarse)").matches ||
            let tooltipNode = null;
                        mw.config.get("wgDisplayResolution") === "mobile" ||
            let hideTimer = null;
                        "ontouchstart" in document.documentElement;
            let showTimer = null;
    const hoverDelay = isTouchscreen ? 0 : 200; // 悬停延迟:触摸设备无延迟
            const element = this;


             // 保存原始title并添加高亮类
    /*===== 工具函数:弹窗创建/更新/定位/控制 =====*/
             const originalTitle = this.title;
    /**
             this.title = "";
    * 初始化单个僻字元素的数据(存储到元素data中,避免全局污染)
             $(this).addClass("UHTarget");
    * @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;
    }


            /*-- 工具提示控制函数 --*/
    /**
            function hideTooltip() {
    * 创建/更新弹窗内容(支持多段落,适配原始title换行)
                if (!tooltipNode || tooltipNode.parentNode !== bodyContent) return;
    * @param {string} originalTitle - 原始title文本
               
    * @returns {HTMLElement} 内容容器DOM
                clearTimeout(hideTimer);
    */
                hideTimer = setTimeout(() => {
    function createContentElement(originalTitle) {
                    $(tooltipNode).animate({ opacity: 0 }, 100, () => {
        const $contentContainer = $("<div>");
                        tooltipNode.parentNode.removeChild(tooltipNode);
        // 按换行分割文本,生成段落(跳过空行)
                    });
        originalTitle.split("\n").forEach(line => {
                }, isTouchscreen ? 16 : 100);
            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(); // 阻止事件冒泡,避免关闭弹窗
                // 可扩展设置逻辑:如弹窗样式、显示开关等
            });
        // 组装内容区
        $contentLi.append(contentElement).append($settingsIcon);
        // 箭头li(用于指向目标元素)
        const $arrowLi = $("<li>");
        // 组装弹窗
        $(tooltipNode).append($contentLi).append($arrowLi);
        // 非触摸设备:绑定弹窗悬停事件
        if (!isTouchscreen) {
            $(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 - 13; // 13px=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));


             function showTooltip() {
        /*===== 箭头定位:精准指向目标元素中心 =====*/
                if (!tooltipNode.parentNode || tooltipNode.parentNode.nodeType === 11) {
        const arrowOffset = elementOffset.left - left + elementWidth / 2 - 7; // 7px=箭头偏移补偿
                     bodyContent.appendChild(tooltipNode);
        $tooltip.find("li:last-child").css("margin-left", `${arrowOffset}px`);
 
        /*===== 应用定位并恢复显示状态 =====*/
        $tooltip.css({
            top: `${top}px`,
            left: `${left}px`,
            visibility: "visible",
             display: ""
        });
    }
 
    /**
    * 显示弹窗(清除隐藏定时器,执行淡入动画)
    * @param {Object} tooltipData - 弹窗数据对象
    * @param {HTMLElement} tooltipNode - 弹窗DOM节点
    */
    function showTooltip(tooltipData, tooltipNode) {
        // 确保弹窗已插入DOM
        if (!tooltipNode.parentNode || tooltipNode.parentNode.nodeType === 11) {
            bodyContent.appendChild(tooltipNode);
        }
        // 停止动画排队,淡入显示
        $(tooltipNode).stop().animate({ opacity: 1 }, 100);
        // 清除隐藏定时器
        if (tooltipData.hideTimer) {
            clearTimeout(tooltipData.hideTimer);
            tooltipData.hideTimer = null;
        }
    }
 
    /**
    * 隐藏弹窗(延迟执行,执行淡出动画后移除)
    * @param {Object} tooltipData - 弹窗数据对象
    * @param {HTMLElement} tooltipNode - 弹窗DOM节点
    */
    function hideTooltip(tooltipData, tooltipNode) {
        // 清除显示定时器
        if (tooltipData.showTimer) {
            clearTimeout(tooltipData.showTimer);
            tooltipData.showTimer = null;
        }
        // 延迟隐藏(避免误触)
        tooltipData.hideTimer = setTimeout(() => {
            // 淡出动画后移除弹窗
            $(tooltipNode).animate({ opacity: 0 }, 100, () => {
                if (bodyContent.contains(tooltipNode)) {
                     bodyContent.removeChild(tooltipNode);
                }
            });
        }, 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();
                 }
                 }
                $(tooltipNode).stop().animate({ opacity: 1 }, 100);
                clearTimeout(hideTimer);
             }
             }
        };
        // 绑定文档点击/触摸事件
        $(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));
        }


            /*-- 工具提示创建/更新 --*/
        // 定位并显示弹窗
            function createOrUpdateTooltip() {
        positionTooltip(element, tooltipData.tooltipNode);
                if (!tooltipNode) {
        showTooltip(tooltipData, tooltipData.tooltipNode);
                    // 首次创建工具提示
                    tooltipNode = document.createElement("ul");
                    tooltipNode.className = "unihantooltip";
                   
                    // 内容容器 (第一个li)
                    const contentLi = document.createElement("li");
                   
                    // 设置齿轮图标
                    const settingsIcon = document.createElement("div");
                    settingsIcon.className = "UHsettings";
                    settingsIcon.title = "设置";
                    $(settingsIcon).on("click", (e) => {
                        e.stopPropagation();
                        // 这里添加设置图标的点击处理逻辑
                    });
                   
                    // 箭头元素 (第二个li)
                    const arrowLi = document.createElement("li");
                   
                    // 组装DOM结构
                    contentLi.appendChild(createContentElement());
                    contentLi.appendChild(settingsIcon);
                    tooltipNode.appendChild(contentLi);
                    tooltipNode.appendChild(arrowLi);


                    // 非触摸设备的悬停处理
        // 触摸设备:绑定外部点击关闭事件
                    if (!isTouchscreen) {
        if (isTouchscreen) {
                        $(tooltipNode).on("mouseenter", showTooltip)
            setupClickOutsideHandler(tooltipData, tooltipData.tooltipNode, element);
                                      .on("mouseleave", hideTooltip);
        }
                    }
    }
                } else {
                    // 更新已有内容
                    const contentContainer = tooltipNode.firstChild.firstChild;
                    tooltipNode.firstChild.replaceChild(
                        createContentElement(),
                        contentContainer
                    );
                }


                // 定位并显示
    /*===== 事件委托:支持动态加载的僻字元素(1.43关键优化) =====*/
                positionTooltip();
    // 触摸设备:绑定点击事件
                showTooltip();
    if (isTouchscreen) {
             }
        $bodyContent.on("click", ".inline-unihan", function(e) {
            e.preventDefault(); // 阻止默认行为(如跳转)
            const element = this;
             const tooltipData = initElementData(element);


             /*-- 内容生成函数 --*/
             // 弹窗已显示则隐藏,未显示则显示
             function createContentElement() {
             if (tooltipData.tooltipNode && bodyContent.contains(tooltipData.tooltipNode)) {
                const container = document.createElement("div");
                hideTooltip(tooltipData, tooltipData.tooltipNode);
                originalTitle.split("\n").forEach(line => {
            } else {
                    if (line.trim()) {
                 triggerTooltip(element, tooltipData);
                        const p = document.createElement("p");
                        p.textContent = line;
                        container.appendChild(p);
                    }
                 });
                return container;
             }
             }
        });
    } else {
        // 非触摸设备:绑定鼠标移进/移出事件
        $bodyContent.on("mouseenter", ".inline-unihan", function() {
            const element = this;
            const tooltipData = initElementData(element);


             /*-- 精确定位逻辑 (关键调整) --*/
             // 延迟显示(防误触)
             function positionTooltip() {
             tooltipData.showTimer = setTimeout(() => {
                const $element = $(element);
                 triggerTooltip(element, tooltipData);
                const elementPos = $element.offset();
            }, hoverDelay);
                const elementHeight = $element.outerHeight();
        }).on("mouseleave", ".inline-unihan", function() {
                const elementWidth = $element.outerWidth();
            const element = this;
               
            const tooltipData = initElementData(element);
                // 临时显示以获取尺寸
                tooltipNode.style.display = 'block';
                const tooltipWidth = tooltipNode.offsetWidth;
                const contentHeight = tooltipNode.firstChild.offsetHeight;
                 const arrowHeight = 12; // 对应CSS中的箭头高度
               
                // 视口边界检测
                const viewport = {
                    top: $(window).scrollTop(),
                    bottom: $(window).scrollTop() + $(window).height(),
                    left: 0,
                    right: $(window).width()
                };
               
                /*===== 垂直定位 =====*/
                // 默认上方显示 (减内容高度和箭头高度)
                let top = elementPos.top - contentHeight - arrowHeight;
               
                // 检查是否需要翻转 (空间不足时)
                const needFlip = (top < viewport.top) ||
                                (elementPos.top + contentHeight > viewport.bottom);
               
                $(tooltipNode).toggleClass("UHflipped", needFlip);
               
                // 应用翻转偏移量 (13px对应CSS的padding-top)
                top = needFlip
                    ? elementPos.top + elementHeight + arrowHeight - 13
                    : top;
               
                // 确保不超出视口
                top = Math.max(viewport.top, Math.min(top, viewport.bottom - contentHeight));
               
                /*===== 水平定位 =====*/
                // 中心对齐 (7px对应CSS箭头的margin-left)
                let left = elementPos.left + (elementWidth - tooltipWidth) / 2;
               
                // 边界保护 (左右各留10px安全边距)
                left = Math.max(viewport.left + 10,
                              Math.min(left, viewport.right - tooltipWidth - 10));
               
                /*===== 箭头定位 =====*/
                const arrow = tooltipNode.lastChild;
                const arrowCenterOffset = elementPos.left - left + elementWidth / 2 - 7;
                $(arrow).css("margin-left", arrowCenterOffset + "px");
               
                // 应用最终位置
                $(tooltipNode).css({
                    top: top + "px",
                    left: left + "px",
                    display: '' // 恢复原始display
                });
            }


             /*-- 触摸设备的外部点击处理 --*/
             // 鼠标移出,隐藏弹窗
             function setupClickOutsideHandler() {
             if (tooltipData.tooltipNode) {
                const handler = function(e) {
                hideTooltip(tooltipData, tooltipData.tooltipNode);
                    if (!tooltipNode.contains(e.target) && e.target !== element) {
                        hideTooltip();
                        $(document).off("click touchstart", handler);
                    }
                };
                $(document).on("click touchstart", handler);
             }
             }
        });
    }


            /*===== 事件绑定 =====*/
})(jQuery, mediaWiki);
            $(element).on(isTouchscreen ? 'click' : 'mouseenter mouseleave', function(e) {
                // 触摸设备处理
                if (isTouchscreen && e.type === 'click') {
                    e.preventDefault();
                   
                    if (!tooltipNode || tooltipNode.parentNode !== bodyContent) {
                        createOrUpdateTooltip();
                        setupClickOutsideHandler();
                    }
                    return;
                }
               
                // 鼠标设备处理
                if (e.type === 'mouseleave') {
                    clearTimeout(showTimer);
                    hideTooltip();
                    return;
                }
               
                // mouseenter处理
                clearTimeout(showTimer);
                showTimer = setTimeout(createOrUpdateTooltip, hoverDelay);
            });
        });
    })();
})();

2025年11月11日 (二) 11:53的版本

/*
  本小工具可以將[[Template:僻字]]的提示由原來的title提示改為元素式彈出提示,使觸控式裝置可以觀看有關提示
  适配说明:兼容MediaWiki 1.43版本,弃用过时API,支持动态元素,优化触摸体验,修复布局抖动
*/
// 显式传入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(); // 阻止事件冒泡,避免关闭弹窗
                // 可扩展设置逻辑:如弹窗样式、显示开关等
            });

        // 组装内容区
        $contentLi.append(contentElement).append($settingsIcon);
        // 箭头li(用于指向目标元素)
        const $arrowLi = $("<li>");

        // 组装弹窗
        $(tooltipNode).append($contentLi).append($arrowLi);

        // 非触摸设备:绑定弹窗悬停事件
        if (!isTouchscreen) {
            $(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 - 13; // 13px=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));

        /*===== 箭头定位:精准指向目标元素中心 =====*/
        const arrowOffset = elementOffset.left - left + elementWidth / 2 - 7; // 7px=箭头偏移补偿
        $tooltip.find("li:last-child").css("margin-left", `${arrowOffset}px`);

        /*===== 应用定位并恢复显示状态 =====*/
        $tooltip.css({
            top: `${top}px`,
            left: `${left}px`,
            visibility: "visible",
            display: ""
        });
    }

    /**
     * 显示弹窗(清除隐藏定时器,执行淡入动画)
     * @param {Object} tooltipData - 弹窗数据对象
     * @param {HTMLElement} tooltipNode - 弹窗DOM节点
     */
    function showTooltip(tooltipData, tooltipNode) {
        // 确保弹窗已插入DOM
        if (!tooltipNode.parentNode || tooltipNode.parentNode.nodeType === 11) {
            bodyContent.appendChild(tooltipNode);
        }
        // 停止动画排队,淡入显示
        $(tooltipNode).stop().animate({ opacity: 1 }, 100);
        // 清除隐藏定时器
        if (tooltipData.hideTimer) {
            clearTimeout(tooltipData.hideTimer);
            tooltipData.hideTimer = null;
        }
    }

    /**
     * 隐藏弹窗(延迟执行,执行淡出动画后移除)
     * @param {Object} tooltipData - 弹窗数据对象
     * @param {HTMLElement} tooltipNode - 弹窗DOM节点
     */
    function hideTooltip(tooltipData, tooltipNode) {
        // 清除显示定时器
        if (tooltipData.showTimer) {
            clearTimeout(tooltipData.showTimer);
            tooltipData.showTimer = null;
        }
        // 延迟隐藏(避免误触)
        tooltipData.hideTimer = setTimeout(() => {
            // 淡出动画后移除弹窗
            $(tooltipNode).animate({ opacity: 0 }, 100, () => {
                if (bodyContent.contains(tooltipNode)) {
                    bodyContent.removeChild(tooltipNode);
                }
            });
        }, 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);
            }
        });
    }

})(jQuery, mediaWiki);