PageSourceSearch

https://elucive.co/assets/js/palette.353cc89133.js

js elucive.co collected 2026-10-03 18:19:32 UTC 9,289 bytes, 225 lines download raw bytes

1/* ==========================================================================
2   The command palette
3   --------------------------------------------------------------------------
4   Cmd-K, or Ctrl-K, or "/" from anywhere that is not a text field. Type a few
5   letters, hit Enter, go. It indexes the page's own sections and links at
6   load, so there is no list to keep in step with the navigation - add a
7   section and it is in the palette.
8
9   WHY IT IS WORTH THE KILOBYTES ON A MARKETING SITE. Most visitors will never
10   press it, and that is fine: it costs them nothing, because the whole thing
11   binds one keydown listener and builds no DOM until it is opened. The ones
12   who do press it are developers, and they are the audience.
13
14   Matching is a subsequence test, not a substring one: "vbs" finds "Village
15   Banking System" the way a fuzzy finder does. Ranked by how tightly the
16   letters landed, so an exact prefix beats letters scattered across a phrase.
17   ========================================================================== */
18
19(function () {
20  "use strict";
21
22  var overlay = null, input = null, list = null, items = [], active = 0, lastFocus = null;
23
24  /* ------------------------------------------------------------- the index */
25  function build() {
26    var seen = {}, out = [];
27
28    function add(label, href, group) {
29      var key = group + "|" + label.toLowerCase();
30      if (!label || seen[key]) return;
31      seen[key] = 1;
32      out.push({ label: label.replace(/\s+/g, " ").trim(), href: href, group: group });
33    }
34
35    document.querySelectorAll("main section[id], main div[id]").forEach(function (s) {
36      var h = s.querySelector("h1, h2, .statement__title, .section__title");
37      if (h) add(h.textContent, "#" + s.id, "On this page");
38    });
39    document.querySelectorAll(".nav a[href], .footer a[href]").forEach(function (a) {
40      var href = a.getAttribute("href");
41      if (!href || href.charAt(0) === "#" || /^(mailto|tel):/.test(href)) return;
42      add(a.textContent, href, /blog/.test(href) ? "Writing" : "Pages");
43    });
44    document.querySelectorAll("a[href^='/blog/']").forEach(function (a) {
45      var t = a.querySelector("h2, h3");
46      if (t) add(t.textContent, a.getAttribute("href"), "Writing");
47    });
48
49    add("Switch theme", "#theme", "Actions");
50    return out;
51  }
52
53  /* SUBSEQUENCE, NOT SUBSTRING. Returns null for no match, otherwise a score
54     where lower is tighter - consecutive letters cost nothing, gaps cost. */
55  function score(needle, hay) {
56    if (!needle) return 0;
57    var n = needle.toLowerCase(), h = hay.toLowerCase();
58    var i = 0, gaps = 0, lastHit = -1;
59    for (var j = 0; j < h.length && i < n.length; j++) {
60      if (h[j] === n[i]) {
61        if (lastHit >= 0) gaps += j - lastHit - 1;
62        lastHit = j; i++;
63      }
64    }
65    if (i < n.length) return null;
66    return gaps + lastHit * 0.1;
67  }
68
69  /* -------------------------------------------------------------- the view */
70  function open() {
71    if (overlay) return;
72    lastFocus = document.activeElement;
73    items = build();
74
75    overlay = document.createElement("div");
76    overlay.className = "palette";
77    overlay.setAttribute("role", "dialog");
78    overlay.setAttribute("aria-modal", "true");
79    overlay.setAttribute("aria-label", "Command palette");
80    overlay.innerHTML =
81      '<div class="palette__panel">' +
82        '<div class="palette__bar">' +
83          '<span class="palette__prompt" aria-hidden="true">&rsaquo;</span>' +
84          '<input class="palette__input" type="text" placeholder="Jump to…" ' +
85                 'autocomplete="off" spellcheck="false" aria-label="Search the site" ' +
86                 'role="combobox" aria-expanded="false" aria-autocomplete="list" ' +
87                 'aria-controls="palette-list">' +
88          '<kbd class="palette__esc">esc</kbd>' +
89        '</div>' +
90        '<div class="palette__list" id="palette-list" role="listbox" ' +
91             'aria-label="Results"></div>' +
92      '</div>';
93    document.body.appendChild(overlay);
94    document.documentElement.style.overflow = "hidden";
95
96    input = overlay.querySelector(".palette__input");
97    list = overlay.querySelector(".palette__list");
98    input.addEventListener("input", render);
99    input.addEventListener("keydown", onKey);
100    overlay.addEventListener("mousedown", function (e) { if (e.target === overlay) close(); });
101
102    render();
103    input.focus();
104  }
105
106  function close() {
107    if (!overlay) return;
108    overlay.remove();
109    overlay = null;
110    document.documentElement.style.overflow = "";
111    if (lastFocus && lastFocus.focus) lastFocus.focus();
112  }
113
114  function matches() {
115    var q = input.value.trim();
116    return items
117      .map(function (it) { var s = score(q, it.label); return s === null ? null : { it: it, s: s }; })
118      .filter(Boolean)
119      .sort(function (a, b) { return a.s - b.s; })
120      .slice(0, 9)
121      .map(function (x) { return x.it; });
122  }
123
124  var shown = [];
125  function render() {
126    shown = matches();
127    active = 0;
128    list.textContent = "";
129    if (!shown.length) {
130      var none = document.createElement("p");
131      none.className = "palette__empty";
132      none.textContent = "Nothing matches that.";
133      list.appendChild(none);
134      input.removeAttribute("aria-activedescendant");
135      input.setAttribute("aria-expanded", "false");
136      return;
137    }
138    shown.forEach(function (it, i) {
139      var row = document.createElement("button");
140      row.type = "button";
141      row.className = "palette__item" + (i === 0 ? " is-active" : "");
142      row.setAttribute("role", "option");
143      /* The rows are <button>, so they are in the tab order by default - which
144         means Tab walks THROUGH the results instead of the arrow keys driving
145         them, and the is-active highlight and the real focus ring drift apart.
146         Focus stays on the input; the rows are pointed at, not tabbed to. */
147      row.id = "palette-opt-" + i;
148      row.tabIndex = -1;
149      row.setAttribute("aria-selected", i === 0 ? "true" : "false");
150      row.innerHTML = '<span class="palette__label"></span><span class="palette__group"></span>';
151      row.querySelector(".palette__label").textContent = it.label;
152      row.querySelector(".palette__group").textContent = it.group;
153      row.addEventListener("click", function () { go(it); });
154      row.addEventListener("mousemove", function () { setActive(i); });
155      list.appendChild(row);
156    });
157    input.setAttribute("aria-expanded", "true");
158    input.setAttribute("aria-activedescendant", "palette-opt-0");
159  }
160
161  function setActive(i) {
162    var rows = list.querySelectorAll(".palette__item");
163    if (!rows.length) return;
164    active = (i + rows.length) % rows.length;
165    rows.forEach(function (r, n) {
166      r.classList.toggle("is-active", n === active);
167      r.setAttribute("aria-selected", n === active ? "true" : "false");
168    });
169    /* Without this the highlight is purely visual: focus never leaves the
170       input, so a screen reader announces nothing as the arrow keys move. */
171    input.setAttribute("aria-activedescendant", rows[active].id);
172    rows[active].scrollIntoView({ block: "nearest" });
173  }
174
175  function go(it) {
176    close();
177    if (it.href === "#theme") {
178      var t = document.querySelector("[data-theme-toggle]");
179      if (t) t.click();
180      return;
181    }
182    if (it.href.charAt(0) === "#") {
183      var target = document.querySelector(it.href);
184      if (target) {
185        target.scrollIntoView({ behavior: "smooth", block: "start" });
186        history.replaceState(null, "", it.href);
187      }
188      return;
189    }
190    window.location.href = it.href;
191  }
192
193  function onKey(e) {
194    /* The overlay says aria-modal="true", which tells assistive technology that
195       nothing behind it exists. Tab did not agree: it walked straight out to the
196       page underneath, leaving focus somewhere invisible behind the scrim with
197       no way back except the mouse. Nothing outside is reachable, so the only
198       correct destination for Tab is where focus already is. */
199    if (e.key === "Tab") { e.preventDefault(); return; }
200    if (e.key === "Escape") { e.preventDefault(); close(); }
201    else if (e.key === "ArrowDown") { e.preventDefault(); setActive(active + 1); }
202    else if (e.key === "ArrowUp") { e.preventDefault(); setActive(active - 1); }
203    else if (e.key === "Enter") { e.preventDefault(); if (shown[active]) go(shown[active]); }
204  }
205
206  /* ------------------------------------------------------------ the trigger */
207  document.addEventListener("keydown", function (e) {
208    var typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement.tagName) ||
209                 document.activeElement.isContentEditable;
210    if ((e.key === "k" || e.key === "K") && (e.metaKey || e.ctrlKey)) {
211      e.preventDefault();
212      overlay ? close() : open();
213      return;
214    }
215    /* "/" is the other muscle memory, and it must not fire while somebody is
216       typing a slash into the contact form. */
217    if (e.key === "/" && !typing && !overlay) { e.preventDefault(); open(); }
218  });
219
220  /* Something visible has to say it exists, or nobody finds it. */
221  document.querySelectorAll("[data-palette-open]").forEach(function (b) {
222    b.addEventListener("click", open);
223    b.hidden = false;
224  });
225})();

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.