153 lines
6.1 KiB
JavaScript
153 lines
6.1 KiB
JavaScript
/**
|
|||
|
|
* 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();
|
||
|
|
})();
|