hexo-theme-linen 1.0.27 → 1.0.29
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.
- package/package.json +1 -1
- package/scripts/lazyload/inject.js +5 -3
- package/source/linen-theme/css/_mediaquery.scss +1 -3
- package/source/linen-theme/css/linen.scss +11 -2
- package/source/linen-theme/js/hdr.js +2 -2
- package/source/linen-theme/js/linen.js +95 -44
- package/source/linen-theme/js/replace-emoji.js +11 -3
- package/source/linen-theme/js/toc.js +191 -161
package/package.json
CHANGED
|
@@ -19,9 +19,11 @@ module.exports = function (hexo) {
|
|
|
19
19
|
...(hexo?.config?.theme_config?.lazyload || {}),
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const placeholderImage =
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const placeholderImage =
|
|
23
|
+
/^(https?:\/\/|\/|data:image|\.\.\/)/i.test(loadingImage) ||
|
|
24
|
+
/\.(svg|png|jpg|jpeg|webp|gif)$/i.test(loadingImage)
|
|
25
|
+
? `url(${loadingImage})`
|
|
26
|
+
: loadingImage;
|
|
25
27
|
|
|
26
28
|
const injectedStyles = `.lazyload-outer-wrap .caption{
|
|
27
29
|
width: 100%;
|
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
}
|
|
11
11
|
.nav-header.is-post {
|
|
12
12
|
position: fixed;
|
|
13
|
-
.header-inner {
|
|
14
|
-
padding: 0 $commonPadding;
|
|
15
|
-
}
|
|
16
13
|
}
|
|
17
14
|
.nav-header.is-post.hide {
|
|
18
15
|
transform: translateY(-100%);
|
|
@@ -42,6 +39,7 @@
|
|
|
42
39
|
border-right: none;
|
|
43
40
|
-ms-overflow-style: unset;
|
|
44
41
|
scrollbar-width: unset;
|
|
42
|
+
overscroll-behavior: contain;
|
|
45
43
|
@include safe-padding(15px);
|
|
46
44
|
.toc-item-wrap {
|
|
47
45
|
transition: unset;
|
|
@@ -41,6 +41,9 @@ html {
|
|
|
41
41
|
min-width: 320px;
|
|
42
42
|
scroll-behavior: smooth;
|
|
43
43
|
overscroll-behavior: auto none;
|
|
44
|
+
&.no-scroll {
|
|
45
|
+
overflow: hidden !important;
|
|
46
|
+
}
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
* {
|
|
@@ -217,6 +220,9 @@ body {
|
|
|
217
220
|
display: block;
|
|
218
221
|
}
|
|
219
222
|
}
|
|
223
|
+
&.no-scroll {
|
|
224
|
+
overflow: hidden !important;
|
|
225
|
+
}
|
|
220
226
|
}
|
|
221
227
|
* {
|
|
222
228
|
box-sizing: border-box;
|
|
@@ -233,7 +239,8 @@ body {
|
|
|
233
239
|
background-color: #fff;
|
|
234
240
|
// backdrop-filter: saturate(180%) blur(20px);
|
|
235
241
|
transform: translateY(0);
|
|
236
|
-
transition: transform 0.
|
|
242
|
+
transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
|
|
243
|
+
will-change: transform;
|
|
237
244
|
z-index: 9;
|
|
238
245
|
position: relative;
|
|
239
246
|
top: 0;
|
|
@@ -362,7 +369,7 @@ body {
|
|
|
362
369
|
}
|
|
363
370
|
}
|
|
364
371
|
&.hide {
|
|
365
|
-
transform: translateY(
|
|
372
|
+
transform: translateY(-100%);
|
|
366
373
|
}
|
|
367
374
|
}
|
|
368
375
|
|
|
@@ -1830,6 +1837,7 @@ body {
|
|
|
1830
1837
|
transition: transform cubic-bezier(0.4, 0, 0.6, 1) 0.3s;
|
|
1831
1838
|
display: flex;
|
|
1832
1839
|
flex-direction: column;
|
|
1840
|
+
overscroll-behavior: contain;
|
|
1833
1841
|
@include safe-padding(15px);
|
|
1834
1842
|
&.visible {
|
|
1835
1843
|
transform: translateY(0);
|
|
@@ -2594,6 +2602,7 @@ body {
|
|
|
2594
2602
|
&.visible {
|
|
2595
2603
|
pointer-events: all;
|
|
2596
2604
|
opacity: 1;
|
|
2605
|
+
touch-action: none;
|
|
2597
2606
|
}
|
|
2598
2607
|
}
|
|
2599
2608
|
|
|
@@ -52,7 +52,7 @@ function switchHDR() {
|
|
|
52
52
|
} else {
|
|
53
53
|
var hrefAttr = link.getAttribute("href");
|
|
54
54
|
if (hrefAttr && hrefAttr.includes(SDRPath)) {
|
|
55
|
-
link.setAttribute("href", HDRPath);
|
|
55
|
+
link.setAttribute("href", hrefAttr.replace(SDRPath, HDRPath));
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
} else {
|
|
@@ -61,7 +61,7 @@ function switchHDR() {
|
|
|
61
61
|
} else {
|
|
62
62
|
var hrefAttr = link.getAttribute("href");
|
|
63
63
|
if (hrefAttr && hrefAttr.includes(HDRPath)) {
|
|
64
|
-
link.setAttribute("href", SDRPath);
|
|
64
|
+
link.setAttribute("href", hrefAttr.replace(HDRPath, SDRPath));
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -147,30 +147,27 @@ function isElementInViewport(el) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
function copyToClipboard(text) {
|
|
150
|
-
if (navigator.clipboard) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.catch((err) => {
|
|
156
|
-
console.error("复制失败:", err);
|
|
157
|
-
});
|
|
158
|
-
};
|
|
159
|
-
} else {
|
|
160
|
-
copyToClipboard = function () {
|
|
161
|
-
const textArea = document.createElement("textarea");
|
|
162
|
-
textArea.value = text;
|
|
163
|
-
document.body.appendChild(textArea);
|
|
164
|
-
textArea.select();
|
|
165
|
-
try {
|
|
166
|
-
document.execCommand("copy");
|
|
167
|
-
} catch (err) {
|
|
150
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
151
|
+
navigator.clipboard
|
|
152
|
+
.writeText(text)
|
|
153
|
+
.then(() => {})
|
|
154
|
+
.catch((err) => {
|
|
168
155
|
console.error("复制失败:", err);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
const textArea = document.createElement("textarea");
|
|
159
|
+
textArea.value = text;
|
|
160
|
+
textArea.style.position = "fixed";
|
|
161
|
+
textArea.style.opacity = "0";
|
|
162
|
+
document.body.appendChild(textArea);
|
|
163
|
+
textArea.select();
|
|
164
|
+
try {
|
|
165
|
+
document.execCommand("copy");
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error("复制失败:", err);
|
|
168
|
+
}
|
|
169
|
+
document.body.removeChild(textArea);
|
|
172
170
|
}
|
|
173
|
-
copyToClipboard(text);
|
|
174
171
|
}
|
|
175
172
|
|
|
176
173
|
function highlightAnchor(hash, highlightClass = "anchor-highlight") {
|
|
@@ -270,7 +267,8 @@ function onScroll() {
|
|
|
270
267
|
var header = document.querySelector(".nav-header");
|
|
271
268
|
if (!header) return;
|
|
272
269
|
|
|
273
|
-
var
|
|
270
|
+
var rawScrollY = window.scrollY;
|
|
271
|
+
var currentScrollY = Math.max(0, rawScrollY);
|
|
274
272
|
var triggerHeight = window.innerHeight * 1.5;
|
|
275
273
|
|
|
276
274
|
if (!window.$gitalkInitiated && currentScrollY > window.innerHeight) {
|
|
@@ -283,27 +281,32 @@ function onScroll() {
|
|
|
283
281
|
return;
|
|
284
282
|
}
|
|
285
283
|
|
|
286
|
-
|
|
284
|
+
// 接近顶部时(<= 60px),强制展开并退出,消除 iOS/Mac 橡皮筋回弹误判与闪烁
|
|
285
|
+
if (currentScrollY <= 60) {
|
|
287
286
|
header.classList.remove("hide");
|
|
287
|
+
if (backToTop) backToTop.classList.remove("visible");
|
|
288
288
|
previousScrollY = currentScrollY;
|
|
289
289
|
return;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
var scrollDelta = currentScrollY - previousScrollY;
|
|
293
|
-
|
|
293
|
+
// 滤除手指微小抖动
|
|
294
|
+
if (Math.abs(scrollDelta) < 8) {
|
|
294
295
|
return;
|
|
295
296
|
}
|
|
296
297
|
|
|
297
298
|
if (scrollDelta < 0) {
|
|
299
|
+
// 向上滑动:即刻平滑展开导航栏
|
|
298
300
|
if (!document.querySelector(".pswp--open")) {
|
|
299
301
|
header.classList.remove("hide");
|
|
300
302
|
}
|
|
301
|
-
if (currentScrollY >= triggerHeight) {
|
|
303
|
+
if (backToTop && currentScrollY >= triggerHeight) {
|
|
302
304
|
backToTop.classList.add("visible");
|
|
303
305
|
}
|
|
304
306
|
} else {
|
|
305
|
-
|
|
306
|
-
if (
|
|
307
|
+
// 向下滑动:离开顶部 60% 视口后平滑收起导航栏
|
|
308
|
+
if (backToTop) backToTop.classList.remove("visible");
|
|
309
|
+
if (currentScrollY > window.innerHeight * 0.6) {
|
|
307
310
|
header.classList.add("hide");
|
|
308
311
|
}
|
|
309
312
|
}
|
|
@@ -311,7 +314,20 @@ function onScroll() {
|
|
|
311
314
|
previousScrollY = currentScrollY;
|
|
312
315
|
}
|
|
313
316
|
|
|
314
|
-
|
|
317
|
+
var isScrollTicking = false;
|
|
318
|
+
window.addEventListener(
|
|
319
|
+
"scroll",
|
|
320
|
+
function () {
|
|
321
|
+
if (!isScrollTicking) {
|
|
322
|
+
isScrollTicking = true;
|
|
323
|
+
window.requestAnimationFrame(function () {
|
|
324
|
+
onScroll();
|
|
325
|
+
isScrollTicking = false;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
{ passive: true }
|
|
330
|
+
);
|
|
315
331
|
|
|
316
332
|
var resizeTimer = null;
|
|
317
333
|
window.addEventListener("resize", function () {
|
|
@@ -339,6 +355,18 @@ function onclickPostItem(element) {
|
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
357
|
|
|
358
|
+
function setBodyScrollLocked(locked) {
|
|
359
|
+
var body = document.body;
|
|
360
|
+
var html = document.documentElement;
|
|
361
|
+
if (locked) {
|
|
362
|
+
body && body.classList.add("no-scroll");
|
|
363
|
+
html && html.classList.add("no-scroll");
|
|
364
|
+
} else {
|
|
365
|
+
body && body.classList.remove("no-scroll");
|
|
366
|
+
html && html.classList.remove("no-scroll");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
342
370
|
function handleClick(e) {
|
|
343
371
|
try {
|
|
344
372
|
var target = e.target;
|
|
@@ -358,7 +386,7 @@ function handleClick(e) {
|
|
|
358
386
|
if (/close-series-popup-icon/.test(target.className)) {
|
|
359
387
|
seriesElement?.classList?.remove("visible");
|
|
360
388
|
maskElement.classList.remove("visible");
|
|
361
|
-
|
|
389
|
+
setBodyScrollLocked(false);
|
|
362
390
|
} else if (["H2", "H3"].includes(target.tagName) && target.id) {
|
|
363
391
|
const newLocation = new URL(location.href);
|
|
364
392
|
newLocation.hash = `#${target.id}`;
|
|
@@ -415,7 +443,7 @@ function handleClick(e) {
|
|
|
415
443
|
if (tocElement?.classList?.contains("visible")) {
|
|
416
444
|
tocElement.classList.remove("visible");
|
|
417
445
|
maskElement.classList.remove("visible");
|
|
418
|
-
|
|
446
|
+
setBodyScrollLocked(false);
|
|
419
447
|
}
|
|
420
448
|
if (targetId === "to-page-top") {
|
|
421
449
|
window.scrollTo({
|
|
@@ -456,11 +484,11 @@ function handleClick(e) {
|
|
|
456
484
|
if (tocElement?.classList?.contains("visible")) {
|
|
457
485
|
tocElement.classList.remove("visible");
|
|
458
486
|
maskElement.classList.remove("visible");
|
|
459
|
-
|
|
487
|
+
setBodyScrollLocked(false);
|
|
460
488
|
} else {
|
|
461
489
|
tocElement.classList.add("visible");
|
|
462
490
|
maskElement.classList.add("visible");
|
|
463
|
-
|
|
491
|
+
setBodyScrollLocked(true);
|
|
464
492
|
}
|
|
465
493
|
return;
|
|
466
494
|
} else if (
|
|
@@ -473,13 +501,13 @@ function handleClick(e) {
|
|
|
473
501
|
seriesElement?.classList?.add("visible");
|
|
474
502
|
maskElement?.classList?.add("visible");
|
|
475
503
|
donateModal?.classList?.remove("visible");
|
|
476
|
-
|
|
504
|
+
setBodyScrollLocked(true);
|
|
477
505
|
return;
|
|
478
506
|
} else if (target?.className?.includes("series-item-link")) {
|
|
479
507
|
e.preventDefault();
|
|
480
508
|
seriesElement?.classList?.remove("visible");
|
|
481
509
|
maskElement.classList.remove("visible");
|
|
482
|
-
|
|
510
|
+
setBodyScrollLocked(false);
|
|
483
511
|
const href =
|
|
484
512
|
target.getAttribute("href") ||
|
|
485
513
|
target.parentElement.getAttribute("href");
|
|
@@ -489,7 +517,7 @@ function handleClick(e) {
|
|
|
489
517
|
tocElement?.classList?.remove("visible");
|
|
490
518
|
seriesElement?.classList?.remove("visible");
|
|
491
519
|
maskElement.classList.remove("visible");
|
|
492
|
-
|
|
520
|
+
setBodyScrollLocked(false);
|
|
493
521
|
return;
|
|
494
522
|
} else if (target?.className?.includes("fold-toggle")) {
|
|
495
523
|
const parentElement = target.parentElement;
|
|
@@ -517,24 +545,27 @@ function handleClick(e) {
|
|
|
517
545
|
}
|
|
518
546
|
}
|
|
519
547
|
|
|
548
|
+
var isClickHandlerBound = false;
|
|
520
549
|
function bindClickHandler() {
|
|
550
|
+
if (isClickHandlerBound) return;
|
|
521
551
|
document.addEventListener("click", handleClick, true);
|
|
552
|
+
isClickHandlerBound = true;
|
|
522
553
|
}
|
|
523
554
|
|
|
524
555
|
window.addEventListener("load", bindClickHandler);
|
|
525
|
-
window.addEventListener("pageshow", (
|
|
526
|
-
|
|
527
|
-
bindClickHandler();
|
|
528
|
-
});
|
|
556
|
+
window.addEventListener("pageshow", () => {
|
|
557
|
+
bindClickHandler();
|
|
529
558
|
});
|
|
530
559
|
|
|
531
560
|
function initAgeWarning() {
|
|
532
561
|
const ageWarningElement = document.getElementById("post-age-warning");
|
|
533
562
|
if (!ageWarningElement) return;
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
563
|
+
const dateElement = document.querySelector("#post .post-meta .date");
|
|
564
|
+
const dateStr = dateElement ? dateElement.getAttribute("datetime") : null;
|
|
565
|
+
if (!dateStr) return;
|
|
566
|
+
|
|
537
567
|
const postDate = new Date(dateStr);
|
|
568
|
+
if (isNaN(postDate.getTime())) return;
|
|
538
569
|
const now = new Date();
|
|
539
570
|
|
|
540
571
|
const diffTime = now - postDate;
|
|
@@ -547,10 +578,30 @@ function initAgeWarning() {
|
|
|
547
578
|
}
|
|
548
579
|
} else {
|
|
549
580
|
if (ageWarningElement) {
|
|
550
|
-
ageWarningElement.style = "
|
|
581
|
+
ageWarningElement.style.display = "none";
|
|
551
582
|
}
|
|
552
583
|
}
|
|
553
584
|
return diffDays;
|
|
554
585
|
}
|
|
555
586
|
|
|
556
587
|
initAgeWarning();
|
|
588
|
+
|
|
589
|
+
function initMaskScrollLock() {
|
|
590
|
+
var mask = document.getElementById("mask");
|
|
591
|
+
if (!mask) return;
|
|
592
|
+
mask.addEventListener(
|
|
593
|
+
"touchmove",
|
|
594
|
+
function (e) {
|
|
595
|
+
if (mask.classList.contains("visible")) {
|
|
596
|
+
e.preventDefault();
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
{ passive: false }
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (document.readyState === "loading") {
|
|
604
|
+
document.addEventListener("DOMContentLoaded", initMaskScrollLock);
|
|
605
|
+
} else {
|
|
606
|
+
initMaskScrollLock();
|
|
607
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
function replaceEmojis() {
|
|
2
|
+
if (!document.body) return;
|
|
2
3
|
const replacements = [{ emoji: "✅", className: "custom-checked-checkbox" }];
|
|
3
4
|
|
|
4
5
|
replacements.forEach(({ emoji, className }) => {
|
|
@@ -7,9 +8,16 @@ function replaceEmojis() {
|
|
|
7
8
|
NodeFilter.SHOW_TEXT,
|
|
8
9
|
{
|
|
9
10
|
acceptNode(node) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
if (!node.nodeValue || !node.nodeValue.includes(emoji)) {
|
|
12
|
+
return NodeFilter.FILTER_REJECT;
|
|
13
|
+
}
|
|
14
|
+
if (
|
|
15
|
+
node.parentElement &&
|
|
16
|
+
node.parentElement.closest("pre, code, textarea, script, style")
|
|
17
|
+
) {
|
|
18
|
+
return NodeFilter.FILTER_REJECT;
|
|
19
|
+
}
|
|
20
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
13
21
|
},
|
|
14
22
|
},
|
|
15
23
|
);
|
|
@@ -2,182 +2,212 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
2
2
|
const tocContainer = document.getElementById("toc");
|
|
3
3
|
if (!tocContainer) return;
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
const contentContainer =
|
|
6
|
+
document.getElementById("page-content") ||
|
|
7
|
+
document.getElementById("post") ||
|
|
8
|
+
document.body;
|
|
9
|
+
|
|
10
|
+
const headerElements = Array.from(
|
|
11
|
+
contentContainer.querySelectorAll("h2, h3"),
|
|
12
|
+
);
|
|
13
|
+
if (headerElements.length === 0) return;
|
|
14
|
+
|
|
15
|
+
// 1. 保证所有标题均具备有效 ID(补全缺失的 ID 并写回 DOM)
|
|
16
|
+
headerElements.forEach((header, index) => {
|
|
17
|
+
if (!header.id) {
|
|
18
|
+
const text = (header.textContent || "").trim();
|
|
19
|
+
header.id =
|
|
20
|
+
text.replace(/\s+/g, "-").toLowerCase() || `heading-${index}`;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// 2. 解析 TOC 目录的 DOM 结构建立映射字典
|
|
25
|
+
const tocItemWraps = Array.from(
|
|
26
|
+
tocContainer.querySelectorAll(".toc-item-wrap"),
|
|
27
|
+
);
|
|
28
|
+
const tocItemsMap = new Map(); // h2Id -> { wrap, link, subMap: Map(h3Id -> link) }
|
|
29
|
+
const h3ToH2Map = new Map(); // h3Id -> h2Id
|
|
30
|
+
|
|
31
|
+
tocItemWraps.forEach((wrap) => {
|
|
32
|
+
const h2Link = wrap.querySelector(".toc-item-link");
|
|
33
|
+
if (!h2Link) return;
|
|
34
|
+
const h2Id = h2Link.dataset.id;
|
|
35
|
+
if (!h2Id) return;
|
|
36
|
+
|
|
37
|
+
const subMap = new Map();
|
|
38
|
+
const subLinks = Array.from(wrap.querySelectorAll(".toc-sub-item-link"));
|
|
39
|
+
subLinks.forEach((subLink) => {
|
|
40
|
+
const h3Id = subLink.dataset.id;
|
|
41
|
+
if (h3Id) {
|
|
42
|
+
subMap.set(h3Id, subLink);
|
|
43
|
+
h3ToH2Map.set(h3Id, h2Id);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
tocItemsMap.set(h2Id, {
|
|
48
|
+
wrap,
|
|
49
|
+
link: h2Link,
|
|
50
|
+
subMap,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 3. 构建正文标题列表
|
|
55
|
+
let currentH2Id = null;
|
|
56
|
+
const headers = headerElements.map((header) => {
|
|
57
|
+
const isH2 = header.tagName === "H2";
|
|
58
|
+
if (isH2) {
|
|
59
|
+
currentH2Id = header.id;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
id: header.id,
|
|
63
|
+
isH2,
|
|
64
|
+
h2Id: isH2 ? header.id : h3ToH2Map.get(header.id) || currentH2Id,
|
|
65
|
+
element: header,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 顶部导航栏避让阈值
|
|
70
|
+
const TOP_OFFSET = 80;
|
|
7
71
|
|
|
8
|
-
const toc = [];
|
|
9
|
-
let lastH2 = null;
|
|
10
|
-
let previousH2Id = null;
|
|
11
|
-
let previousH3Id = null;
|
|
12
72
|
let lastActiveH2Id = null;
|
|
13
73
|
let lastActiveH3Id = null;
|
|
14
74
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
75
|
+
// 4. 目录容器内部滚动居中/可见,避免触发外部 window 的异常抖动
|
|
76
|
+
function scrollIntoTocView(element) {
|
|
77
|
+
if (!element || !tocContainer) return;
|
|
78
|
+
const containerRect = tocContainer.getBoundingClientRect();
|
|
79
|
+
const elementRect = element.getBoundingClientRect();
|
|
80
|
+
|
|
81
|
+
if (elementRect.top < containerRect.top) {
|
|
82
|
+
tocContainer.scrollTop -= containerRect.top - elementRect.top + 20;
|
|
83
|
+
} else if (elementRect.bottom > containerRect.bottom) {
|
|
84
|
+
tocContainer.scrollTop += elementRect.bottom - containerRect.bottom + 20;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 5. 计算当前应当激活的 H2 / H3
|
|
89
|
+
function determineActive() {
|
|
90
|
+
// 优先兼容外部锚定参数 window.anchoringId
|
|
91
|
+
const currentAnchoringId =
|
|
92
|
+
typeof window !== "undefined" && "anchoringId" in window
|
|
93
|
+
? window.anchoringId
|
|
94
|
+
: null;
|
|
95
|
+
|
|
96
|
+
if (currentAnchoringId) {
|
|
97
|
+
if (tocItemsMap.has(currentAnchoringId)) {
|
|
98
|
+
return {
|
|
99
|
+
h2Id: currentAnchoringId,
|
|
100
|
+
h3Id: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (h3ToH2Map.has(currentAnchoringId)) {
|
|
104
|
+
return {
|
|
105
|
+
h2Id: h3ToH2Map.get(currentAnchoringId),
|
|
106
|
+
h3Id: currentAnchoringId,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 根据滚动位置,查找位于视口顶部触发线(top <= TOP_OFFSET)之上的最后一个标题
|
|
112
|
+
let activeHeading = null;
|
|
113
|
+
for (let i = 0; i < headers.length; i++) {
|
|
114
|
+
const header = headers[i];
|
|
115
|
+
const rect = header.element.getBoundingClientRect();
|
|
116
|
+
if (rect.top <= TOP_OFFSET) {
|
|
117
|
+
activeHeading = header;
|
|
118
|
+
} else {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 若未滚动到任何标题(如文章开头前言),激活第一个 H2
|
|
124
|
+
if (!activeHeading) {
|
|
125
|
+
const firstH2 = headers.find((h) => h.isH2);
|
|
126
|
+
return {
|
|
127
|
+
h2Id: firstH2 ? firstH2.id : null,
|
|
128
|
+
h3Id: null,
|
|
27
129
|
};
|
|
28
|
-
toc.push(lastH2);
|
|
29
|
-
} else if (header.tagName === "H3" && lastH2) {
|
|
30
|
-
lastH2.children.push({
|
|
31
|
-
title,
|
|
32
|
-
id,
|
|
33
|
-
active: false,
|
|
34
|
-
visible: false,
|
|
35
|
-
});
|
|
36
130
|
}
|
|
37
|
-
});
|
|
38
131
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const isVisible = !!entry.intersectionRatio;
|
|
46
|
-
toc.forEach((h2) => {
|
|
47
|
-
if (h2.id === id) {
|
|
48
|
-
h2.visible = isVisible;
|
|
49
|
-
if (isVisible) {
|
|
50
|
-
previousH2Id = h2.id;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
h2.children.forEach((h3) => {
|
|
54
|
-
if (h3.id === id) {
|
|
55
|
-
h3.visible = isVisible;
|
|
56
|
-
if (isVisible) {
|
|
57
|
-
previousH3Id = h3.id;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
toc.forEach((item) => {
|
|
63
|
-
item.passiveVisible =
|
|
64
|
-
(item.children || []).findIndex((h3) => h3.visible) !== -1;
|
|
65
|
-
if (item.passiveVisible) {
|
|
66
|
-
previousH2Id = item.id;
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
});
|
|
132
|
+
if (activeHeading.isH2) {
|
|
133
|
+
return {
|
|
134
|
+
h2Id: activeHeading.id,
|
|
135
|
+
h3Id: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
70
138
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
h2.children.forEach((h3) => {
|
|
85
|
-
h3.active = false;
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
});
|
|
139
|
+
return {
|
|
140
|
+
h2Id: activeHeading.h2Id,
|
|
141
|
+
h3Id: activeHeading.id,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 6. 执行高亮更新与 DOM 渲染
|
|
146
|
+
function updateActive() {
|
|
147
|
+
const { h2Id, h3Id } = determineActive();
|
|
148
|
+
|
|
149
|
+
if (lastActiveH2Id === h2Id && lastActiveH3Id === h3Id) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
89
152
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
? item.id === anchoringId
|
|
93
|
-
: item.active,
|
|
94
|
-
);
|
|
153
|
+
lastActiveH2Id = h2Id;
|
|
154
|
+
lastActiveH3Id = h3Id;
|
|
95
155
|
|
|
96
|
-
|
|
97
|
-
let targetH3Id = null;
|
|
156
|
+
let targetElementToScroll = null;
|
|
98
157
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
158
|
+
tocItemsMap.forEach((item, currentH2Id) => {
|
|
159
|
+
const isH2Active = currentH2Id === h2Id;
|
|
160
|
+
if (isH2Active) {
|
|
161
|
+
item.wrap.classList.add("active");
|
|
162
|
+
targetElementToScroll = item.wrap;
|
|
103
163
|
} else {
|
|
104
|
-
|
|
105
|
-
if (isScrollingUp) {
|
|
106
|
-
const previousH2Index = toc.findIndex((h2) => h2.id === previousH2Id);
|
|
107
|
-
const previousH3ParentIndex = toc.findIndex((h2) =>
|
|
108
|
-
h2 && h2.children && h2.children.find((h3) => h3.id === previousH3Id),
|
|
109
|
-
);
|
|
110
|
-
let targetH2Index = 0;
|
|
111
|
-
if (!previousH3Id || previousH3ParentIndex > previousH2Index) {
|
|
112
|
-
targetH2Index = previousH2Index - 1;
|
|
113
|
-
} else {
|
|
114
|
-
targetH2Index = previousH3ParentIndex;
|
|
115
|
-
}
|
|
116
|
-
const newActiveH2 = toc[targetH2Index];
|
|
117
|
-
if (newActiveH2) {
|
|
118
|
-
targetH2Id = newActiveH2.id;
|
|
119
|
-
let activeH3Index = (newActiveH2.children ? newActiveH2.children.length : 0) - 1;
|
|
120
|
-
if (newActiveH2.children && newActiveH2.children.find((h3) => h3.id === previousH3Id)) {
|
|
121
|
-
const previousH3Index = newActiveH2.children.findIndex(
|
|
122
|
-
(h3) => h3.id === previousH3Id,
|
|
123
|
-
);
|
|
124
|
-
activeH3Index =
|
|
125
|
-
previousH3Index - 1 > -1 ? previousH3Index - 1 : 0;
|
|
126
|
-
}
|
|
127
|
-
targetH3Id = (newActiveH2.children && newActiveH2.children[activeH3Index] && newActiveH2.children[activeH3Index].id) || null;
|
|
128
|
-
}
|
|
129
|
-
} else {
|
|
130
|
-
targetH2Id = previousH2Id;
|
|
131
|
-
targetH3Id = previousH3Id;
|
|
132
|
-
}
|
|
164
|
+
item.wrap.classList.remove("active");
|
|
133
165
|
}
|
|
134
166
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
167
|
+
// 仅在当前 H2 为 active 时匹配子项 H3,彻底杜绝跨章节残留
|
|
168
|
+
item.subMap.forEach((subLink, currentH3Id) => {
|
|
169
|
+
if (isH2Active && currentH3Id === h3Id) {
|
|
170
|
+
subLink.classList.add("active");
|
|
171
|
+
targetElementToScroll = subLink;
|
|
172
|
+
} else {
|
|
173
|
+
subLink.classList.remove("active");
|
|
138
174
|
}
|
|
139
|
-
|
|
140
|
-
|
|
175
|
+
});
|
|
176
|
+
});
|
|
141
177
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
178
|
+
if (targetElementToScroll) {
|
|
179
|
+
scrollIntoTocView(targetElementToScroll);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
145
182
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
rootMargin: "0px",
|
|
179
|
-
threshold: [0, 1],
|
|
180
|
-
},
|
|
181
|
-
);
|
|
182
|
-
headers.forEach((header) => observer.observe(header));
|
|
183
|
+
// 7. 使用 requestAnimationFrame 优化滚动触发频率
|
|
184
|
+
let isTicking = false;
|
|
185
|
+
function requestUpdate() {
|
|
186
|
+
if (isTicking) return;
|
|
187
|
+
isTicking = true;
|
|
188
|
+
window.requestAnimationFrame(() => {
|
|
189
|
+
updateActive();
|
|
190
|
+
isTicking = false;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 8. 监听视口变化与滚动
|
|
195
|
+
window.addEventListener("scroll", requestUpdate, { passive: true });
|
|
196
|
+
window.addEventListener("resize", requestUpdate, { passive: true });
|
|
197
|
+
|
|
198
|
+
if ("IntersectionObserver" in window) {
|
|
199
|
+
const observer = new IntersectionObserver(
|
|
200
|
+
() => {
|
|
201
|
+
requestUpdate();
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
rootMargin: `-${TOP_OFFSET}px 0px 0px 0px`,
|
|
205
|
+
threshold: [0, 1],
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
headerElements.forEach((header) => observer.observe(header));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 初始执行一次
|
|
212
|
+
updateActive();
|
|
183
213
|
});
|