feat(评论): 最近表情改为分组 tab + 提交后面板收起 + 小编徽章配色 + 走心评论 20 条
- emoticon-recent.js: 「最近使用」从面板顶部独立栏重构为分组切换条最左侧的
同构 tab(data-index="recent"),无记录时整个 tab 隐藏;复用 Artalk 原生
.atk-grp[data-type=image] 网格样式,零自定义布局。
- artalk.js:
· 新建评论成功(res.ok)后,对当前激活的插件按钮派发 click(),收起仍展开的
表情面板(Artalk 上游只 reset 输入内容、不关面板),内部 openedPlug 状态同步复位。
· 徽章处理末尾把徽章文字镜像到 data-badge 属性,供 CSS 按名字配色。
- artalk.html: 新增 .atk-badge[data-badge="小编"] 玫红实底规则(含暗色模式),
特异性高于博主规则,两者互不覆盖。
- featured-comments.html: 走心评论 limit 8 → 20(后端上限)。
- main.css: 移除旧 .atk-recent-emoticons 相关样式。
This commit is contained in:
1 parent
1b2e3d46f8
commit
79c9f2371c
5 files changed
+160
-114
No files matched your search
@@ -1,14 +1,26 @@
|
||||
/**
|
||||
* emoticon-recent —— 表情包「最近使用」
|
||||
* emoticon-recent —— 表情面板「最近使用」分组
|
||||
*
|
||||
* 场景:表情面板几十套图翻页找常用的太累,用过的排最前,一次点击直达。
|
||||
*
|
||||
* 零侵入实现(不碰 Artalk 源码,靠 DOM 契约):
|
||||
* - Artalk 表情面板里每张原生图是 `<img src alt=key>`,点击后插入 `:[key]` 令牌;
|
||||
* - 这里用事件委托记录点击(localStorage,最多 8 个,去重置顶);
|
||||
* - 面板顶部注入「最近」一栏;点最近里的图 = 找到面板里原生的同一张
|
||||
* (按 alt 匹配)然后 programmatic click(),完全复用 Artalk 自己的插入逻辑。
|
||||
* - 面板由 Artalk 动态创建/重建(PJAX、点回复),MutationObserver 兜底重注入。
|
||||
* 实现方式(零侵入,不碰 Artalk 源码,靠 DOM 契约):
|
||||
* - Artalk 表情面板的结构是固定的(见 Artalk.js emoticons 插件):
|
||||
* .atk-editor-plug-emoticons
|
||||
* ├── .atk-grp-wrap ← 分组内容区
|
||||
* │ └── .atk-grp[data-index=i][data-type=image]
|
||||
* │ └── span.atk-item[title=key] > img[src alt=key]
|
||||
* └── .atk-grp-switcher ← 底部分组切换条
|
||||
* └── span[data-index=i] (click → openGrp(i))
|
||||
* - 这里在切换条**最前面**插一个「最近」tab,在内容区插一个同构的 .atk-grp 装最近用过的表情。
|
||||
* 因为 .atk-item / [data-type=image] 的原生样式(63×63 网格、hover 底色)都靠类名命中,
|
||||
* 同构插入 = 排版自动和「抖音」等原生分组完全一致,一行自定义布局都不用写。
|
||||
* - tab 的点击由我们接管(显示自己、隐藏其它、切 active)。反过来,Artalk 的 openGrp 是按
|
||||
* data-index 数字匹配来显示/隐藏分组的 —— 我们的分组 data-index="recent" 永远匹配不上,
|
||||
* 所以用户点原生 tab 时它会被自动 display:none,两边互补,不会打架。
|
||||
* - 点最近里的图 = 找到面板里原生的同一张(按 alt=key 匹配)然后 programmatic click(),
|
||||
* 完全复用 Artalk 自己的插入逻辑(插 :[key] 令牌),不重复实现插入。
|
||||
* - 最近列表为空时整个 tab 不出现(没用过就没有「最近」,符合直觉)。
|
||||
* - 面板 DOM 首次打开才创建,MutationObserver 兜底重注入。
|
||||
*/
|
||||
(function () {
|
||||
if (window.__atkEmoticonRecentReady) return; // PJAX 重复执行防抖
|
||||
@@ -36,63 +48,52 @@
|
||||
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;
|
||||
function itemImg(item) {
|
||||
return item && item.querySelector ? item.querySelector('img[alt]') : null;
|
||||
}
|
||||
|
||||
// ── 记录:点的是面板里的原生表情图(capture,先于 Artalk 的 onclick)────
|
||||
// ── 记录:点的是面板里的原生表情(capture,先于 Artalk 绑在 .atk-item 上的 onclick)──
|
||||
// 监听 .atk-item 而不是 img:点在图与图之间的内边距上也算用过(原生 padding:5px)。
|
||||
document.addEventListener('click', function (e) {
|
||||
var el = e.target;
|
||||
if (!el || el.tagName !== 'IMG' || !el.closest) return;
|
||||
if (!el || !el.closest) return;
|
||||
if (!el.closest('.atk-editor-plug-emoticons')) return;
|
||||
if (el.closest('.atk-recent-emoticons')) return; // 最近栏自己的点击单独处理
|
||||
var key = el.getAttribute('alt');
|
||||
if (el.closest('.atk-recent-grp')) return; // 最近分组自己的点击单独处理
|
||||
var img = itemImg(el.closest('.atk-item'));
|
||||
if (!img) return;
|
||||
var key = img.getAttribute('alt');
|
||||
if (key) {
|
||||
record(key, el.getAttribute('src') || '');
|
||||
scheduleScan(); // 顺序变了,刷新最近栏
|
||||
record(key, img.getAttribute('src') || '');
|
||||
scheduleScan(); // 顺序变了,刷新最近分组
|
||||
}
|
||||
}, true);
|
||||
|
||||
// ── 在面板里找原生表情图(排除最近栏自己的),按 alt=key 匹配 ──────────
|
||||
// ── 在面板里找原生表情图(排除最近分组自己的),按 alt=key 匹配 ──────────
|
||||
function findNativeImg(panel, key) {
|
||||
var imgs = panel.querySelectorAll('img');
|
||||
var imgs = panel.querySelectorAll('.atk-grp-wrap > .atk-grp:not(.atk-recent-grp) img[alt]');
|
||||
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;
|
||||
if (imgs[i].getAttribute('alt') === key) return imgs[i];
|
||||
}
|
||||
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');
|
||||
function buildItems() {
|
||||
var frag = document.createDocumentFragment();
|
||||
load().forEach(function (it) {
|
||||
var item = document.createElement('span');
|
||||
item.className = 'atk-item';
|
||||
item.title = it.k;
|
||||
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');
|
||||
item.appendChild(img);
|
||||
item.onclick = function () {
|
||||
var panel = item.closest('.atk-editor-plug-emoticons');
|
||||
var orig = panel ? findNativeImg(panel, it.k) : null;
|
||||
if (orig) {
|
||||
orig.click(); // 复用 Artalk 的插入逻辑(插 :[key] 令牌)
|
||||
record(it.k, it.s); // 再点一次最近栏的图,顺序也要更新
|
||||
record(it.k, it.s); // 再点一次最近里的图,顺序也要更新
|
||||
scheduleScan();
|
||||
} else {
|
||||
// 表情包配置变了,这张已经不存在 → 从最近记录里淘汰
|
||||
@@ -100,37 +101,90 @@
|
||||
scheduleScan();
|
||||
}
|
||||
};
|
||||
li.appendChild(img);
|
||||
ul.appendChild(li);
|
||||
frag.appendChild(item);
|
||||
});
|
||||
wrap.appendChild(ul);
|
||||
return wrap;
|
||||
return frag;
|
||||
}
|
||||
|
||||
// 最近分组当前展示的键序(用于和存储比对,判断要不要重建)
|
||||
function grpKeys(grp) {
|
||||
var imgs = grp.querySelectorAll('img[alt]');
|
||||
var keys = [];
|
||||
for (var i = 0; i < imgs.length; i++) keys.push(imgs[i].getAttribute('alt'));
|
||||
return keys;
|
||||
}
|
||||
|
||||
// ── 切到「最近」:自己接管显示。原生 openGrp 会在用户点回原生 tab 时
|
||||
// 自动把我们的分组藏起来(按 data-index 数字匹配),所以这里不需要反向干预。──
|
||||
function showRecent(panel) {
|
||||
var switcher = panel.querySelector('.atk-grp-switcher');
|
||||
var wrap = panel.querySelector('.atk-grp-wrap');
|
||||
if (!switcher || !wrap) return;
|
||||
var kids = wrap.children;
|
||||
for (var i = 0; i < kids.length; i++) {
|
||||
kids[i].style.display = kids[i].classList.contains('atk-recent-grp') ? '' : 'none';
|
||||
}
|
||||
var spans = switcher.querySelectorAll('span');
|
||||
for (var j = 0; j < spans.length; j++) spans[j].classList.remove('active');
|
||||
var tab = switcher.querySelector('.atk-recent-tab');
|
||||
if (tab) tab.classList.add('active');
|
||||
}
|
||||
|
||||
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; }
|
||||
var switcher = panel.querySelector('.atk-grp-switcher');
|
||||
var wrap = panel.querySelector('.atk-grp-wrap');
|
||||
// 只有一组表情时 Artalk 不建切换条(没有 tab 可挂)→ 不注入
|
||||
if (!switcher || !wrap) return;
|
||||
// 原生分组还没渲染出来时先不注入(找原生图会找不到),等下轮扫描
|
||||
if (!wrap.querySelector('.atk-grp:not(.atk-recent-grp) img')) return;
|
||||
|
||||
var list = load();
|
||||
var tab = switcher.querySelector('.atk-recent-tab');
|
||||
var grp = wrap.querySelector('.atk-recent-grp');
|
||||
|
||||
// 没有最近记录 → 整个 tab 不出现;若正停在「最近」上则切回第一组
|
||||
if (!list.length) {
|
||||
if (tab) {
|
||||
var wasActive = tab.classList.contains('active');
|
||||
switcher.removeChild(tab);
|
||||
if (grp && grp.parentNode) grp.parentNode.removeChild(grp);
|
||||
if (wasActive) {
|
||||
var first = switcher.querySelector('span[data-index="0"]');
|
||||
if (first) first.click();
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
if (!tab) {
|
||||
tab = document.createElement('span');
|
||||
tab.className = 'atk-recent-tab';
|
||||
tab.setAttribute('data-index', 'recent');
|
||||
tab.textContent = '最近';
|
||||
tab.onclick = function () { showRecent(panel); };
|
||||
switcher.insertBefore(tab, switcher.firstChild); // 放在「抖音」左边
|
||||
}
|
||||
if (!grp) {
|
||||
grp = document.createElement('div');
|
||||
grp.className = 'atk-grp atk-recent-grp';
|
||||
grp.setAttribute('data-index', 'recent');
|
||||
grp.setAttribute('data-grp-name', '最近');
|
||||
grp.setAttribute('data-type', 'image'); // 命中原生 63×63 网格样式
|
||||
grp.style.display = 'none'; // 与原生分组一致:默认不显示
|
||||
wrap.appendChild(grp);
|
||||
}
|
||||
// 内容比对:键序一致就跳过重建(避免无谓重建打断 hover)
|
||||
var have = grpKeys(grp);
|
||||
var same = have.length === list.length;
|
||||
if (same) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].k !== have[i]) { same = false; break; }
|
||||
}
|
||||
}
|
||||
if (same) return;
|
||||
grp.innerHTML = '';
|
||||
grp.appendChild(buildItems());
|
||||
}
|
||||
|
||||
var scanTimer = null;
|
||||
|
||||
Reference in new issue
Block a user