/** * emoticon-recent —— 表情包「最近使用」 * * 场景:表情面板几十套图翻页找常用的太累,用过的排最前,一次点击直达。 * * 零侵入实现(不碰 Artalk 源码,靠 DOM 契约): * - Artalk 表情面板里每张原生图是 `key`,点击后插入 `:[key]` 令牌; * - 这里用事件委托记录点击(localStorage,最多 8 个,去重置顶); * - 面板顶部注入「最近」一栏;点最近里的图 = 找到面板里原生的同一张 * (按 alt 匹配)然后 programmatic click(),完全复用 Artalk 自己的插入逻辑。 * - 面板由 Artalk 动态创建/重建(PJAX、点回复),MutationObserver 兜底重注入。 */ (function () { if (window.__atkEmoticonRecentReady) return; // PJAX 重复执行防抖 window.__atkEmoticonRecentReady = true; var STORE_KEY = 'atk_recent_emoticons'; var MAX = 8; function load() { try { var arr = JSON.parse(localStorage.getItem(STORE_KEY) || '[]'); if (!Array.isArray(arr)) return []; return arr.filter(function (it) { return it && it.k && it.s; }); } catch (e) { return []; } } function save(list) { try { localStorage.setItem(STORE_KEY, JSON.stringify(list.slice(0, MAX))); } catch (e) {} } function record(key, src) { if (!key || !src) return; var list = load().filter(function (it) { return it.k !== key; }); list.unshift({ k: key, s: src }); save(list); } // 最近栏当前展示的键序(用于和存储比对,判断要不要重建) function blockKeys(panel) { var imgs = panel.querySelectorAll('.atk-recent-emoticons img'); var keys = []; for (var i = 0; i < imgs.length; i++) keys.push(imgs[i].getAttribute('alt')); return keys; } // ── 记录:点的是面板里的原生表情图(capture,先于 Artalk 的 onclick)──── document.addEventListener('click', function (e) { var el = e.target; if (!el || el.tagName !== 'IMG' || !el.closest) return; if (!el.closest('.atk-editor-plug-emoticons')) return; if (el.closest('.atk-recent-emoticons')) return; // 最近栏自己的点击单独处理 var key = el.getAttribute('alt'); if (key) { record(key, el.getAttribute('src') || ''); scheduleScan(); // 顺序变了,刷新最近栏 } }, true); // ── 在面板里找原生表情图(排除最近栏自己的),按 alt=key 匹配 ────────── function findNativeImg(panel, key) { var imgs = panel.querySelectorAll('img'); for (var i = 0; i < imgs.length; i++) { var im = imgs[i]; if (im.closest('.atk-recent-emoticons')) continue; if (im.getAttribute('alt') === key) return im; } return null; } function buildBlock() { var list = load(); if (!list.length) return null; var wrap = document.createElement('div'); wrap.className = 'atk-recent-emoticons'; var label = document.createElement('span'); label.className = 'atk-recent-label'; label.textContent = '最近'; wrap.appendChild(label); var ul = document.createElement('ul'); list.forEach(function (it) { var li = document.createElement('li'); var img = document.createElement('img'); img.src = it.s; img.alt = it.k; img.title = it.k; img.loading = 'lazy'; img.onclick = function () { var panel = img.closest('.atk-editor-plug-emoticons'); var orig = panel ? findNativeImg(panel, it.k) : null; if (orig) { orig.click(); // 复用 Artalk 的插入逻辑(插 :[key] 令牌) record(it.k, it.s); // 再点一次最近栏的图,顺序也要更新 scheduleScan(); } else { // 表情包配置变了,这张已经不存在 → 从最近记录里淘汰 save(load().filter(function (x) { return x.k !== it.k; })); scheduleScan(); } }; li.appendChild(img); ul.appendChild(li); }); wrap.appendChild(ul); return wrap; } function inject(panel) { if (!panel || !panel.querySelector) return; var existing = panel.querySelector('.atk-recent-emoticons'); var want = load(); if (existing) { // 内容比对:键序和存储一致就不动(避免无谓重建打断 hover), // 不一致(点了新表情 / 记录被淘汰)就整块重建 var have = blockKeys(panel); var same = have.length === want.length; if (same) { for (var i = 0; i < want.length; i++) { if (want[i].k !== have[i]) { same = false; break; } } } if (same) return; var fresh = buildBlock(); if (fresh) panel.replaceChild(fresh, existing); else existing.parentNode.removeChild(existing); return; } // 原生分组还没渲染出来时先不注入(找原生图会找不到),等下轮扫描 if (!panel.querySelector('img')) return; var block = buildBlock(); if (block) panel.insertBefore(block, panel.firstChild); } var scanTimer = null; function scheduleScan() { if (scanTimer) return; scanTimer = setTimeout(function () { scanTimer = null; try { var panels = document.querySelectorAll('.atk-editor-plug-emoticons'); for (var i = 0; i < panels.length; i++) inject(panels[i]); } catch (e) {} }, 120); } try { new MutationObserver(scheduleScan).observe(document.body, { childList: true, subtree: true }); } catch (e) {} document.addEventListener('focusin', scheduleScan, true); scheduleScan(); })();