2026-10-26 13:25:51 +08:00
|
|
|
|
/*!
|
|
|
|
|
|
* 归档式文章列表([Archives])—— 年份切换
|
|
|
|
|
|
* Yongquan Yang <yangyq.net>
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 它只做一件事 ────────────────────────────────────────────
|
|
|
|
|
|
* 把 `<script type="application/json" data-role="data">` 里那份数据
|
|
|
|
|
|
* 按选中的年份重建成 DOM,替换 `[data-role="body"]` 的内容。
|
|
|
|
|
|
* **不发任何请求** —— 数据首屏就全在页面里了。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么"不刷新"要做成增强而不是替代 ──────────────────────
|
|
|
|
|
|
* 导航按钮本身是 `<a href="?arc_year=2025">`。这意味着:
|
|
|
|
|
|
* · JS 没加载 / 报错 → 点下去是新的一次请求,仍然能看到 2025 的归档;
|
|
|
|
|
|
* · 中键 / Ctrl+点击 → 新标签打开对应年份,行为符合直觉;
|
|
|
|
|
|
* · 爬虫 → 顺着链接能爬到每一年。
|
|
|
|
|
|
* JS 接管的只是"同页内的普通左键点击":`preventDefault` 后走 DOM 重建。
|
|
|
|
|
|
* 这样"不刷新"是锦上添花,而不是唯一能用的一条路。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么用事件委托而不是给每个按钮绑 ──────────────────────
|
|
|
|
|
|
* 每次切年都会重建导航(禁用态、目标年份都要变),逐个绑就得逐个解绑,
|
|
|
|
|
|
* 漏一个就是"点一下就切两年"。委托到容器上,重建多少次都不会漏。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么用 scrollIntoView 而不是 scrollTop ────────────────
|
|
|
|
|
|
* 切年后列表高度会变(12 个月 → 3 个月)。若不做处理,用户可能停在
|
|
|
|
|
|
* 页面中部、看到的是一片空白,会以为"点坏了"。切完把容器顶部滚到
|
|
|
|
|
|
* 视口内即可,同时尊重 `prefers-reduced-motion`。
|
|
|
|
|
|
*/
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
|
|
var ROOT_SEL = '.yang-archives';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 安全的 textContent 设置
|
|
|
|
|
|
*
|
|
|
|
|
|
* 一律走 textContent,**绝不用 innerHTML 拼标题** ——
|
|
|
|
|
|
* 文章标题里可能带 `<`、`&`、引号,拼进去就是 XSS 面。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function txt(node, s) {
|
|
|
|
|
|
node.textContent = s == null ? '' : String(s);
|
|
|
|
|
|
return node;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 建一个元素
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} tag
|
|
|
|
|
|
* @param {string} cls
|
|
|
|
|
|
* @param {string} [text]
|
|
|
|
|
|
* @returns {HTMLElement}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function el(tag, cls, text) {
|
|
|
|
|
|
var n = document.createElement(tag);
|
|
|
|
|
|
if (cls) {
|
|
|
|
|
|
n.className = cls;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (text !== undefined) {
|
|
|
|
|
|
txt(n, text);
|
|
|
|
|
|
}
|
|
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 读页面里的 JSON 数据
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 解析失败必须**出声**,不能静默 return。
|
|
|
|
|
|
* 这个 JSON 是 PHP 用 JSON_HEX_TAG 等四个 flag 编的,
|
|
|
|
|
|
* 正常不可能坏;一旦坏了(比如别的插件过滤了短代码输出),
|
|
|
|
|
|
* 页面会表现为"按钮点了没反应",而控制台一个字都没有 ——
|
|
|
|
|
|
* 那是最难查的一类。所以这里必须 console.error。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {HTMLElement} root
|
|
|
|
|
|
* @returns {Object|null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function readData(root) {
|
|
|
|
|
|
var node = root.querySelector('[data-role="data"]');
|
|
|
|
|
|
if (!node) {
|
|
|
|
|
|
if (window.console && console.error) {
|
|
|
|
|
|
console.error('[yang-archives] 找不到数据节点 [data-role="data"],'
|
|
|
|
|
|
+ '年份切换不可用。检查 inc/theme-archives.php 的输出。');
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(node.textContent || '{}');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (window.console && console.error) {
|
|
|
|
|
|
console.error('[yang-archives] 数据解析失败,年份切换不可用:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 渲染单条
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 类名必须与 PHP 的 yang_archives_render_item() **逐个一致**。
|
|
|
|
|
|
* 首屏是 PHP 出的、切年后是这里出的,两者样式必须完全一样,
|
|
|
|
|
|
* 否则会出现"刚进来是对的、点一下年就变样"。
|
|
|
|
|
|
* 判据在测试 `_archives_e2e.js`:它比对两条路径建出的类名集合。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {Object} it
|
|
|
|
|
|
* @param {number} month
|
|
|
|
|
|
* @param {Object} i18n
|
|
|
|
|
|
* @returns {HTMLLIElement}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildItem(it, month, i18n) {
|
|
|
|
|
|
var li = el('li', 'ar-item');
|
|
|
|
|
|
|
|
|
|
|
|
var date = el('time', 'ar-date', pad2(month) + '-' + pad2(it.day));
|
|
|
|
|
|
date.setAttribute('datetime', it.date || '');
|
|
|
|
|
|
li.appendChild(date);
|
|
|
|
|
|
|
|
|
|
|
|
var a = el('a', 'ar-title');
|
|
|
|
|
|
a.href = it.url || '#';
|
|
|
|
|
|
txt(a, it.title || i18n.untitled);
|
|
|
|
|
|
li.appendChild(a);
|
|
|
|
|
|
|
|
|
|
|
|
var stats = el('span', 'ar-stats');
|
|
|
|
|
|
stats.appendChild(buildStat('views', 'i-hot', it.views, i18n.views));
|
|
|
|
|
|
stats.appendChild(buildStat('comments', 'i-comments', it.comments, i18n.comments));
|
|
|
|
|
|
stats.appendChild(buildStat('love', 'i-good', it.love, i18n.love));
|
|
|
|
|
|
li.appendChild(stats);
|
|
|
|
|
|
|
|
|
|
|
|
return li;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 单个计数
|
|
|
|
|
|
*
|
|
|
|
|
|
* 结构与 PHP 的 yang_archives_render_stat() 一致(含 data-zero 与 sr-only)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} kind
|
|
|
|
|
|
* @param {string} icon
|
|
|
|
|
|
* @param {number} num
|
|
|
|
|
|
* @param {string} label
|
|
|
|
|
|
* @returns {HTMLSpanElement}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildStat(kind, icon, num, label) {
|
|
|
|
|
|
var n = parseInt(num, 10);
|
|
|
|
|
|
if (!isFinite(n) || n < 0) {
|
|
|
|
|
|
n = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var s = el('span', 'ar-stat ar-stat-' + kind);
|
|
|
|
|
|
s.setAttribute('title', label || '');
|
|
|
|
|
|
s.setAttribute('data-zero', n === 0 ? '1' : '0');
|
|
|
|
|
|
|
|
|
|
|
|
var i = document.createElement('i');
|
|
|
|
|
|
i.className = 'kicon ' + icon;
|
|
|
|
|
|
i.setAttribute('aria-hidden', 'true');
|
|
|
|
|
|
s.appendChild(i);
|
|
|
|
|
|
|
|
|
|
|
|
s.appendChild(el('span', 'ar-num', formatNum(n)));
|
|
|
|
|
|
|
|
|
|
|
|
var sr = el('span', 'sr-only', label || '');
|
|
|
|
|
|
s.appendChild(sr);
|
|
|
|
|
|
|
|
|
|
|
|
return s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 两位补零 */
|
|
|
|
|
|
function pad2(v) {
|
|
|
|
|
|
v = String(v == null ? '' : v);
|
|
|
|
|
|
return v.length >= 2 ? v : ('0' + v);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 千分位
|
|
|
|
|
|
*
|
|
|
|
|
|
* 与服务端的 number_format_i18n 对齐。这里只做最常见的"每三位一个
|
|
|
|
|
|
* 逗号",不跟随 WP 的语言设置 —— 归档计数是纯数字,差异只在
|
|
|
|
|
|
* 少数语言的分隔符上;真要精确对齐,应该把格式化后的字符串
|
|
|
|
|
|
* 直接从 PHP 传下来。目前站内是中文/英文,逗号一致。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function formatNum(n) {
|
|
|
|
|
|
var s = String(n);
|
|
|
|
|
|
if (s.length <= 3) {
|
|
|
|
|
|
return s;
|
|
|
|
|
|
}
|
|
|
|
|
|
return s.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 渲染某一年的列表
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {Object} yd {count, months}
|
|
|
|
|
|
* @param {number} year
|
|
|
|
|
|
* @param {number} showMonth
|
|
|
|
|
|
* @param {Object} i18n
|
|
|
|
|
|
* @returns {DocumentFragment}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildYear(yd, year, showMonth, i18n) {
|
|
|
|
|
|
var frag = document.createDocumentFragment();
|
|
|
|
|
|
if (!yd || !yd.months) {
|
|
|
|
|
|
frag.appendChild(el('p', 'ar-empty', i18n.empty));
|
|
|
|
|
|
return frag;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-26 18:42:40 +08:00
|
|
|
|
/* 月份**倒序**(12 月在先)—— 与 PHP 的 krsort 一致。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 3.2.48 从升序改成降序,必须与 inc/theme-archives.php 那个
|
|
|
|
|
|
* krsort 同向。这里写死方向、不读服务端顺序,是因为切年时
|
|
|
|
|
|
* JS 拿到的 JSON 里 months 是**对象**,对象键序在 JS 里对
|
|
|
|
|
|
* 纯数字键会**自动升序**(ECMAScript 的整数索引规则),
|
|
|
|
|
|
* `Object.keys()` 得到的本来就是 1..12 升序 —— 也就是说
|
|
|
|
|
|
* 「照抄服务端顺序」这件事在这里根本做不到,只能自己排。
|
|
|
|
|
|
*/
|
2026-10-26 13:25:51 +08:00
|
|
|
|
var months = Object.keys(yd.months).map(Number).sort(function (a, b) {
|
2026-10-26 18:42:40 +08:00
|
|
|
|
return b - a;
|
2026-10-26 13:25:51 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!months.length) {
|
|
|
|
|
|
frag.appendChild(el('p', 'ar-empty', i18n.empty));
|
|
|
|
|
|
return frag;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
months.forEach(function (m) {
|
|
|
|
|
|
var md = yd.months[m] || yd.months[String(m)];
|
|
|
|
|
|
if (!md) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var box = el('div', 'ar-month');
|
|
|
|
|
|
|
|
|
|
|
|
if (showMonth) {
|
|
|
|
|
|
var h = el('h4', 'ar-month-title', fmt(i18n.month, pad2MonthLabel(m)));
|
2026-10-26 18:42:40 +08:00
|
|
|
|
/* 3.2.48:裸数字 → 「共 N 篇」,与 PHP 侧同一文案。
|
|
|
|
|
|
* i18n.monthCount 由 PHP 透传('共 %s 篇'),这里只做占位替换,
|
|
|
|
|
|
* **不要**在 JS 里硬写中文 —— 站点语言切换时两侧会不一致。 */
|
|
|
|
|
|
var cnt = el('span', 'ar-month-count',
|
|
|
|
|
|
fmt(i18n.monthCount || '共 %s 篇', formatNum(md.count || 0)));
|
2026-10-26 13:25:51 +08:00
|
|
|
|
h.appendChild(cnt);
|
|
|
|
|
|
box.appendChild(h);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var ul = el('ul', 'ar-list');
|
|
|
|
|
|
(md.items || []).forEach(function (it) {
|
|
|
|
|
|
ul.appendChild(buildItem(it, m, i18n));
|
|
|
|
|
|
});
|
|
|
|
|
|
box.appendChild(ul);
|
|
|
|
|
|
|
|
|
|
|
|
frag.appendChild(box);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return frag;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 月份标题文字
|
|
|
|
|
|
*
|
|
|
|
|
|
* PHP 侧用的是 number_format_i18n($m),中文环境就是 "10"。
|
|
|
|
|
|
* 这里不做补零(与 PHP 保持一致),只在必要时补。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function pad2MonthLabel(m) {
|
|
|
|
|
|
return String(m);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 极简 %s 占位符替换(不引入 sprintf 依赖) */
|
|
|
|
|
|
function fmt(tpl, val) {
|
|
|
|
|
|
if (typeof tpl !== 'string') {
|
|
|
|
|
|
return String(val);
|
|
|
|
|
|
}
|
|
|
|
|
|
return tpl.replace('%s', val);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 找一个方向的目标年份
|
|
|
|
|
|
*
|
|
|
|
|
|
* `order` 是**倒序**年份数组:[2026, 2025, 2024]
|
|
|
|
|
|
* · prev(上一年/更近)→ 索引 - 1
|
|
|
|
|
|
* · next(下一年/更早)→ 索引 + 1
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 这个方向与"按下一年却回到过去"的文案一致,见 PHP 里的说明。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {number[]} order
|
|
|
|
|
|
* @param {number} year
|
|
|
|
|
|
* @param {string} dir prev|next
|
|
|
|
|
|
* @returns {number} 0 表示没有
|
|
|
|
|
|
*/
|
|
|
|
|
|
function pickTarget(order, year, dir) {
|
|
|
|
|
|
var i = order.indexOf(Number(year));
|
|
|
|
|
|
if (i < 0) {
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
var t = dir === 'prev' ? i - 1 : i + 1;
|
|
|
|
|
|
return order[t] === undefined ? 0 : Number(order[t]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重建导航
|
|
|
|
|
|
*
|
|
|
|
|
|
* 每次切年都要重建:目标年份变了、禁用态变了。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {HTMLElement} navBox .ar-nav 容器
|
|
|
|
|
|
* @param {Object} cfg {order, year, i18n, baseUrl}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function rebuildNav(navBox, cfg) {
|
|
|
|
|
|
if (!navBox) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
navBox.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
var prev = pickTarget(cfg.order, cfg.year, 'prev');
|
|
|
|
|
|
var next = pickTarget(cfg.order, cfg.year, 'next');
|
|
|
|
|
|
|
|
|
|
|
|
navBox.appendChild(buildNavBtn(prev, 'prev', cfg));
|
|
|
|
|
|
navBox.appendChild(buildNavBtn(next, 'next', cfg));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 单个导航按钮
|
|
|
|
|
|
*
|
|
|
|
|
|
* 结构与 PHP 的 yang_archives_render_nav_btn() 一致。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {number} target
|
|
|
|
|
|
* @param {string} dir
|
|
|
|
|
|
* @param {Object} cfg
|
|
|
|
|
|
* @returns {HTMLElement}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildNavBtn(target, dir, cfg) {
|
|
|
|
|
|
var isPrev = dir === 'prev';
|
|
|
|
|
|
var label = isPrev ? cfg.i18n.prev : cfg.i18n.next;
|
|
|
|
|
|
var arrow = isPrev ? '\u2191' : '\u2193';
|
|
|
|
|
|
|
|
|
|
|
|
if (!target) {
|
|
|
|
|
|
var span = el('span', 'ar-nav-btn is-disabled');
|
|
|
|
|
|
span.setAttribute('aria-disabled', 'true');
|
|
|
|
|
|
span.appendChild(el('span', 'ar-nav-arrow', arrow));
|
|
|
|
|
|
span.appendChild(el('span', 'ar-nav-label', label));
|
|
|
|
|
|
return span;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var a = el('a', 'ar-nav-btn ar-nav-' + dir);
|
|
|
|
|
|
/* 回退地址带上目标年份:无 JS / 新标签打开都能用。
|
|
|
|
|
|
* 用 URL 对象改参数,不去猜 baseUrl 里有没有 ? —— 手工拼
|
|
|
|
|
|
* 字符串在带 ? 的地址上会拼出 `?a=1&arc_year=2` 这种错。 */
|
|
|
|
|
|
a.href = buildYearUrl(cfg.baseUrl, target);
|
|
|
|
|
|
a.setAttribute('data-year', String(target));
|
|
|
|
|
|
a.setAttribute('rel', 'nofollow');
|
|
|
|
|
|
|
|
|
|
|
|
a.appendChild(el('span', 'ar-nav-arrow', arrow));
|
|
|
|
|
|
a.appendChild(el('span', 'ar-nav-label', label));
|
|
|
|
|
|
/* 🔴 年份不千分位。`formatNum(2025)` 会输出「2,025」——
|
|
|
|
|
|
* 与 PHP 侧的 number_format_i18n 犯的是同一个错,两边一起改。
|
|
|
|
|
|
* 年份是编号,不是数量。 */
|
|
|
|
|
|
a.appendChild(el('span', 'ar-nav-year', String(target)));
|
|
|
|
|
|
return a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 拼 ?arc_year=YYYY
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} base
|
|
|
|
|
|
* @param {number} year
|
|
|
|
|
|
* @returns {string}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function buildYearUrl(base, year) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var u = new URL(base || window.location.href, window.location.href);
|
|
|
|
|
|
u.searchParams.set('arc_year', String(year));
|
|
|
|
|
|
/* 去掉 page 参数:切年后还带着原来的分页页码,在分页型页面上
|
|
|
|
|
|
* 会跳到一个不存在的页,表现是"404 或空白"。 */
|
|
|
|
|
|
u.searchParams.delete('page');
|
|
|
|
|
|
return u.toString();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// URL 不可用(很老的浏览器)就退回字符串拼
|
|
|
|
|
|
var s = (base || window.location.pathname);
|
|
|
|
|
|
return s + (s.indexOf('?') >= 0 ? '&' : '?') + 'arc_year=' + year;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 切到某一年
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {HTMLElement} root
|
|
|
|
|
|
* @param {Object} data
|
|
|
|
|
|
* @param {number} year
|
|
|
|
|
|
* @param {boolean} [push] 是否把年份写进地址栏
|
|
|
|
|
|
*/
|
|
|
|
|
|
function switchTo(root, data, year, push) {
|
|
|
|
|
|
var yd = data.years[year] || data.years[String(year)];
|
|
|
|
|
|
if (!yd) {
|
|
|
|
|
|
// 目标年份没有数据 —— 正常不会走到(导航按 order 算的),
|
|
|
|
|
|
// 但**不能静默**:出声比"点了没反应"好查得多。
|
|
|
|
|
|
if (window.console && console.warn) {
|
|
|
|
|
|
console.warn('[yang-archives] 没有 ' + year + ' 年的归档数据');
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var body = root.querySelector('[data-role="body"]');
|
|
|
|
|
|
var titleEl = root.querySelector('[data-role="year-title"]');
|
|
|
|
|
|
var totalEl = root.querySelector('[data-role="year-total"]');
|
|
|
|
|
|
var i18n = data.i18n || {};
|
|
|
|
|
|
|
|
|
|
|
|
if (body) {
|
|
|
|
|
|
body.innerHTML = '';
|
|
|
|
|
|
body.appendChild(buildYear(yd, year, data.showMonth, i18n));
|
|
|
|
|
|
|
|
|
|
|
|
/* 淡入:先移除再加类,确保连续切年时动画每次都会重放
|
|
|
|
|
|
* (同一个类名不加移除,浏览器认为没变化,动画不会重跑)。 */
|
|
|
|
|
|
body.classList.remove('is-switching');
|
|
|
|
|
|
// 读一次布局属性强制回流,让上面的移除生效
|
|
|
|
|
|
void body.offsetWidth;
|
|
|
|
|
|
body.classList.add('is-switching');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (titleEl) {
|
|
|
|
|
|
titleEl.textContent = String(year);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (totalEl) {
|
|
|
|
|
|
totalEl.textContent = fmt(i18n.totalFmt || '共 %s 篇', formatNum(yd.count || 0));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
root.setAttribute('data-year', String(year));
|
|
|
|
|
|
data.current = year;
|
|
|
|
|
|
|
|
|
|
|
|
// 两个导航(上排 + 下排)都要重建
|
|
|
|
|
|
var navs = root.querySelectorAll('.ar-nav');
|
|
|
|
|
|
for (var i = 0; i < navs.length; i++) {
|
|
|
|
|
|
rebuildNav(navs[i], {
|
|
|
|
|
|
order: data.order,
|
|
|
|
|
|
year: year,
|
|
|
|
|
|
i18n: i18n,
|
|
|
|
|
|
baseUrl: data.baseUrl || window.location.href
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 地址栏同步。用 replaceState 而不是 pushState:
|
|
|
|
|
|
* 翻归档年份是"同一次浏览里的筛选",不该在历史里留下十几条记录
|
|
|
|
|
|
* (用户按后退会一年一年退回去,体验很差)。
|
|
|
|
|
|
* 同时让复制出去的链接能定位到当前年份 —— 这正是 PHP 侧
|
|
|
|
|
|
* 「?arc_year 优先于短代码 year」的配套。 */
|
|
|
|
|
|
if (push && window.history && typeof window.history.replaceState === 'function') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
window.history.replaceState(
|
|
|
|
|
|
{ arcYear: year },
|
|
|
|
|
|
'',
|
|
|
|
|
|
buildYearUrl(data.baseUrl || window.location.href, year)
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
/* 跨域等情况下 replaceState 会抛。地址栏没同步不影响功能,
|
|
|
|
|
|
* 但也不该让整个切换失败 —— 所以只吞掉这一个异常,
|
|
|
|
|
|
* 上面的 DOM 已经更新好了。 */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 把容器顶部拉回视口。
|
|
|
|
|
|
* 🔴 必须放在 DOM 更新**之后**:切年前量的位置,切年后高度变了,
|
|
|
|
|
|
* 滚到旧位置可能正好落在空白处。 */
|
|
|
|
|
|
scrollToRoot(root);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 切年后把容器顶部带回视野
|
|
|
|
|
|
*
|
|
|
|
|
|
* 只在"用户已经滑过容器顶部"时才滚 —— 否则用户本来就在看列表,
|
|
|
|
|
|
* 无端滚动等于把阅读位置丢掉。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {HTMLElement} root
|
|
|
|
|
|
*/
|
|
|
|
|
|
function scrollToRoot(root) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
var rect = root.getBoundingClientRect();
|
|
|
|
|
|
var headRoom = 60; // 预留一点顶部空间,别贴着视口边缘
|
|
|
|
|
|
if (rect.top < headRoom) {
|
|
|
|
|
|
var reduce = window.matchMedia
|
|
|
|
|
|
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
|
|
root.scrollIntoView({
|
|
|
|
|
|
behavior: reduce ? 'auto' : 'smooth',
|
|
|
|
|
|
block: 'start'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
/* getBoundingClientRect / scrollIntoView 在极老浏览器上可能没有,
|
|
|
|
|
|
* 或元素已从文档里移除。滚不动不影响功能,静默即可。 */
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初始化一个容器
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {HTMLElement} root
|
|
|
|
|
|
*/
|
|
|
|
|
|
function initRoot(root) {
|
|
|
|
|
|
// 幂等:同一个容器被初始化两次会导致一次点击切两年
|
|
|
|
|
|
if (root.getAttribute('data-ar-ready') === '1') {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var data = readData(root);
|
|
|
|
|
|
if (!data || !data.order || !data.order.length) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
root.setAttribute('data-ar-ready', '1');
|
|
|
|
|
|
|
|
|
|
|
|
/* 事件委托:click 挂在容器上。
|
|
|
|
|
|
* 只接管"左键 + 无修饰键 + 落在 .ar-nav-btn 上"的点击,
|
|
|
|
|
|
* 其余(中键、Ctrl/Cmd/Shift + 点击)交给浏览器默认行为 ——
|
|
|
|
|
|
* 这样"新标签打开某一年"照常可用。 */
|
|
|
|
|
|
root.addEventListener('click', function (ev) {
|
|
|
|
|
|
if (ev.defaultPrevented || ev.button !== 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var btn = ev.target && ev.target.closest
|
|
|
|
|
|
? ev.target.closest('.ar-nav-btn')
|
|
|
|
|
|
: null;
|
|
|
|
|
|
if (!btn || !root.contains(btn)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (btn.classList && btn.classList.contains('is-disabled')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var target = parseInt(btn.getAttribute('data-year'), 10);
|
|
|
|
|
|
if (!target || target === data.current) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 接管:不刷新页面
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
switchTo(root, data, target, true);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 启动
|
|
|
|
|
|
*
|
|
|
|
|
|
* 脚本在页脚输出(`in_footer = true`),正常此时 DOM 已就绪;
|
|
|
|
|
|
* 但仍然判一次 readyState —— 万一被别的插件提前加载,
|
|
|
|
|
|
* 直接 `querySelectorAll` 会拿到空集,页面就永久不工作了。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function boot() {
|
|
|
|
|
|
var roots = document.querySelectorAll(ROOT_SEL);
|
|
|
|
|
|
for (var i = 0; i < roots.length; i++) {
|
|
|
|
|
|
initRoot(roots[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', boot);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
boot();
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|