Greasy Fork

Greasy Fork is available in English.

Emoji 含义选中提示

在网页中选中 Emoji 时,显示其含义、名称和分类。使用 Google Noto Emoji SVG 源,通过标准 IMG 标签加载。

当前为 2025-12-01 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Emoji Tooltip
// @name:zh-CN  Emoji 含义选中提示
// @namespace   http://tampermonkey.net/
// @version     1.16
// @description:zh-CN 在网页中选中 Emoji 时,显示其含义、名称和分类。使用 Google Noto Emoji SVG 源,通过标准 IMG 标签加载。
// @description When an emoji is selected, display its meaning, name, and category. Uses Google Noto Emoji SVG source via standard IMG tags.
// @author      Kaesinol
// @match       *://*/*
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_openInTab
// @connect     cdn.jsdelivr.net
// @connect     raw.githubusercontent.com
// @run-at      document-start
// @license     MIT
// ==/UserScript==

(function () {
    'use strict';

    // ====================
    // ⚙️ Configuration
    // ====================
    const CONFIG = {
        BASE_URL: 'https://cdn.jsdelivr.net/npm/[email protected]',
        // 修改为 Google Noto Emoji SVG 仓库
        SVG_BASE_URL: 'https://raw.githubusercontent.com/googlefonts/noto-emoji/refs/heads/main/svg',
        CACHE_KEY: 'emoji_tooltip_data_v5',
        CACHE_VERSION: '1.15', // 版本更新
        AUTO_HIDE_DELAY: 10000,
        MOUSE_MOVE_THRESHOLD: 300,
        GROUP_MAP: {
            0: 'Smileys & Emotion', 1: 'People & Body', 2: 'Component',
            3: 'Animals & Nature', 4: 'Food & Drink', 5: 'Travel & Places',
            6: 'Activities', 7: 'Objects', 8: 'Symbols', 9: 'Flags'
        }
    };

    // ====================
    // 📦 State Variables
    // ====================
    let emojiMap = new Map();
    let tooltipElement;
    let hideTimer, autoHideTimer, debounceTimer;
    let isTooltipVisible = false;
    let lastMousePosition = { x: 0, y: 0 };


    // ====================
    // 🎨 Tooltip UI Logic
    // ====================

    /** 初始化 Tooltip 元素并注入到 DOM */
    function initTooltipElement() {
        tooltipElement = document.createElement('div');
        tooltipElement.id = 'emoji-tooltip-container';
        tooltipElement.style.cssText = `
            position: fixed; background: #2b2b2b; color: #fff; padding: 10px 14px;
            border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.4);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px; line-height: 1.4; z-index: 2147483647; max-width: 320px;
            opacity: 0; transition: opacity 0.2s, transform 0.2s; display: none;
            transform: translateX(10px) translateY(5px);
            border: 1px solid #444;
            user-select: text;
            -webkit-user-select: text;
        `;

        if (document.body) {
            document.body.appendChild(tooltipElement);
        } else {
            new MutationObserver((mutations, observer) => {
                if (document.body) {
                    document.body.appendChild(tooltipElement);
                    observer.disconnect();
                }
            }).observe(document.documentElement, { childList: true, subtree: true });
        }
    }

    /** 显示 Tooltip */
    function showTooltip(content, x, y) {
        clearTimeout(hideTimer);
        clearTimeout(autoHideTimer);

        tooltipElement.innerHTML = content;
        tooltipElement.style.display = 'block';
        tooltipElement.style.opacity = '0';
        tooltipElement.style.transform = 'translateX(10px) translateY(5px)';
        void tooltipElement.offsetWidth;

        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;
        const tooltipWidth = tooltipElement.clientWidth || 200;
        const tooltipHeight = tooltipElement.clientHeight || 80;

        let left = x + 15;
        let top = y + 15;

        // 智能定位
        if (left + tooltipWidth > viewportWidth - 10) left = x - tooltipWidth - 15;
        if (top + tooltipHeight > viewportHeight - 10) top = y - tooltipHeight - 15;
        if (left < 10) left = 10;

        tooltipElement.style.left = `${left}px`;
        tooltipElement.style.top = `${top}px`;

        requestAnimationFrame(() => {
            tooltipElement.style.opacity = '1';
            tooltipElement.style.transform = 'translateX(0) translateY(0)';
        });

        isTooltipVisible = true;
        autoHideTimer = setTimeout(hideTooltip, CONFIG.AUTO_HIDE_DELAY);
    }

    /** 隐藏 Tooltip */
    function hideTooltip() {
        if (!isTooltipVisible) return;
        clearTimeout(hideTimer);
        clearTimeout(autoHideTimer);
        tooltipElement.style.opacity = '0';
        tooltipElement.style.transform = 'translateX(10px) translateY(5px)';
        hideTimer = setTimeout(() => {
            tooltipElement.style.display = 'none';
            tooltipElement.onclick = null;
            tooltipElement.title = '';
            tooltipElement.style.cursor = 'default';
            isTooltipVisible = false;
        }, 200);
    }

    /** 显示加载状态 */
    function showLoadingTooltip(x, y) {
        const content = `
            <div style="display: flex; align-items: center; gap: 12px; pointer-events: none;">
                <div style="width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-size: 16px;">
                    ⏳
                </div>
                <div>
                    <div style="font-weight: 600; font-size: 14px; color: #f9e67d;">
                        ✨ Loading Emoji Data...
                    </div>
                </div>
            </div>
        `;
        tooltipElement.onclick = null;
        tooltipElement.style.cursor = 'default';
        showTooltip(content, x, y);
    }


    // ====================
    // 🧠 Event Handling
    // ====================

    function handleSelection() {
        let selection;
        let rangeRect;

        try {
            selection = window.getSelection();
            const selectionText = selection.toString().trim();

            if (!selectionText || selectionText.length < 1 || selectionText.length > 15) {
                if (isTooltipVisible) hideTimer = setTimeout(hideTooltip, 2000);
                return;
            }

            if (selection.rangeCount > 0) {
                rangeRect = selection.getRangeAt(0).getBoundingClientRect();
            }

            let emojiData = emojiMap.get(selectionText);
            let finalChar = selectionText;

            if (!emojiData) {
                const selectionWithVariant = selectionText + '\uFE0F';
                emojiData = emojiMap.get(selectionWithVariant);
                if (emojiData) {
                    finalChar = selectionWithVariant;
                }
            }

            if (rangeRect && (emojiData || emojiMap.size === 0)) {
                const x = rangeRect.left + (rangeRect.width / 2);
                const y = rangeRect.bottom;

                if (emojiData) {
                    showEmojiTooltip(emojiData, finalChar, x, y);
                } else if (emojiMap.size === 0) {
                    showLoadingTooltip(x, y);
                }
            } else {
                if (isTooltipVisible) hideTooltip();
            }

        } catch (error) {
            console.warn('Selection Error:', error);
            hideTooltip();
        }
    }

    function debouncedSelectionHandler() {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(handleSelection, 300);
    }

    /**
     * 构建 Tooltip 内容并显示
     */
    function showEmojiTooltip(emojiData, emojiChar, x, y) {
        const name = emojiData.name.charAt(0).toUpperCase() + emojiData.name.slice(1);

        // --- 核心修改:构建 Noto Emoji SVG URL ---
        // 1. 获取 hexcode (例如: "0031-20E3" 或 "1F600")
        // 2. 转换为小写
        // 3. 将连字符 '-' 替换为下划线 '_'
        // 4. 拼接 URL: emoji_u + code + .svg
        const notoFilename = `emoji_u${emojiData.hexcode.toLowerCase().replace(/-/g, '_')}.svg`;
        const svgUrl = `${CONFIG.SVG_BASE_URL}/${notoFilename}`;

        let lang = navigator.language.toLowerCase();
        lang = lang.startsWith('zh')
            ? (/(tw|hk|mo|hant)/.test(lang) ? 'zh-hant' : 'zh-hans')
            : lang.slice(0, 2);
        const targetUrl = `https://www.emojiall.com/${lang}/emoji/${encodeURIComponent(emojiChar)}`;

        // 设置 Tooltip 容器属性
        tooltipElement.title = `Unicode: U+${emojiData.hexcode}`;
        tooltipElement.style.cursor = 'pointer';

        // 绑定点击跳转事件
        tooltipElement.onclick = (e) => {
            const selection = window.getSelection();
            const selectedText = selection.toString();
            if (selectedText.length > 0) {
                if (tooltipElement.contains(selection.anchorNode)) {
                    return;
                }
            }
            window.open(targetUrl, '_blank');
        };

        // --- 核心修改:使用传统 img 标签 ---
        // 直接设置 src,并在 img 标签上添加 onerror 处理加载失败的情况
        const iconHtml = `
            <img src="${svgUrl}"
                 alt="${emojiChar}"
                 style="width: 32px; height: 32px; vertical-align: middle; object-fit: contain;"
                 onerror="this.onerror=null; this.style.display='none'; this.nextElementSibling.style.display='block';"
            >
            <div style="display:none; font-size: 24px; line-height: 32px;">${emojiChar}</div>
        `;

        const content = `
            <div style="display: flex; align-items: center; gap: 12px; pointer-events: none;">
                <div style="width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;">
                    ${iconHtml}
                </div>
                <div>
                    <div style="font-weight: 600; line-height: 1.3; font-size: 14px; pointer-events: auto;">${name}</div>
                    <div style="color: #bbb; font-size: 12px; margin-top: 3px; pointer-events: auto;">
                        Group: ${emojiData.group}
                    </div>
                </div>
            </div>
        `;
        showTooltip(content, x, y);
        lastMousePosition = { x, y };
    }

    function handleMouseMove(e) {
        if (!isTooltipVisible) return;
        if (lastMousePosition.x === 0 && lastMousePosition.y === 0) return;
        const dx = Math.abs(e.clientX - lastMousePosition.x);
        const dy = Math.abs(e.clientY - lastMousePosition.y);
        if (dx > CONFIG.MOUSE_MOVE_THRESHOLD || dy > CONFIG.MOUSE_MOVE_THRESHOLD) {
            hideTooltip();
        }
    }


    // ====================
    // 💾 Data & Cache Logic
    // ====================

    function processAndCacheData(data, langCode, origin) {
        try {
            emojiMap.clear();
            data.forEach(item => {
                if (item.emoji && item.label && item.hexcode) {
                    const info = { name: item.label, group: CONFIG.GROUP_MAP[item.group] || 'Other', hexcode: item.hexcode };
                    emojiMap.set(item.emoji, info);
                }
                if (Array.isArray(item.skins)) {
                    item.skins.forEach(skin => {
                        if (skin.emoji && skin.label && skin.hexcode) {
                            emojiMap.set(skin.emoji, { name: skin.label, group: CONFIG.GROUP_MAP[skin.group || item.group] || 'Other', hexcode: skin.hexcode });
                        }
                    });
                }
            });

            if (origin === 'network') {
                GM_setValue(CONFIG.CACHE_KEY, { version: CONFIG.CACHE_VERSION, lang: langCode, timestamp: Date.now(), data: data });
            }
            console.log(`[Emoji Tooltip] ${emojiMap.size} emojis loaded (${langCode}) from ${origin}.`);
        } catch (e) {
            console.error('[Emoji Tooltip] Failed to process data', e);
        }
    }

    function fetchEmojiData(langCode, isFallback = false) {
        const url = `${CONFIG.BASE_URL}/${langCode}/data.json`;
        GM_xmlhttpRequest({
            method: 'GET', url: url,
            onload: function (response) {
                if (response.status === 200) {
                    processAndCacheData(JSON.parse(response.responseText), langCode, 'network');
                } else if (!isFallback && langCode !== 'en') {
                    fetchEmojiData('en', true);
                }
            },
            onerror: function () { if (!isFallback && langCode !== 'en') fetchEmojiData('en', true); }
        });
    }

    function loadEmojiData() {
        const browserLang = (navigator.language || 'en').split('-')[0];
        const cached = GM_getValue(CONFIG.CACHE_KEY, null);
        if (cached && cached.version === CONFIG.CACHE_VERSION) {
            if (cached.lang === browserLang || cached.lang === 'en') {
                try { processAndCacheData(cached.data, cached.lang, 'cache'); return; }
                catch (e) { GM_setValue(CONFIG.CACHE_KEY, null); }
            }
        }
        fetchEmojiData(browserLang);
    }


    // ====================
    // 启动程序
    // ====================
    function init() {
        initTooltipElement();
        loadEmojiData();

        document.addEventListener('mouseup', handleSelection, { passive: true });
        document.addEventListener('touchend', handleSelection, { passive: true });
        document.addEventListener('selectionchange', debouncedSelectionHandler, { passive: true });
        document.addEventListener('mousemove', handleMouseMove, { passive: true });
        window.addEventListener('scroll', hideTooltip, { passive: true });
        window.addEventListener('blur', hideTooltip);
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && isTooltipVisible) hideTooltip();
        });
    }

    init();
})();