[object Object]

← 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 →