master
 1import { t } from '@utils/i18n';
 2import type { ParentComponent } from 'solid-js';
 3import { createSignal, onCleanup, onMount } from 'solid-js';
 4
 5const TocButton: ParentComponent = (props) => {
 6  const [isOpen, setIsOpen] = createSignal(false);
 7  const [isWideScreen, setIsWideScreen] = createSignal(false);
 8  const [hasToc, setHasToc] = createSignal(false);
 9  let tocWrapperRef: HTMLDivElement | undefined;
10  let buttonRef: HTMLButtonElement | undefined;
11
12  const handleResize = () => {
13    setIsWideScreen(window.innerWidth > 1280);
14  };
15
16  const setup = () => {
17    const toc = document.getElementById('toc');
18    const wrapper = tocWrapperRef;
19    if (toc && wrapper) {
20      wrapper.innerHTML = '';
21      wrapper.appendChild(toc.cloneNode(true));
22      (wrapper.children[0] as HTMLElement).id = 'stb-toc-content';
23
24      const remainAttrs = ['class', 'style'];
25      Array.from((wrapper.children[0] as Element).attributes).forEach((attr) => {
26        if (!remainAttrs.includes(attr.name)) {
27          (wrapper.children[0] as Element).removeAttribute(attr.name);
28        }
29      });
30
31      setHasToc(true);
32    } else {
33      setHasToc(false);
34    }
35
36    window.addEventListener('resize', handleResize);
37    handleResize(); // 初始一次
38  };
39
40  const cleanup = () => {
41    setIsOpen(false);
42    setHasToc(false);
43    tocWrapperRef!.innerHTML = '';
44    window.removeEventListener('resize', handleResize);
45    setIsWideScreen(false);
46  };
47
48  onMount(() => {
49    document.addEventListener('astro:page-load', setup);
50    setup();
51    document.addEventListener('astro:before-swap', cleanup);
52
53    onCleanup(() => {
54      document.removeEventListener('astro:page-load', setup);
55      document.removeEventListener('astro:before-swap', cleanup);
56      cleanup();
57      setIsOpen(false);
58      tocWrapperRef!.innerHTML = '';
59    });
60  });
61
62  return (
63    <div
64      classList={{
65        hidden: !hasToc() || isWideScreen(),
66      }}
67    >
68      <button
69        ref={buttonRef}
70        type="button"
71        class="btn btn-circle btn-secondary btn-sm"
72        title={t.info.toc()}
73        aria-label={t.info.toc()}
74        aria-expanded={isOpen()}
75        aria-controls="stb-toc-wrapper"
76        onClick={() => setIsOpen((v) => !v)}
77      >
78        {props.children}
79      </button>
80
81      <div
82        ref={tocWrapperRef}
83        id="stb-toc-wrapper"
84        classList={{
85          'rounded-box absolute w-[calc(100vw-4rem)] -translate-x-1/2 -translate-y-1/2 max-w-72 backdrop-blur-md duration-300 text-base-content text-start': true,
86          'scale-0 opacity-0': !isOpen() || isWideScreen(), // closed
87          '-translate-x-[calc(100%+0.5rem)]! -translate-y-[calc(100%-2.5rem)]!':
88            isOpen() && !isWideScreen(),
89          hidden: !hasToc() || isWideScreen(),
90        }}
91        inert={!isOpen() || isWideScreen()}
92      />
93    </div>
94  );
95};
96
97export default TocButton;