← back to La Socrata Ingester
Viewer: per-column filter inputs in the List header
48e81f352ceec741e0c31c54230fff17e3efdd1b · 2026-08-12 11:51:21 -0700 · Steve Abrams
- A sticky filter row under the column headers: each filterable column gets an
input wired to the matching server filter param (address->q contains, zip/cd/
work_type/building_type/status/era->exact with facet-fed <datalist> autocomplete,
issued->since date, project_value->min_value). Reuses state.filters + qs(), so it
composes with facets, deep-links, and saved views for free.
- Focus + caret preserved across the reload-triggered re-render; address input
stays in sync with the top search box; inputs only appear for visible columns.
Author: Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M viewer/public/index.html
Diff
commit 48e81f352ceec741e0c31c54230fff17e3efdd1b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 12 11:51:21 2026 -0700
Viewer: per-column filter inputs in the List header
- A sticky filter row under the column headers: each filterable column gets an
input wired to the matching server filter param (address->q contains, zip/cd/
work_type/building_type/status/era->exact with facet-fed <datalist> autocomplete,
issued->since date, project_value->min_value). Reuses state.filters + qs(), so it
composes with facets, deep-links, and saved views for free.
- Focus + caret preserved across the reload-triggered re-render; address input
stays in sync with the top search box; inputs only appear for visible columns.
Author: Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
viewer/public/index.html | 52 +++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 51 insertions(+), 1 deletion(-)
diff --git a/viewer/public/index.html b/viewer/public/index.html
index f34b1c2..936b821 100644
--- a/viewer/public/index.html
+++ b/viewer/public/index.html
@@ -58,6 +58,13 @@
thead th.drag{opacity:.4}
thead th .arrow{color:var(--teal);margin-left:3px}
thead th .grip{color:#b7bfc7;margin-right:5px;cursor:grab}
+ .filterrow th{position:sticky;top:28px;z-index:1;background:#f6f7f9;border-bottom:1px solid var(--line);
+ padding:3px 6px;cursor:default}
+ .filterrow th:hover{background:#f6f7f9}
+ .filterrow input{width:100%;min-width:52px;padding:3px 6px;font-size:11px;border:1px solid var(--line);
+ border-radius:5px;background:#fff;color:var(--ink)}
+ .filterrow input:focus{outline:none;border-color:var(--teal)}
+ .filterrow th.nofilter{background:#f6f7f9}
tbody td{padding:var(--pad);border-bottom:1px solid var(--line);white-space:nowrap;height:var(--rowh);
max-width:340px;overflow:hidden;text-overflow:ellipsis}
tbody tr:hover{background:#f0f9f8;cursor:pointer}
@@ -162,7 +169,8 @@
<main>
<div id="overlay" class="overlay hidden"></div>
<div id="listview">
- <table id="tbl"><thead><tr id="head"></tr></thead><tbody id="rows"></tbody></table>
+ <table id="tbl"><thead><tr id="head"></tr><tr id="filterrow" class="filterrow"></tr></thead><tbody id="rows"></tbody></table>
+ <div id="dls" hidden></div>
<button class="loadmore" id="more" style="display:none">Load more</button>
</div>
<div id="gridview" class="hidden">
@@ -270,6 +278,7 @@ function qs(extra={}){
async function loadFacets(){
const d = await (await fetch(api('/api/facets?'+qs()))).json();
+ facetData = d; renderDatalists(); // feed the per-column filter datalists
const rail = $('#rail'); const collapsed = LS.get('collapsed',{});
rail.innerHTML = FACETS.map(([key,label])=>{
const items=(d[key]||[]).map(o=>{
@@ -324,6 +333,47 @@ function renderHead(){
th.ondragover=e=>e.preventDefault();
th.ondrop=e=>{e.preventDefault();dropCol(th.dataset.k);};
});
+ renderFilterRow();
+}
+// per-column filters map each column to a server filter param (reuses state.filters + qs())
+const COLFILTER = {
+ issued: {param:'since', type:'date', ph:'on/after'},
+ address: {param:'q', type:'search', ph:'contains…'},
+ zip: {param:'zip', type:'text', ph:'ZIP', list:'zip'},
+ cd: {param:'cd', type:'text', ph:'CD', list:'cd'},
+ work_type: {param:'work_type', type:'text', ph:'any', list:'work_type'},
+ building_type: {param:'building_type',type:'text', ph:'any', list:'building_type'},
+ status: {param:'status', type:'text', ph:'any', list:'status'},
+ era: {param:'era', type:'text', ph:'any', list:'era'},
+ project_value: {param:'min_value', type:'number', ph:'≥ $'},
+};
+let facetData = {};
+function renderFilterRow(){
+ const fr=$('#filterrow'); if(!fr) return;
+ const act=document.activeElement, actP=(act&&act.dataset&&act.dataset.p)?act.dataset.p:null;
+ const selPos = actP ? act.selectionStart : null;
+ fr.innerHTML = cols().map(c=>{
+ const f=COLFILTER[c.key];
+ if(!f) return `<th class="nofilter"></th>`;
+ const v = state.filters[f.param]!=null ? escapeAttr(state.filters[f.param]) : '';
+ return `<th><input data-p="${f.param}" type="${f.type}" placeholder="${f.ph}" value="${v}"${f.list?` list="dl_${f.param}"`:''}></th>`;
+ }).join('');
+ fr.querySelectorAll('input').forEach(inp=>{
+ const debounce = (inp.type==='date') ? 0 : 300;
+ inp.oninput=()=>{ clearTimeout(inp._t); inp._t=setTimeout(()=>{
+ const p=inp.dataset.p, v=inp.value.trim();
+ if(v) state.filters[p]=v; else delete state.filters[p];
+ if(p==='q') $('#q').value=v; // keep top search box in sync
+ reload();
+ }, debounce); };
+ });
+ if(actP){ const el=fr.querySelector(`input[data-p="${actP}"]`); if(el){ el.focus(); try{ el.setSelectionRange(selPos,selPos); }catch(e){} } }
+ renderDatalists();
+}
+function renderDatalists(){
+ const h=$('#dls'); if(!h) return;
+ const mk=(param,key)=>`<datalist id="dl_${param}">`+((facetData[key]||[]).map(o=>`<option value="${escapeAttr(o.v)}">`).join(''))+`</datalist>`;
+ h.innerHTML = mk('zip','zip')+mk('cd','cd')+mk('work_type','work_type')+mk('building_type','building_type')+mk('status','status')+mk('era','era');
}
let drag=null;
function dropCol(target){ if(!drag||drag===target)return; const a=colOrder.indexOf(drag),b=colOrder.indexOf(target);
← bcf4edb Viewer: saved views — named snapshots of the full view state
·
back to La Socrata Ingester
·
Viewer: CD/ZIP quick-jump control on the Map view e6f0356 →