Files
blog/themes/Ying/assets/js/modules/emoticon-recent.js
T

153 lines
6.1 KiB
JavaScript
Raw Normal View History

/**
* emoticon-recent —— 表情包「最近使用」
*
* 场景:表情面板几十套图翻页找常用的太累,用过的排最前,一次点击直达。
*
* 零侵入实现(不碰 Artalk 源码,靠 DOM 契约):
* - Artalk 表情面板里每张原生图是 `<img src alt=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();
})();