refactor(seltable): promote to shared/ with per-column autofilters
Move classifier/js/seltable.js → shared/seltable.js so both the classifier and the tables tool can use it (the MDL realignment splits work across both). Adds per-column autofilter inputs (one per column, AND-combined) on top of the programmatic global filter; selection + select-filtered + ctrl-Enter unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fd11278417
commit
93f1eb8d63
2 changed files with 41 additions and 21 deletions
|
|
@ -56,7 +56,7 @@ concat_files \
|
||||||
"js/classify.js" \
|
"js/classify.js" \
|
||||||
"js/workspace.js" \
|
"js/workspace.js" \
|
||||||
"js/dnd.js" \
|
"js/dnd.js" \
|
||||||
"js/seltable.js" \
|
"../shared/seltable.js" \
|
||||||
"js/validator.js" \
|
"js/validator.js" \
|
||||||
"js/scanner.js" \
|
"js/scanner.js" \
|
||||||
"js/tree.js" \
|
"js/tree.js" \
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* ZDDC Classifier — shared selectable + autofilter table.
|
* ZDDC — shared selectable + autofilter table (used by the classifier catalog
|
||||||
|
* and the tables tool's "Add from archive").
|
||||||
*
|
*
|
||||||
* A flat table with one global autofilter (AND of space-separated terms over
|
* A flat table with PER-COLUMN autofilters (one input per column, AND-combined,
|
||||||
* every column) and powerful selection for building complex sets quickly:
|
* each an AND of space-separated terms) plus an optional programmatic global
|
||||||
|
* filter, and powerful selection for building complex sets quickly:
|
||||||
* click replace selection + set anchor
|
* click replace selection + set anchor
|
||||||
* ctrl/cmd-click toggle one row
|
* ctrl/cmd-click toggle one row
|
||||||
* shift-click range from the anchor (replaces the selection)
|
* shift-click range from the anchor (replaces the selection)
|
||||||
* ctrl-shift-click ADD the anchor→row range to the existing selection
|
* ctrl-shift-click ADD the anchor→row range to the existing selection
|
||||||
* ctrl/cmd-Enter fire onActivate(selectedIds) — a bulk action
|
* ctrl/cmd-Enter fire onActivate(selectedIds) — a bulk action
|
||||||
* Esc clear
|
* Esc clear
|
||||||
* Ranges run over the CURRENTLY FILTERED order, so "filter to a transmittal,
|
* Ranges run over the CURRENTLY FILTERED order. Selection is keyed by a stable
|
||||||
* then shift-select the visible block" works. Selection is keyed by a stable
|
|
||||||
* rowId so it survives filtering and re-render.
|
* rowId so it survives filtering and re-render.
|
||||||
*
|
|
||||||
* Used by the MDL instantiate flow (Phase 1) and the By-MDL drop-target table
|
|
||||||
* (Phase 2).
|
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -41,17 +39,28 @@
|
||||||
var getRows = (typeof opts.rows === 'function') ? opts.rows : function () { return opts.rows || []; };
|
var getRows = (typeof opts.rows === 'function') ? opts.rows : function () { return opts.rows || []; };
|
||||||
var selected = Object.create(null); // id -> true
|
var selected = Object.create(null); // id -> true
|
||||||
var anchorId = null;
|
var anchorId = null;
|
||||||
var ft = []; // global filter terms
|
var globalTerms = []; // programmatic global filter (tests/reveal)
|
||||||
|
var colFilters = Object.create(null); // colKey -> terms[] (the per-column autofilters)
|
||||||
|
|
||||||
function rows() { return getRows() || []; }
|
function rows() { return getRows() || []; }
|
||||||
|
function colByKey(k) { for (var i = 0; i < columns.length; i++) { if (columns[i].key === k) return columns[i]; } return null; }
|
||||||
function colVal(col, row) { return col.get ? col.get(row) : (row[col.key] == null ? '' : row[col.key]); }
|
function colVal(col, row) { return col.get ? col.get(row) : (row[col.key] == null ? '' : row[col.key]); }
|
||||||
function rowBlob(row) { var s = ''; for (var i = 0; i < columns.length; i++) { s += colVal(columns[i], row) + ' '; } return s; }
|
function rowBlob(row) { var s = ''; for (var i = 0; i < columns.length; i++) { s += colVal(columns[i], row) + ' '; } return s; }
|
||||||
function filtered() { return ft.length ? rows().filter(function (r) { return hit(rowBlob(r), ft); }) : rows().slice(); }
|
function rowMatches(row) {
|
||||||
|
if (globalTerms.length && !hit(rowBlob(row), globalTerms)) return false;
|
||||||
|
for (var k in colFilters) {
|
||||||
|
var col = colByKey(k);
|
||||||
|
if (col && !hit(colVal(col, row), colFilters[k])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function filtered() { return rows().filter(rowMatches); }
|
||||||
|
|
||||||
function getSelection() { return Object.keys(selected); }
|
function getSelection() { return Object.keys(selected); }
|
||||||
function getFilteredRows() { return filtered(); }
|
function getFilteredRows() { return filtered(); }
|
||||||
function fireSel() { if (opts.onSelectionChange) opts.onSelectionChange(getSelection()); }
|
function fireSel() { if (opts.onSelectionChange) opts.onSelectionChange(getSelection()); }
|
||||||
function setFilter(q) { ft = terms(q); renderBody(); }
|
function setFilter(q) { globalTerms = terms(q); renderBody(); }
|
||||||
|
function setColFilter(colKey, q) { var t = terms(q); if (t.length) colFilters[colKey] = t; else delete colFilters[colKey]; renderBody(); }
|
||||||
function selectAllFiltered() { filtered().forEach(function (r) { selected[rowId(r)] = true; }); anchorId = null; renderBody(); fireSel(); }
|
function selectAllFiltered() { filtered().forEach(function (r) { selected[rowId(r)] = true; }); anchorId = null; renderBody(); fireSel(); }
|
||||||
function clearSel() { selected = Object.create(null); anchorId = null; renderBody(); fireSel(); }
|
function clearSel() { selected = Object.create(null); anchorId = null; renderBody(); fireSel(); }
|
||||||
|
|
||||||
|
|
@ -75,15 +84,12 @@
|
||||||
container.textContent = '';
|
container.textContent = '';
|
||||||
container.classList.add('seltable');
|
container.classList.add('seltable');
|
||||||
var bar = elt('div', 'seltable__bar');
|
var bar = elt('div', 'seltable__bar');
|
||||||
var filterEl = elt('input', 'seltable__filter'); filterEl.type = 'search';
|
|
||||||
filterEl.placeholder = opts.filterPlaceholder || 'Filter…'; filterEl.spellcheck = false;
|
|
||||||
filterEl.addEventListener('input', function () { setFilter(this.value); });
|
|
||||||
var allBtn = elt('button', 'btn btn-sm btn-secondary', 'Select filtered');
|
var allBtn = elt('button', 'btn btn-sm btn-secondary', 'Select filtered');
|
||||||
allBtn.addEventListener('click', selectAllFiltered);
|
allBtn.addEventListener('click', selectAllFiltered);
|
||||||
var clrBtn = elt('button', 'btn btn-sm btn-secondary', 'Clear');
|
var clrBtn = elt('button', 'btn btn-sm btn-secondary', 'Clear');
|
||||||
clrBtn.addEventListener('click', clearSel);
|
clrBtn.addEventListener('click', clearSel);
|
||||||
countEl = elt('span', 'seltable__count');
|
countEl = elt('span', 'seltable__count');
|
||||||
bar.appendChild(filterEl); bar.appendChild(allBtn); bar.appendChild(clrBtn); bar.appendChild(countEl);
|
bar.appendChild(allBtn); bar.appendChild(clrBtn); bar.appendChild(countEl);
|
||||||
container.appendChild(bar);
|
container.appendChild(bar);
|
||||||
|
|
||||||
var scroll = elt('div', 'seltable__scroll');
|
var scroll = elt('div', 'seltable__scroll');
|
||||||
|
|
@ -91,7 +97,22 @@
|
||||||
var thead = elt('thead'), htr = elt('tr');
|
var thead = elt('thead'), htr = elt('tr');
|
||||||
columns.forEach(function (c) { htr.appendChild(elt('th', c.cls || null, c.title || c.key)); });
|
columns.forEach(function (c) { htr.appendChild(elt('th', c.cls || null, c.title || c.key)); });
|
||||||
if (opts.rowExtra) htr.appendChild(elt('th', 'seltable__extrah', opts.extraTitle || ''));
|
if (opts.rowExtra) htr.appendChild(elt('th', 'seltable__extrah', opts.extraTitle || ''));
|
||||||
thead.appendChild(htr); table.appendChild(thead);
|
thead.appendChild(htr);
|
||||||
|
// Per-column autofilter row.
|
||||||
|
var ftr = elt('tr', 'seltable__filters');
|
||||||
|
columns.forEach(function (c) {
|
||||||
|
var th = elt('th');
|
||||||
|
if (c.filterable !== false) {
|
||||||
|
var inp = elt('input', 'seltable__colfilter'); inp.type = 'search'; inp.placeholder = 'filter…'; inp.spellcheck = false;
|
||||||
|
inp.setAttribute('data-no-select', '');
|
||||||
|
inp.addEventListener('input', function () { setColFilter(c.key, this.value); });
|
||||||
|
th.appendChild(inp);
|
||||||
|
}
|
||||||
|
ftr.appendChild(th);
|
||||||
|
});
|
||||||
|
if (opts.rowExtra) ftr.appendChild(elt('th'));
|
||||||
|
thead.appendChild(ftr);
|
||||||
|
table.appendChild(thead);
|
||||||
bodyEl = elt('tbody'); table.appendChild(bodyEl);
|
bodyEl = elt('tbody'); table.appendChild(bodyEl);
|
||||||
scroll.appendChild(table); container.appendChild(scroll);
|
scroll.appendChild(table); container.appendChild(scroll);
|
||||||
|
|
||||||
|
|
@ -111,7 +132,7 @@
|
||||||
var tr = elt('tr', 'seltable__row' + (selected[id] ? ' is-selected' : ''));
|
var tr = elt('tr', 'seltable__row' + (selected[id] ? ' is-selected' : ''));
|
||||||
tr.dataset.id = id;
|
tr.dataset.id = id;
|
||||||
tr.addEventListener('click', function (e) {
|
tr.addEventListener('click', function (e) {
|
||||||
if (e.target.closest('input,button,select,a,[data-no-select]')) return; // let controls work
|
if (e.target.closest('input,button,select,a,[data-no-select]')) return;
|
||||||
onRowClick(e, row, fr);
|
onRowClick(e, row, fr);
|
||||||
});
|
});
|
||||||
if (opts.onRowDrop) {
|
if (opts.onRowDrop) {
|
||||||
|
|
@ -137,15 +158,14 @@
|
||||||
});
|
});
|
||||||
if (countEl) {
|
if (countEl) {
|
||||||
var nSel = getSelection().length;
|
var nSel = getSelection().length;
|
||||||
countEl.textContent = fr.length + ' shown' + (nSel ? (' · ' + nSel + ' selected') : '');
|
countEl.textContent = fr.length + ' shown' + (nSel ? ' · ' + nSel + ' selected' : '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
render: render, renderBody: renderBody,
|
render: render, renderBody: renderBody,
|
||||||
getSelection: getSelection, getFilteredRows: getFilteredRows,
|
getSelection: getSelection, getFilteredRows: getFilteredRows,
|
||||||
setFilter: setFilter, selectAllFiltered: selectAllFiltered, clear: clearSel,
|
setFilter: setFilter, setColFilter: setColFilter, selectAllFiltered: selectAllFiltered, clear: clearSel,
|
||||||
// test seam: simulate a row click with modifier keys.
|
|
||||||
clickRow: function (id, mods) {
|
clickRow: function (id, mods) {
|
||||||
var fr = filtered();
|
var fr = filtered();
|
||||||
var row = fr.filter(function (r) { return String(rowId(r)) === String(id); })[0];
|
var row = fr.filter(function (r) { return String(rowId(r)) === String(id); })[0];
|
||||||
Loading…
Reference in a new issue