/** * Patch: add datalist autocomplete to the web component. * * Fetches /micropub?q=category&filter= as the user types (debounced * 300ms) and populates a native . Selecting a suggestion fills the * text field; the user confirms with comma or blur as usual. * * Datalist is explicitly cleared in keydown(Comma) and blur handlers because * the existing handlers clear the field via programmatic .value = "" which * does NOT fire the input event (HTML Living Standard). */ import { access, readFile, writeFile } from "node:fs/promises"; const MARKER = "// [patch] tag-input-autocomplete"; const candidates = [ "node_modules/@indiekit/frontend/components/tag-input/index.js", "node_modules/@indiekit/indiekit/node_modules/@indiekit/frontend/components/tag-input/index.js", ]; const OLD_SNIPPET = ` // Capture any value in input not converted to tag (for example, by clicking // outside component before pressing tab key) and add to list of tags. $tagInputInput.addEventListener("blur", () => { if ($tagInputInput.value) { tagInput.addTag($tagInputInput.value, false); $tagInputInput.value = ""; } }); return tagInput;`; const NEW_SNIPPET = ` // Capture any value in input not converted to tag (for example, by clicking // outside component before pressing tab key) and add to list of tags. $tagInputInput.addEventListener("blur", () => { if ($tagInputInput.value) { tagInput.addTag($tagInputInput.value, false); $tagInputInput.value = ""; } }); // Autocomplete: wire datalist to Micropub q=category endpoint ${MARKER} const _micropubHref = document.querySelector('link[rel="micropub"]')?.href ?? "/micropub"; const _datalistId = "tag-suggestions-" + (this.$replacedInput?.getAttribute("name") ?? Math.random().toString(36).slice(2)); const _$datalist = document.createElement("datalist"); _$datalist.id = _datalistId; this.appendChild(_$datalist); $tagInputInput.setAttribute("list", _datalistId); let _autocompleteTimer; $tagInputInput.addEventListener("input", () => { clearTimeout(_autocompleteTimer); const _val = $tagInputInput.value.trim(); if (!_val) { _$datalist.replaceChildren(); return; } _autocompleteTimer = setTimeout(async () => { try { const _res = await fetch( \`\${_micropubHref}?q=category&filter=\${encodeURIComponent(_val)}\` ); if (!_res.ok) return; const _data = await _res.json(); _$datalist.replaceChildren( ...(_data.categories ?? []).map((_cat) => { const _opt = document.createElement("option"); _opt.value = _cat; return _opt; }) ); } catch {} }, 300); }); // Clear datalist after tag confirmed via comma (programmatic clear doesn't fire input) $tagInputInput.addEventListener("keydown", (_e) => { if (_e.code === "Comma") _$datalist.replaceChildren(); }); // Clear datalist after tag confirmed via blur $tagInputInput.addEventListener("blur", () => { _$datalist.replaceChildren(); }); return tagInput;`; async function exists(p) { try { await access(p); return true; } catch { return false; } } let totalPatched = 0; let totalChecked = 0; for (const filePath of candidates) { if (!(await exists(filePath))) continue; totalChecked++; const source = await readFile(filePath, "utf8"); if (source.includes(MARKER)) { console.log(`[postinstall] patch-tag-input-autocomplete: already applied to ${filePath}`); continue; } if (!source.includes(OLD_SNIPPET)) { console.warn(`[postinstall] patch-tag-input-autocomplete: snippet not found in ${filePath} (upstream changed?)`); continue; } const updated = source.replace(OLD_SNIPPET, NEW_SNIPPET); await writeFile(filePath, updated, "utf8"); console.log(`[postinstall] Applied patch-tag-input-autocomplete to ${filePath}`); totalPatched++; } if (totalChecked === 0) { console.log("[postinstall] patch-tag-input-autocomplete: no target files found"); } else if (totalPatched === 0) { console.log("[postinstall] patch-tag-input-autocomplete: already up to date"); } else { console.log(`[postinstall] patch-tag-input-autocomplete: patched ${totalPatched} file(s)`); }