~james/core

core/docs/src/components/Search.astro -rw-r--r-- 5.1 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
---
// starlight's search, with each result's excerpt cut down to a little context
// before its first match, so the match survives the one-line clamp in tangled.css.
// excerpts that start or stop mid-sentence are marked so the stylesheet can add
// an ellipsis at that end. results show a page of cards at a time, the last
// one faded, and scrolling it into view shows the next page, asking pagefind
// for more once the shown cards run low. the arrow keys move through the results
// while focus stays in the input, and enter opens the active one. each card's
// title is prefixed with its page's title, unless the card is the page itself
import Default from "@astrojs/starlight/components/Search.astro";
---

<Default />

<script>
  const context = 32;
  const pageSize = 7;
  const seen = new WeakSet<Text>();
  const cut = new WeakSet<Text>();

  const trim = (excerpt: Element) => {
    const lead = excerpt.firstChild;
    if (lead instanceof Text && lead.nextSibling && !seen.has(lead)) {
      seen.add(lead);
      if (lead.data.length > context) {
        const tail = lead.data.slice(-context);
        lead.data = tail.slice(tail.indexOf(" ") + 1);
        cut.add(lead);
      }
    }
    const text = excerpt.textContent?.trim() ?? "";
    const startsMidSentence = /^\p{Ll}/u.test(text);
    excerpt.toggleAttribute("data-clipped", (lead instanceof Text && cut.has(lead)) || startsMidSentence);
    excerpt.toggleAttribute("data-trails", !/[.!?:]$/.test(text));
  };

  const results = document.querySelector<HTMLElement>("#starlight__search");
  if (results) {
    let active: Element | undefined;
    let chosen = false;
    let limit = pageSize;
    const allCards = () => results.querySelectorAll(".pagefind-ui__result-nested");
    const cards = () => [...results.querySelectorAll(".pagefind-ui__result-nested:not([data-overflow])")];
    const pagefindMore = () => results.querySelector<HTMLButtonElement>(".pagefind-ui__button");

    const frame = results.closest(".dialog-frame");
    let pending = false;
    let count = 0;
    let watched: Element | undefined;

    const loadMore = () => {
      const all = allCards().length;
      if (all > limit) limit += pageSize;
      const button = pagefindMore();
      if (button && !pending && all < limit + pageSize) {
        pending = true;
        button.click();
      }
      paginate();
    };

    const watcher = new IntersectionObserver((entries) => entries.some((e) => e.isIntersecting) && loadMore(), {
      root: frame,
      rootMargin: "0px 0px 120px 0px",
    });

    const paginate = () => {
      const all = allCards();
      all.forEach((card, i) => {
        card.toggleAttribute("data-overflow", i >= limit);
        card.removeAttribute("data-faded");
      });
      const last = all.length > limit || pagefindMore() ? all[Math.min(limit, all.length) - 1] : undefined;
      last?.setAttribute("data-faded", "");
      if (last !== watched) {
        watcher.disconnect();
        watched = last;
        if (last) watcher.observe(last);
      }
    };

    const activate = (card: Element | undefined, scroll = false) => {
      active?.removeAttribute("data-active");
      active = card;
      active?.setAttribute("data-active", "");
      if (scroll) active?.scrollIntoView({ block: "nearest" });
    };

    const label = () => {
      results.querySelectorAll(".pagefind-ui__result").forEach((result) => {
        const page = result.querySelector(".pagefind-ui__result-inner > .pagefind-ui__result-title")?.textContent?.trim();
        result.querySelectorAll(".pagefind-ui__result-nested .pagefind-ui__result-link").forEach((link) => {
          const own = link.textContent?.trim();
          if (page && own && own !== page) link.setAttribute("data-page", page);
          else link.removeAttribute("data-page");
        });
      });
    };

    new MutationObserver(() => {
      if (allCards().length !== count) {
        count = allCards().length;
        pending = false;
      }
      paginate();
      label();
      results.querySelectorAll(".pagefind-ui__result-excerpt").forEach(trim);
      if (!chosen || !active?.isConnected) activate(cards()[0]);
    }).observe(results, { childList: true, subtree: true });

    results.addEventListener("input", () => {
      chosen = false;
      pending = false;
      limit = pageSize;
      activate(undefined);
    });

    results.addEventListener("pointermove", (e) => {
      const card = (e.target as Element).closest(".pagefind-ui__result-nested");
      if (card && card !== active) {
        chosen = true;
        activate(card);
      }
    });

    results.addEventListener("keydown", (e) => {
      if (!(e.target instanceof HTMLInputElement)) return;
      if (e.key === "ArrowDown" || e.key === "ArrowUp") {
        const all = cards();
        if (!all.length) return;
        e.preventDefault();
        chosen = true;
        const i = active ? all.indexOf(active) : -1;
        const next = e.key === "ArrowDown" ? Math.min(i + 1, all.length - 1) : Math.max(i - 1, 0);
        activate(all[next], true);
      } else if (e.key === "Enter" && !e.isComposing && active) {
        e.preventDefault();
        active.querySelector<HTMLAnchorElement>("a")?.click();
      }
    });
  }
</script>