feat: patch tag-input component with datalist autocomplete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Patch: add datalist autocomplete to the <tag-input-field> web component.
|
||||
*
|
||||
* Fetches /micropub?q=category&filter=<value> as the user types (debounced
|
||||
* 300ms) and populates a native <datalist>. 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)`);
|
||||
}
|
||||
Reference in New Issue
Block a user