PageSourceSearch

https://ganzhixiong.com/js/toc.js

js ganzhixiong.com collected 2026-09-25 22:32:05 UTC 2,828 bytes, 80 lines download raw bytes

1(function (window, document) {
2  function register($toc) {
3    const currentInView = new Set();
4    const headingToMenu = new Map();
5    const $menus = Array.from($toc.querySelectorAll('.menu-list > li > a'));
6
7    for (const $menu of $menus) {
8      const elementId = $menu.getAttribute('href').trim().slice(1);
9      const $heading = document.getElementById(elementId);
10      if ($heading) {
11        headingToMenu.set($heading, $menu);
12      }
13    }
14
15    const $headings = Array.from(headingToMenu.keys());
16
17    const callback = (entries) => {
18      for (const entry of entries) {
19        if (entry.isIntersecting) {
20          currentInView.add(entry.target);
21        } else {
22          currentInView.delete(entry.target);
23        }
24      }
25      let $heading;
26      if (currentInView.size) {
27        // heading is the first in-view heading
28        $heading = [...currentInView].sort(($el1, $el2) => $el1.offsetTop - $el2.offsetTop)[0];
29      } else if ($headings.length) {
30        // heading is the closest heading above the viewport top
31        $heading = $headings
32          .filter(($heading) => $heading.offsetTop < window.scrollY)
33          .sort(($el1, $el2) => $el2.offsetTop - $el1.offsetTop)[0];
34      }
35      if ($heading && headingToMenu.has($heading)) {
36        $menus.forEach(($menu) => $menu.classList.remove('is-active'));
37
38        const $menu = headingToMenu.get($heading);
39        $menu.classList.add('is-active');
40        let $menuList = $menu.parentElement.parentElement;
41        while (
42          $menuList.classList.contains('menu-list') &&
43          $menuList.parentElement.tagName.toLowerCase() === 'li'
44        ) {
45          $menuList.parentElement.children[0].classList.add('is-active');
46          $menuList = $menuList.parentElement.parentElement;
47        }
48      }
49    };
50    const observer = new IntersectionObserver(callback, { threshold: 0 });
51
52    for (const $heading of $headings) {
53      observer.observe($heading);
54      // smooth scroll to the heading
55      if (headingToMenu.has($heading)) {
56        const $menu = headingToMenu.get($heading);
57        $menu.setAttribute('data-href', $menu.getAttribute('href'));
58        $menu.setAttribute('href', 'javascript:;');
59        $menu.addEventListener('click', () => {
60          if (typeof $heading.scrollIntoView === 'function') {
61            $heading.scrollIntoView({ behavior: 'smooth' });
62          }
63          const anchor = $menu.getAttribute('data-href');
64          if (history.pushState) {
65            history.pushState(null, null, anchor);
66          } else {
67            location.hash = anchor;
68          }
69        });
70        $heading.style.scrollMargin = '1em';
71      }
72    }
73  }
74
75  if (typeof window.IntersectionObserver === 'undefined') {
76    return;
77  }
78
79  document.querySelectorAll('#toc').forEach(register);
80})(window, document);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.