diff --git a/scripts/patch-tag-input-autocomplete.mjs b/scripts/patch-tag-input-autocomplete.mjs new file mode 100644 index 00000000..22e81b01 --- /dev/null +++ b/scripts/patch-tag-input-autocomplete.mjs @@ -0,0 +1,121 @@ +/** + * 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)`); +}