hexo-theme-stellar 1.35.0 → 1.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/_config.yml +25 -10
  2. package/layout/_partial/comments/artalk/layout.ejs +2 -2
  3. package/layout/_partial/comments/artalk/script.ejs +3 -9
  4. package/layout/_partial/comments/beaudar/script.ejs +2 -8
  5. package/layout/_partial/comments/giscus/script.ejs +2 -8
  6. package/layout/_partial/comments/twikoo/layout.ejs +2 -2
  7. package/layout/_partial/comments/twikoo/script.ejs +3 -9
  8. package/layout/_partial/comments/utterances/script.ejs +2 -8
  9. package/layout/_partial/comments/waline/layout.ejs +1 -1
  10. package/layout/_partial/comments/waline/script.ejs +1 -7
  11. package/layout/_partial/scripts/defines.ejs +118 -1
  12. package/layout/_partial/scripts/lazyload.ejs +7 -6
  13. package/layout/_partial/scripts/services.ejs +11 -13
  14. package/layout/_partial/scripts/utils.ejs +359 -24
  15. package/layout/_partial/widgets/tagtree.ejs +1 -1
  16. package/layout/_partial/widgets/toc.ejs +1 -4
  17. package/layout/_partial/widgets/tree.ejs +1 -1
  18. package/layout/_plugins/fancybox.ejs +23 -8
  19. package/layout/_plugins/scrollreveal.ejs +77 -13
  20. package/package.json +1 -1
  21. package/scripts/filters/lib/img_lazyload.js +9 -9
  22. package/scripts/tags/lib/emoji.js +17 -2
  23. package/scripts/tags/lib/image.js +13 -18
  24. package/source/css/_common/html.styl +0 -2
  25. package/source/css/_components/widgets/toc.styl +2 -2
  26. package/source/css/_plugins/lazyload.styl +40 -26
  27. package/source/css/_plugins/scrollreveal.styl +5 -1
  28. package/source/js/main.js +157 -69
  29. package/source/js/search/algolia-search.js +62 -62
  30. package/source/js/search/local-search.js +37 -36
  31. package/source/js/services/artalk_latest_comment.js +5 -7
  32. package/source/js/services/contributors.js +4 -6
  33. package/source/js/services/fcircle.js +4 -6
  34. package/source/js/services/friends.js +4 -6
  35. package/source/js/services/friends_and_posts.js +4 -6
  36. package/source/js/services/ghinfo.js +6 -8
  37. package/source/js/services/giscus_latest_comment.js +4 -6
  38. package/source/js/services/mdrender.js +2 -2
  39. package/source/js/services/memos.js +3 -4
  40. package/source/js/services/rss.js +10 -12
  41. package/source/js/services/siteinfo.js +24 -26
  42. package/source/js/services/sites.js +4 -6
  43. package/source/js/services/timeline.js +4 -6
  44. package/source/js/services/twikoo_latest_comment.js +5 -7
  45. package/source/js/services/waline_latest_comment.js +4 -6
  46. package/source/js/services/weibo.js +4 -6
@@ -113,7 +113,7 @@
113
113
  // 已加载脚本缓存
114
114
  _loadedScripts: new Set(),
115
115
 
116
- // 已加载元素缓存 (使用 WeakSet 追踪元素实例,避免 PJAX 导航时的状态混淆)
116
+ // 已加载元素缓存 (使用 WeakSet 追踪元素实例,避免重复加载)
117
117
  _loadedElements: new WeakSet(),
118
118
 
119
119
  js: (src, opt) => new Promise((resolve, reject) => {
@@ -146,54 +146,355 @@
146
146
  document.head.appendChild(script)
147
147
  }),
148
148
 
149
- jq: (fn) => {
150
- if (typeof jQuery === 'undefined') {
151
- utils.js(deps.jquery).then(fn)
152
- } else {
153
- fn()
149
+ // 原生 DOM 工具:querySelector / querySelectorAll 简写
150
+ qs: (sel, ctx) => (ctx || document).querySelector(sel),
151
+ qsa: (sel, ctx) => Array.prototype.slice.call((ctx || document).querySelectorAll(sel)),
152
+
153
+ // 原生 DOM 封装:常用 DOM 操作方法子集(find/append/class/attr/事件等)
154
+ dom: (selector, ctx) => {
155
+ var els = [];
156
+ if (typeof selector === 'string') {
157
+ els = utils.qsa(selector, ctx);
158
+ } else if (selector && selector.nodeType === 1) {
159
+ els = [selector];
160
+ } else if (selector && typeof selector.length === 'number') {
161
+ els = Array.prototype.slice.call(selector);
154
162
  }
163
+ var api = {
164
+ length: els.length,
165
+ each: function (fn) {
166
+ els.forEach(function (el, i) {
167
+ fn.call(el, i, el);
168
+ });
169
+ return api;
170
+ },
171
+ find: function (sel) {
172
+ var result = [];
173
+ els.forEach(function (el) {
174
+ Array.prototype.push.apply(result, el.querySelectorAll(sel));
175
+ });
176
+ return utils.dom(result);
177
+ },
178
+ append: function (content) {
179
+ els.forEach(function (el) {
180
+ if (typeof content === 'string') {
181
+ el.insertAdjacentHTML('beforeend', content);
182
+ } else if (content && content.nodeType === 1) {
183
+ el.appendChild(content);
184
+ } else if (content && typeof content.length === 'number') {
185
+ Array.prototype.forEach.call(content, function (child) {
186
+ if (child && child.nodeType === 1) el.appendChild(child);
187
+ });
188
+ }
189
+ });
190
+ return api;
191
+ },
192
+ remove: function () {
193
+ els.forEach(function (el) {
194
+ el.remove();
195
+ });
196
+ },
197
+ addClass: function (cls) {
198
+ els.forEach(function (el) {
199
+ cls.trim().split(/\s+/).forEach(function (c) {
200
+ if (c) el.classList.add(c);
201
+ });
202
+ });
203
+ return api;
204
+ },
205
+ removeClass: function (cls) {
206
+ els.forEach(function (el) {
207
+ cls.trim().split(/\s+/).forEach(function (c) {
208
+ if (c) el.classList.remove(c);
209
+ });
210
+ });
211
+ return api;
212
+ },
213
+ toggleClass: function (cls, force) {
214
+ els.forEach(function (el) {
215
+ cls.trim().split(/\s+/).forEach(function (c) {
216
+ if (c) el.classList.toggle(c, force);
217
+ });
218
+ });
219
+ return api;
220
+ },
221
+ attr: function (name, value) {
222
+ if (value === undefined) {
223
+ return els[0] ? els[0].getAttribute(name) : undefined;
224
+ }
225
+ els.forEach(function (el) {
226
+ el.setAttribute(name, value);
227
+ });
228
+ return api;
229
+ },
230
+ data: function (name) {
231
+ return els[0] ? els[0].getAttribute('data-' + name) : undefined;
232
+ },
233
+ text: function (value) {
234
+ if (value === undefined) {
235
+ return els[0] ? els[0].textContent : undefined;
236
+ }
237
+ els.forEach(function (el) {
238
+ el.textContent = value;
239
+ });
240
+ return api;
241
+ },
242
+ html: function (content) {
243
+ if (content === undefined) {
244
+ return els[0] ? els[0].innerHTML : undefined;
245
+ }
246
+ els.forEach(function (el) {
247
+ if (typeof content !== 'string' && content && typeof content.length === 'number' && content[0] && content[0].nodeType === 1) {
248
+ var frag = document.createDocumentFragment();
249
+ Array.prototype.forEach.call(content, function (child) {
250
+ frag.appendChild(child);
251
+ });
252
+ el.replaceChildren(frag);
253
+ } else {
254
+ el.innerHTML = content;
255
+ }
256
+ });
257
+ return api;
258
+ },
259
+ val: function (value) {
260
+ if (value === undefined) {
261
+ return els[0] ? els[0].value : undefined;
262
+ }
263
+ els.forEach(function (el) {
264
+ el.value = value;
265
+ });
266
+ return api;
267
+ },
268
+ offset: function () {
269
+ var el = els[0];
270
+ if (!el) return { top: 0, left: 0 };
271
+ var rect = el.getBoundingClientRect();
272
+ return { top: rect.top + window.scrollY, left: rect.left + window.scrollX };
273
+ },
274
+ on: function (event, cb) {
275
+ els.forEach(function (el) {
276
+ el.addEventListener(event, cb);
277
+ });
278
+ return api;
279
+ },
280
+ click: function (cb) { return api.on('click', cb); },
281
+ focus: function (cb) { return api.on('focus', cb); },
282
+ keydown: function (cb) { return api.on('keydown', cb); },
283
+ empty: function () {
284
+ els.forEach(function (el) {
285
+ el.replaceChildren();
286
+ });
287
+ return api;
288
+ }
289
+ };
290
+ els.forEach(function (el, i) {
291
+ api[i] = el;
292
+ });
293
+ return api;
155
294
  },
156
295
 
157
296
  onLoading: (el) => {
158
297
  if (el) {
159
- if ($(el).find('.loading-wrap').length === 0){
160
- $(el).append('<div class="loading-wrap"><svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg></div>');
298
+ if (el.querySelector('.loading-wrap') === null) {
299
+ el.insertAdjacentHTML('beforeend', '<div class="loading-wrap"><svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg></div>');
161
300
  }
162
301
  }
163
302
  },
164
303
  onLoadSuccess: (el) => {
165
304
  if (el) {
166
- $(el).find('.loading-wrap').remove();
305
+ var wrap = el.querySelector('.loading-wrap');
306
+ if (wrap) wrap.remove();
167
307
  }
168
308
  },
169
309
  onLoadFailure: (el) => {
170
310
  if (el) {
171
- $(el).find('.loading-wrap svg').remove();
172
- $(el).find('.loading-wrap').append('<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" d="M12 3L21 20H3L12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.5s" values="60;0"/></path><path stroke-dasharray="6" stroke-dashoffset="6" d="M12 10V14"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.2s" values="6;0"/></path></g><circle cx="12" cy="17" r="1" fill="currentColor" fill-opacity="0"><animate fill="freeze" attributeName="fill-opacity" begin="0.8s" dur="0.4s" values="0;1"/></circle></svg>');
173
- $(el).find('.loading-wrap').addClass('error');
311
+ var wrap = el.querySelector('.loading-wrap');
312
+ if (wrap) {
313
+ var svg = wrap.querySelector('svg');
314
+ if (svg) svg.remove();
315
+ wrap.insertAdjacentHTML('beforeend', '<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" d="M12 3L21 20H3L12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.5s" values="60;0"/></path><path stroke-dasharray="6" stroke-dashoffset="6" d="M12 10V14"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.2s" values="6;0"/></path></g><circle cx="12" cy="17" r="1" fill="currentColor" fill-opacity="0"><animate fill="freeze" attributeName="fill-opacity" begin="0.8s" dur="0.4s" values="0;1"/></circle></svg>');
316
+ wrap.classList.add('error');
317
+ }
174
318
  }
175
319
  },
176
- request: (el, url, callback, onFailure) => {
320
+ /********************** data cache ********************************/
321
+ // 动态数据本地缓存:TTL 未过期直接命中;过期后先显示缓存再后台刷新(stale-while-revalidate)
322
+ _cacheEnabled: true,
323
+ _cacheMaxEntryBytes: 200 * 1024,
324
+ cache: {
325
+ prefix: 'Stellar.data_cache.v1.',
326
+ enabled: () => !!def.data_cache?.enable && utils._cacheEnabled,
327
+ serviceId: (el, options) => {
328
+ if (options && options.service) return options.service;
329
+ if (el) {
330
+ const match = String(el.className || '').match(/\bds-([\w-]+)\b/);
331
+ if (match) return match[1];
332
+ }
333
+ return null;
334
+ },
335
+ ttl: (el, options) => {
336
+ const conf = def.data_cache || {};
337
+ const id = utils.cache.serviceId(el, options);
338
+ const value = id && conf.ttl && conf.ttl[id] != null ? conf.ttl[id] : conf.default_ttl;
339
+ return typeof value === 'number' && value > 0 ? value : 0;
340
+ },
341
+ shouldCache: (url, options) => {
342
+ if (!utils.cache.enabled()) return false;
343
+ if (options && (options.cache === false || options.cache === 'no-store')) return false;
344
+ if (options && options.method && options.method !== 'GET') return false;
345
+ // 带时间戳缓存破坏参数的请求不缓存(如 mdrender 的 ?t=)
346
+ if (/[?&]t=\d{10,}/.test(url)) return false;
347
+ return true;
348
+ },
349
+ get: (url) => {
350
+ try {
351
+ const raw = localStorage.getItem(utils.cache.prefix + url);
352
+ if (!raw) return null;
353
+ const entry = JSON.parse(raw);
354
+ if (!entry || typeof entry.text !== 'string' || typeof entry.ts !== 'number') return null;
355
+ return entry;
356
+ } catch (e) {
357
+ console.warn('[cache] 读取失败:', url, e);
358
+ return null;
359
+ }
360
+ },
361
+ set: (url, text, contentType, ttl) => {
362
+ if (!(ttl > 0)) return;
363
+ if (text.length > utils._cacheMaxEntryBytes) return;
364
+ const entry = JSON.stringify({
365
+ text: text,
366
+ contentType: contentType || 'application/json',
367
+ ts: Date.now(),
368
+ ttl: ttl
369
+ });
370
+ const trySet = () => {
371
+ try {
372
+ localStorage.setItem(utils.cache.prefix + url, entry);
373
+ utils.cache.trim();
374
+ return true;
375
+ } catch (e) {
376
+ return false;
377
+ }
378
+ };
379
+ if (!trySet()) {
380
+ // 配额不足时淘汰最旧条目后重试一次,仍失败则本会话禁用缓存
381
+ utils.cache.evictOldest(url);
382
+ if (!trySet()) {
383
+ console.warn('[cache] 写入失败,本会话禁用缓存:', url);
384
+ utils._cacheEnabled = false;
385
+ }
386
+ }
387
+ },
388
+ trim: () => {
389
+ try {
390
+ const max = (def.data_cache && def.data_cache.max_entries) || 200;
391
+ const keys = [];
392
+ for (let i = 0; i < localStorage.length; i++) {
393
+ const key = localStorage.key(i);
394
+ if (key && key.indexOf(utils.cache.prefix) === 0) keys.push(key);
395
+ }
396
+ if (keys.length <= max) return;
397
+ const sorted = keys.map((key) => {
398
+ let ts = 0;
399
+ try {
400
+ const entry = JSON.parse(localStorage.getItem(key) || 'null');
401
+ if (entry && typeof entry.ts === 'number') ts = entry.ts;
402
+ } catch (e) {}
403
+ return { key, ts };
404
+ }).sort((a, b) => a.ts - b.ts);
405
+ for (let i = 0; i < sorted.length - max; i++) {
406
+ localStorage.removeItem(sorted[i].key);
407
+ }
408
+ } catch (e) {
409
+ console.warn('[cache] 清理失败:', e);
410
+ }
411
+ },
412
+ evictOldest: (exceptUrl) => {
413
+ try {
414
+ let oldestKey = null;
415
+ let oldestTs = Infinity;
416
+ const exceptKey = utils.cache.prefix + exceptUrl;
417
+ for (let i = 0; i < localStorage.length; i++) {
418
+ const key = localStorage.key(i);
419
+ if (!key || key.indexOf(utils.cache.prefix) !== 0 || key === exceptKey) continue;
420
+ let ts = 0;
421
+ try {
422
+ const entry = JSON.parse(localStorage.getItem(key) || 'null');
423
+ if (entry && typeof entry.ts === 'number') ts = entry.ts;
424
+ } catch (e) {}
425
+ if (ts < oldestTs) {
426
+ oldestTs = ts;
427
+ oldestKey = key;
428
+ }
429
+ }
430
+ if (oldestKey) localStorage.removeItem(oldestKey);
431
+ } catch (e) {}
432
+ },
433
+ isFresh: (entry) => {
434
+ if (!entry || typeof entry.ts !== 'number' || typeof entry.ttl !== 'number') return false;
435
+ if (!(entry.ttl > 0)) return false;
436
+ return Date.now() - entry.ts < entry.ttl * 1000;
437
+ },
438
+ // 用缓存文本重建 Response,兼容回调中的 resp.json() / resp.text()
439
+ toResponse: (entry) => new Response(entry.text, {
440
+ status: 200,
441
+ statusText: 'OK',
442
+ headers: { 'Content-Type': entry.contentType }
443
+ })
444
+ },
445
+ request: (el, url, callback, onFailure, options) => {
177
446
  // 检查元素实例是否已加载 (而不是检查属性)
178
447
  if (el && utils._loadedElements.has(el)) {
179
448
  return;
180
449
  }
181
450
  const maxRetry = 3;
182
451
  let retryCount = 0;
452
+ const ttl = utils.cache.ttl(el, options);
453
+ const cacheable = utils.cache.shouldCache(url, options) && ttl > 0;
454
+ const cached = cacheable ? utils.cache.get(url) : null;
455
+ // 缓存渲染前的初始结构,后台刷新渲染前恢复,避免重复渲染
456
+ let initialHTML = null;
457
+ // 缓存渲染完成后(或超时兜底)再启动后台刷新,避免异步渲染未结束就清空重绘
458
+ let cacheRenderDone = null;
459
+
460
+ if (cached) {
461
+ if (el) {
462
+ // 已有缓存时不显示加载动画
463
+ utils.onLoadSuccess?.(el);
464
+ initialHTML = el.innerHTML;
465
+ }
466
+ try {
467
+ cacheRenderDone = Promise.resolve(callback(utils.cache.toResponse(cached))).catch(e => {
468
+ console.warn('[request] 缓存渲染失败:', url, e);
469
+ });
470
+ } catch (e) {
471
+ console.warn('[request] 缓存渲染失败:', url, e);
472
+ }
473
+ // 缓存未过期:直接完成,不发请求
474
+ if (utils.cache.isFresh(cached)) {
475
+ if (el) utils._loadedElements.add(el);
476
+ return Promise.resolve(cached.text);
477
+ }
478
+ } else {
479
+ utils.onLoading?.(el);
480
+ }
183
481
 
184
482
  return new Promise((resolve, reject) => {
185
483
  const load = () => {
186
- utils.onLoading?.(el);
187
-
188
484
  let timedOut = false;
189
485
  const timeout = setTimeout(() => {
190
486
  timedOut = true;
191
487
  console.warn('[request] 超时:', url);
192
488
 
193
489
  if (++retryCount >= maxRetry) {
194
- utils.onLoadFailure?.(el);
195
- onFailure?.();
196
- reject('请求超时');
490
+ if (cached) {
491
+ // 已有缓存渲染,保留缓存内容,不显示失败
492
+ resolve(cached.text);
493
+ } else {
494
+ utils.onLoadFailure?.(el);
495
+ onFailure?.();
496
+ reject('请求超时');
497
+ }
197
498
  } else {
198
499
  setTimeout(load, 1000);
199
500
  }
@@ -209,6 +510,16 @@
209
510
  if (timedOut) return;
210
511
  // 标记元素实例为已加载
211
512
  if (el) utils._loadedElements.add(el);
513
+ // 写入缓存(克隆响应,避免影响回调读取)
514
+ if (cacheable && data.ok) {
515
+ data.clone().text().then(text => {
516
+ utils.cache.set(url, text, data.headers.get('Content-Type') || 'application/json', ttl);
517
+ }).catch(() => {});
518
+ }
519
+ // 后台刷新渲染前恢复初始结构,避免缓存渲染的内容重复
520
+ if (el && initialHTML !== null) {
521
+ el.innerHTML = initialHTML;
522
+ }
212
523
  utils.onLoadSuccess?.(el);
213
524
  callback(data);
214
525
  resolve(data);
@@ -217,19 +528,38 @@
217
528
  console.warn('[request] 错误:', err);
218
529
 
219
530
  if (++retryCount >= maxRetry) {
220
- utils.onLoadFailure?.(el);
221
- onFailure?.();
222
- reject(err);
531
+ if (cached) {
532
+ resolve(cached.text);
533
+ } else {
534
+ utils.onLoadFailure?.(el);
535
+ onFailure?.();
536
+ reject(err);
537
+ }
223
538
  } else {
224
539
  setTimeout(load, 1000);
225
540
  }
226
541
  });
227
542
  };
228
543
 
229
- load();
544
+ if (cacheRenderDone) {
545
+ Promise.race([
546
+ cacheRenderDone,
547
+ new Promise(resolve => setTimeout(resolve, 5000))
548
+ ]).then(load);
549
+ } else {
550
+ load();
551
+ }
230
552
  });
231
553
  },
232
554
  requestWithoutLoading: (url, options = {}, maxRetry = 2, timeout = 5000) => {
555
+ const ttl = utils.cache.ttl(null, options);
556
+ const cacheable = utils.cache.shouldCache(url, options) && ttl > 0;
557
+ const cached = cacheable ? utils.cache.get(url) : null;
558
+
559
+ if (cached && utils.cache.isFresh(cached)) {
560
+ return Promise.resolve(utils.cache.toResponse(cached));
561
+ }
562
+
233
563
  return new Promise((resolve, reject) => {
234
564
  let retryCount = 0;
235
565
 
@@ -245,6 +575,11 @@
245
575
  .then(resp => {
246
576
  clearTimeout(timer);
247
577
  if (!resp.ok) throw new Error('bad response');
578
+ if (cacheable) {
579
+ resp.clone().text().then(text => {
580
+ utils.cache.set(url, text, resp.headers.get('Content-Type') || 'application/json', ttl);
581
+ }).catch(() => {});
582
+ }
248
583
  resolve(resp);
249
584
  })
250
585
  .catch(err => {
@@ -268,7 +603,7 @@
268
603
  },
269
604
  dark: {},
270
605
 
271
- // 插件初始化管理器 - 统一处理 DOMContentLoaded 和 pjax:complete 事件
606
+ // 插件初始化管理器 - 统一处理 DOMContentLoaded 事件
272
607
  _pluginInitializers: [],
273
608
  _pluginCleanups: new Map(), // 存储每个插件的清理函数
274
609
 
@@ -344,4 +679,4 @@
344
679
  utils.dark = Object.assign(utils.dark, {
345
680
  push: utils.dark.method.toggle.push,
346
681
  });
347
- </script>
682
+ </script>
@@ -45,7 +45,7 @@
45
45
  <% } %>
46
46
 
47
47
  <% if (tagTree) { %>
48
- <widget class="widget-wrapper<%= scrollreveal(' ') %> post-list">
48
+ <widget class="widget-wrapper<%= scrollreveal(' ') %> post-list" data-notebook="<%= page.notebook %>">
49
49
  <div class="widget-header dis-select">
50
50
  <span class="name"><%= __('meta.tag_tree') %></span>
51
51
  </div>
@@ -37,12 +37,9 @@ function layoutToc(fallback) {
37
37
 
38
38
  function layoutDiv(fallback) {
39
39
  const tocBody = layoutTocBody()
40
- if (tocBody.trim().length == 0) {
41
- return ''
42
- }
43
40
  var el = ''
44
41
  el += `<widget class="widget-wrapper${scrollreveal(' ')} toc" id="data-toc" collapse="${item.collapse}">`
45
- if (tocBody.length > 0) {
42
+ if (tocBody.trim().length > 0) {
46
43
  el += layoutTocHeader()
47
44
  el += `<div class="widget-body">`
48
45
  el += tocBody
@@ -60,7 +60,7 @@ function layoutDiv(fallback) {
60
60
  }
61
61
  }
62
62
  if (el.trim().length > 0) {
63
- return `<widget class="widget-wrapper doc-tree post-list">${el}</widget>`
63
+ return `<widget class="widget-wrapper doc-tree post-list" data-wiki="${page.wiki}">${el}</widget>`
64
64
  } else {
65
65
  return ''
66
66
  }
@@ -1,27 +1,42 @@
1
1
  <script>
2
2
  ctx.fancybox = {
3
+ mode: `<%- conf.mode || 'auto' %>`,
3
4
  selector: `<%- conf.selector %>`,
4
5
  css: `<%- conf.css %>`,
5
6
  js: `<%- conf.js %>`
6
7
  };
7
- var selector = '[data-fancybox]:not(.error), .with-fancybox .atk-content img:not([atk-emoticon])';
8
+ // auto=按需:页面存在可弹窗图片才加载;global=全局:所有页面加载
9
+ const isGlobal = ctx.fancybox.mode === 'global';
10
+ // 评论区图片(artalk/twikoo/waline)显示较小,自动支持放大
11
+ var selector = '[data-fancybox]:not(.error), .with-fancybox img:not([atk-emoticon]):not([class*="emo"])';
8
12
  if (ctx.fancybox.selector) {
9
13
  selector += `, ${ctx.fancybox.selector}`
10
14
  }
11
- var needFancybox = document.querySelectorAll(selector).length !== 0;
15
+ var needFancybox = isGlobal;
12
16
  if (!needFancybox) {
13
- const memos = document.getElementsByClassName('ds-memos');
14
- if (memos != undefined && memos.length > 0) {
15
- needFancybox = true;
17
+ needFancybox = document.querySelectorAll(selector).length !== 0;
18
+ if (!needFancybox) {
19
+ // 图片型评论系统容器:评论区图片小图需要 fancybox
20
+ const withFancybox = document.getElementsByClassName('with-fancybox');
21
+ if (withFancybox != undefined && withFancybox.length > 0) {
22
+ needFancybox = true;
23
+ }
16
24
  }
17
- const fancybox = document.getElementsByClassName('with-fancybox');
18
- if (fancybox != undefined && fancybox.length > 0) {
19
- needFancybox = true;
25
+ if (!needFancybox) {
26
+ // 动态数据服务:memos 渲染的图片需要 fancybox
27
+ const memos = document.getElementsByClassName('ds-memos');
28
+ if (memos != undefined && memos.length > 0) {
29
+ needFancybox = true;
30
+ }
20
31
  }
21
32
  }
22
33
  if (needFancybox) {
23
34
  utils.css(ctx.fancybox.css);
24
35
  utils.js(ctx.fancybox.js, { defer: true }).then(function () {
36
+ if (isGlobal) {
37
+ // 全局模式:正文所有非内联图片均可弹窗
38
+ selector += ', .md-text img:not(.inline):not([atk-emoticon])';
39
+ }
25
40
  Fancybox.bind(selector, {
26
41
  hideScrollbar: false,
27
42
  Thumbs: {
@@ -1,17 +1,81 @@
1
- <script defer src="<%- conf.js %>"></script>
2
1
  <script>
3
2
  utils.initPlugin(() => {
4
- const els = document.querySelectorAll('.slide-up');
5
- if (els.length > 0) {
6
- const slideUp = {
7
- distance: `<%- conf.distance %>`,
8
- duration: `<%- conf.duration %>`,
9
- interval: `<%- conf.interval %>`,
10
- scale: `<%- conf.scale %>`,
11
- opacity: 0,
12
- easing: "ease-out"
13
- };
14
- ScrollReveal().reveal('.slide-up', { ...slideUp });
15
- }
3
+ const els = Array.prototype.slice.call(document.querySelectorAll('.slide-up'));
4
+ if (els.length === 0) return;
5
+
6
+ // 吸顶/固定定位容器内的元素始终在视口内(或由容器控制显隐),
7
+ // ScrollReveal 按文档坐标判断可见性,会误判这类元素为不可见,
8
+ // 导致带锚点/恢复滚动位置打开页面时组件一直不显示。
9
+ // 解决方式:以最近的吸顶/固定祖先作为该组元素的容器,
10
+ // 使可见性判断恒成立,入场动画保留且不再依赖主文档滚动。
11
+ const findPinnedContainer = (el) => {
12
+ let node = el;
13
+ while (node && node.nodeType === 1) {
14
+ const pos = window.getComputedStyle(node).position;
15
+ if (pos === 'sticky' || pos === 'fixed') return node;
16
+ node = node.parentElement;
17
+ }
18
+ return null;
19
+ };
20
+
21
+ const pinnedGroups = new Map(); // 容器 -> 元素列表
22
+ const targets = [];
23
+ els.forEach((el) => {
24
+ const container = findPinnedContainer(el);
25
+ if (container) {
26
+ const group = pinnedGroups.get(container) || [];
27
+ group.push(el);
28
+ pinnedGroups.set(container, group);
29
+ } else {
30
+ targets.push(el);
31
+ }
32
+ });
33
+ if (pinnedGroups.size === 0 && targets.length === 0) return;
34
+
35
+ // 加载/初始化失败时兜底显示内容,避免整页空白
36
+ let fallbackApplied = false;
37
+ const revealFallback = () => {
38
+ if (fallbackApplied) return;
39
+ fallbackApplied = true;
40
+ document.documentElement.classList.add('sr-fallback');
41
+ };
42
+
43
+ const slideUp = {
44
+ distance: `<%- conf.distance %>`,
45
+ duration: `<%- conf.duration %>`,
46
+ interval: `<%- conf.interval %>`,
47
+ scale: `<%- conf.scale %>`,
48
+ opacity: 0,
49
+ easing: "ease-out"
50
+ };
51
+
52
+ // 看门狗:CDN 长时间无响应时强制显示内容
53
+ const watchdog = setTimeout(() => revealFallback(), 3000);
54
+
55
+ utils.js(`<%- conf.js %>`, { defer: true })
56
+ .then(() => {
57
+ if (fallbackApplied) return;
58
+ if (typeof ScrollReveal !== 'function') {
59
+ revealFallback();
60
+ return;
61
+ }
62
+ try {
63
+ const sr = ScrollReveal();
64
+ if (targets.length > 0) {
65
+ sr.reveal(targets, { ...slideUp });
66
+ }
67
+ pinnedGroups.forEach((group, container) => {
68
+ sr.reveal(group, { ...slideUp, container: container });
69
+ });
70
+ clearTimeout(watchdog);
71
+ } catch (err) {
72
+ console.error('[Plugin scrollreveal] 初始化失败:', err);
73
+ revealFallback();
74
+ }
75
+ })
76
+ .catch((err) => {
77
+ console.error('[Plugin scrollreveal] 加载失败:', err);
78
+ revealFallback();
79
+ });
16
80
  }, 'scrollreveal');
17
81
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexo-theme-stellar",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "Elegant and powerful theme for Hexo.",
5
5
  "main": "package.json",
6
6
  "scripts": {