Greasy Fork

Greasy Fork is available in English.

Emoji 含义选中提示

在网页中选中 Emoji 时,显示其含义、名称和分类。非国旗使用 Google Noto SVG,国旗回退至 Emojiall PNG 源。(支持移动端)

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

您需要先安装一个扩展,例如 篡改猴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.18
// @description:zh-CN 在网页中选中 Emoji 时,显示其含义、名称和分类。非国旗使用 Google Noto SVG,国旗回退至 Emojiall PNG 源。(支持移动端)
// @description When an emoji is selected, display its meaning, name, and category. Uses Google Noto SVG, falls back to Emojiall PNG for flags.
// @author      Kaesinol
// @match       *://*/*
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_openInTab
// @connect     cdn.jsdelivr.net
// @connect     raw.githubusercontent.com
// @connect     www.emojiall.com 
// @run-at      document-start
// @license     MIT
// @icon        https://www.emojiall.com/images/60/google/1f609.png
// ==/UserScript==
 
(function () {
    'use strict';
 
    // ====================
    // ⚙️ Configuration
    // ====================
    const CONFIG = {
        BASE_URL: 'https://cdn.jsdelivr.net/npm/[email protected]',
        // SVG 源:Google Noto Emoji
        SVG_BASE_URL: 'https://raw.githubusercontent.com/googlefonts/noto-emoji/refs/heads/main/svg',
        // PNG 回退源:Emojiall
        PNG_BASE_URL: 'https://www.emojiall.com/images/60/google', 
        CACHE_KEY: 'emoji_tooltip_data_v5',
        CACHE_VERSION: '1.18', // 版本更新
        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);
        let imageUrl, imageType;
 
        // --- 核心修改:判断是否为国旗 ---
        if (emojiData.group === 'Flags') {
            // 1. 国旗:使用 Emojiall 的 PNG 源
            imageUrl = `${CONFIG.PNG_BASE_URL}/${emojiData.hexcode.toLowerCase()}.png`;
            imageType = 'png';
        } else {
            // 2. 非国旗:使用 Noto Emoji 的 SVG 源
            let hex = emojiData.hexcode.toLowerCase();
            // Noto 文件名处理: 移除 fe0f 并将 - 换成 _
            hex = hex.replace(/-?fe0f/g, ''); 
            hex = hex.replace(/-/g, '_');
            const notoFilename = `emoji_u${hex}.svg`;
            imageUrl = `${CONFIG.SVG_BASE_URL}/${notoFilename}`;
            imageType = 'svg';
        }
 
        // --- 跳转链接设置 (保持不变) ---
        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');
        };
 
        // --- 图像 HTML ---
        const iconHtml = `
            <img src="${imageUrl}"
                 alt="${emojiChar}"
                 title="Source: ${imageType.toUpperCase()}"
                 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();
})();