Greasy Fork is available in English.
在网页中选中 Emoji 时,强制以 PNG 格式图片显示其高清图标及名称和分类,支持所有带肤色变体的 Emoji。
当前为
// ==UserScript==
// @name Emoji Tooltip
// @name:zh-CN Emoji 含义选中提示
// @namespace http://tampermonkey.net/
// @version 1.11
// @description:zh-CN 在网页中选中 Emoji 时,强制以 PNG 格式图片显示其高清图标及名称和分类,支持所有带肤色变体的 Emoji。
// @description When an emoji is selected on a webpage, it forces the display of its high-resolution icon, name, and category as a PNG image, supporting all emojis with skin tone variations.
// @author Kaesinol
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @connect cdn.jsdelivr.net
// @connect www.emojiall.com
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ====================
// ⚙️ Configuration
// ====================
const CONFIG = {
BASE_URL: 'https://cdn.jsdelivr.net/npm/[email protected]',
PNG_BASE_URL: 'https://www.emojiall.com/images/60/google',
CACHE_KEY: 'emoji_tooltip_data_v5',
CACHE_VERSION: '1.11',
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;
let isTooltipVisible = false;
let lastMousePosition = { x: 0, y: 0 };
// ====================
// 🎨 Tooltip UI Logic
// ====================
/** 初始化 Tooltip 元素并注入到 DOM */
function initTooltipElement() {
tooltipElement = document.createElement('div');
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); pointer-events: none;
border: 1px solid #444;
`;
if (document.body) {
document.body.appendChild(tooltipElement);
} else {
// document-start 运行,可能 body 还没准备好,使用 MutationObserver
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;
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';
isTooltipVisible = false;
}, 200);
}
/** 显示数据加载中的提示Tooltip */
function showLoadingTooltip(x, y) {
const content = `
<div style="display: flex; align-items: center; gap: 12px">
<div style="font-weight: 600; font-size: 14px; color: #f9e67d;">
✨ Loading Emoji Data...
</div>
<div style="color: #bbb; font-size: 12px;">
(Please wait a moment)
</div>
</div>
`;
showTooltip(content, x, y);
lastMousePosition = { x, y };
}
// ====================
// 🧠 Event Handling
// ====================
/** 鼠标松开(选中)事件处理 */
function handleSelection(e) {
let selection;
try {
selection = window.getSelection().toString().trim();
} catch (error) {
hideTooltip();
return;
}
if (!selection || selection.length < 1 || selection.length > 15) {
if (isTooltipVisible) hideTimer = setTimeout(hideTooltip, 2000);
return;
}
const emojiData = emojiMap.get(selection);
if (emojiData) {
// 1. 数据已加载且匹配:显示 Emoji 详情
showEmojiTooltip(emojiData, selection, e.clientX, e.clientY);
} else if (emojiMap.size === 0) {
// 2. 数据未加载(Map为空):显示加载提示
showLoadingTooltip(e.clientX, e.clientY);
} else {
// 3. 数据已加载但未匹配:隐藏 Tooltip
if (isTooltipVisible) hideTooltip();
}
}
/** 构建 Tooltip 内容并显示 */
function showEmojiTooltip(emojiData, emojiChar, x, y) {
const name = emojiData.name.charAt(0).toUpperCase() + emojiData.name.slice(1);
const pngUrl = `${CONFIG.PNG_BASE_URL}/${emojiData.hexcode.toLowerCase()}.png`;
const iconHtml = `<img src="${pngUrl}" alt="${emojiChar}" style="width: 32px; height: 32px; vertical-align: middle; object-fit: contain;">`;
const content = `
<div style="display: flex; align-items: center; gap: 12px">
${iconHtml}
<div>
<div style="font-weight: 600; line-height: 1.3; font-size: 14px;">${name}</div>
<div style="color: #bbb; font-size: 12px; margin-top: 3px;">
Group: ${emojiData.group}
</div>
</div>
</div>
`;
showTooltip(content, x, y);
lastMousePosition = { x, y };
}
/** 鼠标大幅度移动事件处理 (用于自动隐藏) */
function handleMouseMove(e) {
if (!isTooltipVisible) 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();
lastMousePosition = { x: e.clientX, y: e.clientY };
}
}
// ====================
// 💾 Data & Cache Logic
// ====================
/**
* 处理原始数据并保存到 emojiMap 和 GM_setValue。
*/
function processAndCacheData(data, langCode, origin) {
try {
emojiMap.clear();
data.forEach(item => {
if (item.emoji && item.label && item.hexcode) {
emojiMap.set(item.emoji, {
name: item.label,
group: CONFIG.GROUP_MAP[item.group] || 'Other',
hexcode: item.hexcode
});
}
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);
}
}
/** 从网络获取 Emoji 数据 */
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') {
console.warn(`[Emoji Tooltip] Failed to load ${langCode}. Falling back to English (en).`);
fetchEmojiData('en', true);
}
},
onerror: function (error) {
console.error(`[Emoji Tooltip] Network error loading ${langCode}:`, error);
if (!isFallback && langCode !== 'en') {
fetchEmojiData('en', true);
}
}
});
}
/** 加载或获取 Emoji 数据 */
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) {
console.error('[Emoji Tooltip] Cached data corrupted, forcing network reload.');
GM_setValue(CONFIG.CACHE_KEY, null);
}
}
}
fetchEmojiData(browserLang);
}
// ====================
// 启动程序
// ====================
function init() {
initTooltipElement(); // 确保 UI 元素立即创建
loadEmojiData(); // 开始异步加载数据
// 绑定事件,Tooltip 已经就绪,可以接收用户的选中操作
document.addEventListener('mouseup', handleSelection, { 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();
})();