feat(comments): 匿名评论 + 表情最近使用;fix: 自动填充聚焦提示 & 邮件模板回退原版 & 返回顶部改 CSS 动画
- 匿名评论:comments.is_anonymous 列;对外昵称/头像/网址/徽标/等级/IP归属全部脱敏, 真实身份留 users 表(回复通知照发、autofill 不受污染);邮件昵称也用「匿名」防泄露 - 表情包最近使用:面板顶部「最近」栏,localStorage 记录 8 个,零侵入复用 Artalk 插入逻辑 - 匿名开关插在 textarea 与底栏之间独立一行,不挤移动端底栏 - 自动填充:提示改 focusin 触发(原 input+sessionStorage 一次性门导致永不触发); 已填昵称/网址时不覆盖并弹提示 - 邮件模板 UI 回退原版深色气泡设计,保留表情内联尺寸/主题文案等逻辑修复 - 返回顶部:rAF 自绘改 CSS scroll-behavior(滚动期临时挂类),修移动端掉帧
This commit is contained in:
1 parent
090262d278
commit
215929a67f
11 files changed
+587
-207
No files matched your search
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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();
|
||||
})();
|
||||
Reference in new issue
Block a user