OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 153 LINES
 
[ HISTORY ]  [ UP ]
 

---
import Pre from "../Pre/Pre.astro";
import NavBar from "../NavBar/NavBar.astro";
import ThemeToggle from "../ThemeToggle/ThemeToggle.astro";
import { rule, ruleD } from "../../lib/ascii";
import { FRAME_W, MOBILE_FRAME_W } from "../../lib/config";
import {
  MASTHEAD_LABEL,
  MASTHEAD_SEARCH_WIDE,
  MASTHEAD_SEARCH_NARROW,
  MASTHEAD_TOGGLE_GAP,
  mastheadPad,
} from "./TopHeader";
import "./TopHeader.scss";

const padW = mastheadPad(FRAME_W, MASTHEAD_SEARCH_WIDE);
const padN = mastheadPad(MOBILE_FRAME_W, MASTHEAD_SEARCH_NARROW);
const toggleGap = " ".repeat(MASTHEAD_TOGGLE_GAP);
const PLACEHOLDER_WIDE = "_".repeat(20);
const PLACEHOLDER_NARROW = "_".repeat(18);
---
<div class="frame-wide top-header">
  <div class="masthead">
    <Pre><a href="/" class="masthead-home">{MASTHEAD_LABEL}</a>{padW}<span class="
masthead-search">[ /  search the index <input class="masthead-search-input" type="
text" aria-label="Search notes" autocomplete="off" spellcheck="false" placeholder=
{PLACEHOLDER_WIDE} maxlength="20" data-search-input data-cells="20" /> ]</span>{to
ggleGap}<ThemeToggle /></Pre>
    <ul class="masthead-search-results" hidden data-search-results></ul>
  </div>
  <Pre>{rule(FRAME_W)}</Pre>
  <NavBar width={FRAME_W} />
  <Pre>{ruleD(FRAME_W)}</Pre>
</div>
<div class="frame-narrow top-header">
  <div class="masthead">
    <Pre><a href="/" class="masthead-home">{MASTHEAD_LABEL}</a>{padN}<span class="
masthead-search">[ /  search <input class="masthead-search-input" type="text" aria
-label="Search notes" autocomplete="off" spellcheck="false" placeholder={PLACEHOLD
ER_NARROW} maxlength="18" data-search-input data-cells="18" /> ]</span>{toggleGap}
<ThemeToggle /></Pre>
    <ul class="masthead-search-results" hidden data-search-results></ul>
  </div>
  <Pre>{rule(MOBILE_FRAME_W)}</Pre>
  <NavBar width={MOBILE_FRAME_W} gap={1} lang={false} />
  <Pre>{ruleD(MOBILE_FRAME_W)}</Pre>
</div>

<script>
  import { searchIndex, type SearchItem } from "../../lib/search";

  let data: SearchItem[] | null = null;

  async function ensureLoaded() {
    if (data) return;
    const res = await fetch("/search.json");
    data = await res.json();
  }

  function getResultsList(input: HTMLInputElement) {
    return input.closest(".masthead")?.querySelector<HTMLUListElement>("[data-sear
ch-results]") ?? null;
  }

  function activeIndexFor(list: HTMLUListElement) {
    const items = Array.from(list.querySelectorAll("li"));
    return items.findIndex((el) => el.classList.contains("active"));
  }

  function setActive(list: HTMLUListElement, idx: number) {
    const items = Array.from(list.querySelectorAll<HTMLLIElement>("li"));
    items.forEach((el, i) => el.classList.toggle("active", i === idx));
    if (idx >= 0 && items[idx]) items[idx].scrollIntoView({ block: "nearest" });
  }

  function close(list: HTMLUListElement) {
    list.hidden = true;
    list.innerHTML = "";
  }

  function render(list: HTMLUListElement, hits: SearchItem[]) {
    if (!hits.length) { close(list); return; }
    list.hidden = false;
    list.innerHTML = hits
      .map((item, i) => {
        const safe = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
        return `<li${i === 0 ? ' class="active"' : ""}><a href="${safe(item.url)}"
>${safe(item.note)} - ${safe(item.title)}</a></li>`;
      })
      .join("");
  }

  async function onInput(e: Event) {
    const input = (e.target as HTMLElement).closest<HTMLInputElement>("[data-searc
h-input]");
    if (!input) return;
    const list = getResultsList(input);
    if (!list) return;
    const q = input.value.trim();
    if (!q) { close(list); return; }
    await ensureLoaded();
    render(list, searchIndex(data!, q));
  }

  function onKeydown(e: KeyboardEvent) {
    const target = e.target as HTMLElement;
    const input = target.closest?.<HTMLInputElement>("[data-search-input]");
    if (!input) {
      // Global "/" focuses the visible search input.
      if (e.key === "/" && !["INPUT", "TEXTAREA"].includes(target.tagName)) {
        const visible = Array.from(document.querySelectorAll<HTMLInputElement>("[d
ata-search-input]"))
          .find((el) => el.offsetParent !== null);
        if (visible) {
          e.preventDefault();
          visible.focus();
          visible.select();
        }
      }
      return;
    }
    const list = getResultsList(input);
    if (!list || list.hidden) return;
    const items = Array.from(list.querySelectorAll<HTMLLIElement>("li"));
    const idx = activeIndexFor(list);
    if (e.key === "ArrowDown") {
      e.preventDefault();
      setActive(list, Math.min(items.length - 1, idx + 1));
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      setActive(list, Math.max(0, idx - 1));
    } else if (e.key === "Enter") {
      const link = items[Math.max(0, idx)]?.querySelector("a");
      if (link) { e.preventDefault(); window.location.href = link.href; }
    } else if (e.key === "Escape") {
      e.preventDefault();
      input.value = "";
      close(list);
      input.blur();
    }
  }

  function onDocClick(e: MouseEvent) {
    const target = e.target as HTMLElement;
    if (target.closest(".masthead")) return;
    document.querySelectorAll<HTMLUListElement>("[data-search-results]").forEach(c
lose);
  }

  document.addEventListener("input", onInput);
  document.addEventListener("keydown", onKeydown);
  document.addEventListener("click", onDocClick);

  // Tag badges link to /?q=<tag>; prefill the search and show results.
  const q = new URLSearchParams(window.location.search).get("q");
  if (q) {
    const visible = Array.from(document.querySelectorAll<HTMLInputElement>("[data-
search-input]"))
      .find((el) => el.offsetParent !== null);
    if (visible) {
      visible.value = q.slice(0, Number(visible.dataset.cells) || 20);
      visible.dispatchEvent(new Event("input", { bubbles: true }));
      visible.focus();
    }
  }
</script>

---
import Pre from "../Pre/Pre.astro";
import NavBar from "../NavBar/NavBar.astro";
import ThemeToggle from "../ThemeToggle/Them
eToggle.astro";
import { rule, ruleD } from "../../lib/ascii
";
import { FRAME_W, MOBILE_FRAME_W } from "../
../lib/config";
import {
  MASTHEAD_LABEL,
  MASTHEAD_SEARCH_WIDE,
  MASTHEAD_SEARCH_NARROW,
  MASTHEAD_TOGGLE_GAP,
  mastheadPad,
} from "./TopHeader";
import "./TopHeader.scss";

const padW = mastheadPad(FRAME_W, MASTHEAD_S
EARCH_WIDE);
const padN = mastheadPad(MOBILE_FRAME_W, MAS
THEAD_SEARCH_NARROW);
const toggleGap = " ".repeat(MASTHEAD_TOGGLE
_GAP);
const PLACEHOLDER_WIDE = "_".repeat(20);
const PLACEHOLDER_NARROW = "_".repeat(18);
---
<div class="frame-wide top-header">
  <div class="masthead">
    <Pre><a href="/" class="masthead-home">{
MASTHEAD_LABEL}</a>{padW}<span class="masthe
ad-search">[ /  search the index <input clas
s="masthead-search-input" type="text" aria-l
abel="Search notes" autocomplete="off" spell
check="false" placeholder={PLACEHOLDER_WIDE}
 maxlength="20" data-search-input data-cells
="20" /> ]</span>{toggleGap}<ThemeToggle /><
/Pre>
    <ul class="masthead-search-results" hidd
en data-search-results></ul>
  </div>
  <Pre>{rule(FRAME_W)}</Pre>
  <NavBar width={FRAME_W} />
  <Pre>{ruleD(FRAME_W)}</Pre>
</div>
<div class="frame-narrow top-header">
  <div class="masthead">
    <Pre><a href="/" class="masthead-home">{
MASTHEAD_LABEL}</a>{padN}<span class="masthe
ad-search">[ /  search <input class="masthea
d-search-input" type="text" aria-label="Sear
ch notes" autocomplete="off" spellcheck="fal
se" placeholder={PLACEHOLDER_NARROW} maxleng
th="18" data-search-input data-cells="18" />
 ]</span>{toggleGap}<ThemeToggle /></Pre>
    <ul class="masthead-search-results" hidd
en data-search-results></ul>
  </div>
  <Pre>{rule(MOBILE_FRAME_W)}</Pre>
  <NavBar width={MOBILE_FRAME_W} gap={1} lan
g={false} />
  <Pre>{ruleD(MOBILE_FRAME_W)}</Pre>
</div>

<script>
  import { searchIndex, type SearchItem } fr
om "../../lib/search";

  let data: SearchItem[] | null = null;

  async function ensureLoaded() {
    if (data) return;
    const res = await fetch("/search.json");
    data = await res.json();
  }

  function getResultsList(input: HTMLInputEl
ement) {
    return input.closest(".masthead")?.query
Selector<HTMLUListElement>("[data-search-res
ults]") ?? null;
  }

  function activeIndexFor(list: HTMLUListEle
ment) {
    const items = Array.from(list.querySelec
torAll("li"));
    return items.findIndex((el) => el.classL
ist.contains("active"));
  }

  function setActive(list: HTMLUListElement,
 idx: number) {
    const items = Array.from(list.querySelec
torAll<HTMLLIElement>("li"));
    items.forEach((el, i) => el.classList.to
ggle("active", i === idx));
    if (idx >= 0 && items[idx]) items[idx].s
crollIntoView({ block: "nearest" });
  }

  function close(list: HTMLUListElement) {
    list.hidden = true;
    list.innerHTML = "";
  }

  function render(list: HTMLUListElement, hi
ts: SearchItem[]) {
    if (!hits.length) { close(list); return;
 }
    list.hidden = false;
    list.innerHTML = hits
      .map((item, i) => {
        const safe = (s: string) => s.replac
e(/&/g, "&amp;").replace(/</g, "&lt;").repla
ce(/>/g, "&gt;");
        return `<li${i === 0 ? ' class="acti
ve"' : ""}><a href="${safe(item.url)}">${saf
e(item.note)} - ${safe(item.title)}</a></li>
`;
      })
      .join("");
  }

  async function onInput(e: Event) {
    const input = (e.target as HTMLElement).
closest<HTMLInputElement>("[data-search-inpu
t]");
    if (!input) return;
    const list = getResultsList(input);
    if (!list) return;
    const q = input.value.trim();
    if (!q) { close(list); return; }
    await ensureLoaded();
    render(list, searchIndex(data!, q));
  }

  function onKeydown(e: KeyboardEvent) {
    const target = e.target as HTMLElement;
    const input = target.closest?.<HTMLInput
Element>("[data-search-input]");
    if (!input) {
      // Global "/" focuses the visible sear
ch input.
      if (e.key === "/" && !["INPUT", "TEXTA
REA"].includes(target.tagName)) {
        const visible = Array.from(document.
querySelectorAll<HTMLInputElement>("[data-se
arch-input]"))
          .find((el) => el.offsetParent !== 
null);
        if (visible) {
          e.preventDefault();
          visible.focus();
          visible.select();
        }
      }
      return;
    }
    const list = getResultsList(input);
    if (!list || list.hidden) return;
    const items = Array.from(list.querySelec
torAll<HTMLLIElement>("li"));
    const idx = activeIndexFor(list);
    if (e.key === "ArrowDown") {
      e.preventDefault();
      setActive(list, Math.min(items.length 
- 1, idx + 1));
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      setActive(list, Math.max(0, idx - 1));
    } else if (e.key === "Enter") {
      const link = items[Math.max(0, idx)]?.
querySelector("a");
      if (link) { e.preventDefault(); window
.location.href = link.href; }
    } else if (e.key === "Escape") {
      e.preventDefault();
      input.value = "";
      close(list);
      input.blur();
    }
  }

  function onDocClick(e: MouseEvent) {
    const target = e.target as HTMLElement;
    if (target.closest(".masthead")) return;
    document.querySelectorAll<HTMLUListEleme
nt>("[data-search-results]").forEach(close);
  }

  document.addEventListener("input", onInput
);
  document.addEventListener("keydown", onKey
down);
  document.addEventListener("click", onDocCl
ick);

  // Tag badges link to /?q=<tag>; prefill t
he search and show results.
  const q = new URLSearchParams(window.locat
ion.search).get("q");
  if (q) {
    const visible = Array.from(document.quer
ySelectorAll<HTMLInputElement>("[data-search
-input]"))
      .find((el) => el.offsetParent !== null
);
    if (visible) {
      visible.value = q.slice(0, Number(visi
ble.dataset.cells) || 20);
      visible.dispatchEvent(new Event("input
", { bubbles: true }));
      visible.focus();
    }
  }
</script>
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET