← back to Microsite Engine
microsite-engine: prefilled dropdowns + typed autocomplete on niche fields
ed286d5d3f7ea46c3bcad16030cac82b2b48b429 · 2026-07-17 09:12:23 -0700 · Steve
- keyword include/exclude inputs get token-aware <datalist> autocomplete,
prefilled from the real catalog vocabulary (all tags ∪ product types)
- product-types field gains a prefilled dropdown alongside the chips
- all sourced from /api/facets already fetched on boot ($0, no new requests)
Files touched
Diff
commit ed286d5d3f7ea46c3bcad16030cac82b2b48b429
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Jul 17 09:12:23 2026 -0700
microsite-engine: prefilled dropdowns + typed autocomplete on niche fields
- keyword include/exclude inputs get token-aware <datalist> autocomplete,
prefilled from the real catalog vocabulary (all tags ∪ product types)
- product-types field gains a prefilled dropdown alongside the chips
- all sourced from /api/facets already fetched on boot ($0, no new requests)
---
public/index.html | 48 ++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 46 insertions(+), 2 deletions(-)
diff --git a/public/index.html b/public/index.html
index f28ede8..cbce4b0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -92,11 +92,14 @@
<div class="section-title">Niche — which products this site carries</div>
<label>Include keywords <span class="note">(any-match on title+tags+type)</span></label>
- <input id="pos" placeholder="cork, natural" autocomplete="off">
+ <input id="pos" placeholder="type to search the catalog… cork, natural" autocomplete="off" list="posList">
+ <datalist id="posList"></datalist>
<div class="chips" id="tagChips"></div>
<label>Exclude keywords</label>
- <input id="neg" placeholder="commercial" autocomplete="off">
+ <input id="neg" placeholder="type to search… commercial" autocomplete="off" list="negList">
+ <datalist id="negList"></datalist>
<label>Product types</label>
+ <select id="typeAdd"><option value="">+ add a product type…</option></select>
<div class="chips" id="typeChips"></div>
<div class="section-title">Look</div>
@@ -191,9 +194,50 @@ async function boot(){
else{cur.push(c.dataset.tag);c.classList.add('on');}
$('pos').value=cur.join(', '); doPreview();
});
+ // prefilled dropdown of every product type (complements the chips above)
+ $('typeAdd').innerHTML = '<option value="">+ add a product type…</option>' +
+ FACETS.typeList.map(t=>`<option value="${esc(t.name)}">${esc(t.name)} · ${t.n.toLocaleString()}</option>`).join('');
+ $('typeAdd').onchange = ()=>{
+ const t=$('typeAdd').value; $('typeAdd').value='';
+ if(!t || selTypes.has(t)) return;
+ selTypes.add(t);
+ const chip=[...document.querySelectorAll('#typeChips .chip')].find(c=>c.dataset.type===t);
+ if(chip) chip.classList.add('on');
+ doPreview();
+ };
+ // typed-suggestion autocomplete for the include/exclude keyword fields,
+ // prefilled from the REAL catalog vocabulary (all tags ∪ all product types)
+ const VOCAB=[...new Set([...FACETS.tagList.map(t=>t.name), ...FACETS.typeList.map(t=>t.name)])];
+ wireTokenDatalist($('pos'), $('posList'), VOCAB);
+ wireTokenDatalist($('neg'), $('negList'), VOCAB);
loadDrafts(); doPreview();
}
+// Token-aware datalist: the keyword fields are comma-separated, but a native
+// <datalist> matches against the WHOLE input value. So on each keystroke we
+// rebuild the options as "<already-typed tokens>, <suggestion>" and prefix-match
+// the trailing token — giving real autocomplete on a multi-value field, and
+// hiding tokens you've already picked. Prefix-match is the safe common
+// denominator across Chrome/Safari datalist filtering.
+function wireTokenDatalist(input, list, vocab){
+ function refresh(){
+ const val = input.value;
+ const cut = val.lastIndexOf(',');
+ const prefix = cut >= 0 ? val.slice(0, cut+1) : '';
+ const typed = val.slice(cut+1).trim().toLowerCase();
+ const chosen = new Set(val.slice(0, cut>=0?cut:0).split(',').map(s=>s.trim().toLowerCase()).filter(Boolean));
+ const spacer = cut >= 0 ? ' ' : '';
+ list.innerHTML = vocab
+ .filter(v => !chosen.has(v.toLowerCase()))
+ .filter(v => !typed || v.toLowerCase().startsWith(typed))
+ .slice(0, 50)
+ .map(v => `<option value="${esc(prefix+spacer+v)}"></option>`).join('');
+ }
+ input.addEventListener('input', refresh);
+ input.addEventListener('focus', refresh);
+ refresh();
+}
+
let pvTimer;
function doPreviewDebounced(){ clearTimeout(pvTimer); pvTimer=setTimeout(doPreview,250); }
async function doPreview(){
← 0714e9f chore: lint, refactor, harden (path-traversal guard + readBo
·
back to Microsite Engine
·
auto-save: 2026-07-17T09:16:00 (2 files) — public/index.html 92c9d7e →