File size: 2,998 Bytes
e66cfb4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// assets/toc_scrollspy.js
console.log("[toc_scrollspy] loaded (v3-scroll-sync)");

(function () {
  let ticking = false;

  function getTocState() {
    const root = document.getElementById("toc-root");
    if (!root) return null;

    const offset = parseInt(root.dataset.tocOffset || "80", 10);
    const items = Array.from(root.querySelectorAll(".toc-item"));
    if (!items.length) return null;

    const pairs = items
      .map((item) => {
        const targetId = item.dataset.target;
        if (!targetId) return null;
        const el = document.getElementById(targetId);
        if (!el) return null;
        return { item, el, id: targetId };
      })
      .filter(Boolean);

    if (!pairs.length) return null;

    return { root, offset, pairs };
  }

  function keepItemVisible(root, item) {
    if (!root || !item) return;

    const rootTop = root.scrollTop;
    const rootBottom = rootTop + root.clientHeight;

    const itemTop = item.offsetTop;
    const itemBottom = itemTop + item.offsetHeight;

    if (itemTop < rootTop + 8) {
      root.scrollTo({
        top: Math.max(0, itemTop - 8),
        behavior: "smooth",
      });
    } else if (itemBottom > rootBottom - 8) {
      root.scrollTo({
        top: itemBottom - root.clientHeight + 8,
        behavior: "smooth",
      });
    }
  }

  function updateActiveToc() {
    const state = getTocState();
    if (!state) return;

    const { root, offset, pairs } = state;

    // 找目前最接近視窗頂端 offset 的 section
    let active = pairs[0];
    let bestDist = Infinity;

    for (const pair of pairs) {
      const rect = pair.el.getBoundingClientRect();
      const dist = Math.abs(rect.top - offset - 8);

      // 優先選在 offset 上方或接近 offset 的 section
      if (rect.top - offset <= 20 && dist < bestDist) {
        bestDist = dist;
        active = pair;
      }
    }

    // 如果上面都沒找到,就退回第一個可見 section
    if (!active) {
      for (const pair of pairs) {
        const rect = pair.el.getBoundingClientRect();
        if (rect.top >= offset) {
          active = pair;
          break;
        }
      }
    }

    pairs.forEach(({ item }) => item.classList.remove("toc-active"));

    if (active) {
      active.item.classList.add("toc-active");
      keepItemVisible(root, active.item);
    }
  }

  function requestUpdate() {
    if (ticking) return;
    ticking = true;
    window.requestAnimationFrame(() => {
      updateActiveToc();
      ticking = false;
    });
  }

  function start() {
    requestUpdate();

    window.addEventListener("scroll", requestUpdate, { passive: true });
    window.addEventListener("resize", requestUpdate, { passive: true });

    const mo = new MutationObserver(() => {
      requestUpdate();
    });

    mo.observe(document.body, { childList: true, subtree: true });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", start);
  } else {
    start();
  }
})();