diff --git a/classifier/build.sh b/classifier/build.sh index 52d8df0..1791c2a 100755 --- a/classifier/build.sh +++ b/classifier/build.sh @@ -56,7 +56,7 @@ concat_files \ "js/classify.js" \ "js/workspace.js" \ "js/dnd.js" \ - "js/seltable.js" \ + "../shared/seltable.js" \ "js/validator.js" \ "js/scanner.js" \ "js/tree.js" \ diff --git a/classifier/js/seltable.js b/shared/seltable.js similarity index 74% rename from classifier/js/seltable.js rename to shared/seltable.js index 9ff029f..378e55c 100644 --- a/classifier/js/seltable.js +++ b/shared/seltable.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 - * every column) and powerful selection for building complex sets quickly: + * A flat table with PER-COLUMN autofilters (one input per column, AND-combined, + * 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 * ctrl/cmd-click toggle one row * shift-click range from the anchor (replaces the selection) * ctrl-shift-click ADD the anchor→row range to the existing selection * ctrl/cmd-Enter fire onActivate(selectedIds) — a bulk action * Esc clear - * Ranges run over the CURRENTLY FILTERED order, so "filter to a transmittal, - * then shift-select the visible block" works. Selection is keyed by a stable + * Ranges run over the CURRENTLY FILTERED order. Selection is keyed by a stable * 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 () { 'use strict'; @@ -41,17 +39,28 @@ var getRows = (typeof opts.rows === 'function') ? opts.rows : function () { return opts.rows || []; }; var selected = Object.create(null); // id -> true 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 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 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 getFilteredRows() { return filtered(); } 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 clearSel() { selected = Object.create(null); anchorId = null; renderBody(); fireSel(); } @@ -75,15 +84,12 @@ container.textContent = ''; container.classList.add('seltable'); 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'); allBtn.addEventListener('click', selectAllFiltered); var clrBtn = elt('button', 'btn btn-sm btn-secondary', 'Clear'); clrBtn.addEventListener('click', clearSel); 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); var scroll = elt('div', 'seltable__scroll'); @@ -91,7 +97,22 @@ var thead = elt('thead'), htr = elt('tr'); 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 || '')); - 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); scroll.appendChild(table); container.appendChild(scroll); @@ -111,7 +132,7 @@ var tr = elt('tr', 'seltable__row' + (selected[id] ? ' is-selected' : '')); tr.dataset.id = id; 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); }); if (opts.onRowDrop) { @@ -137,15 +158,14 @@ }); if (countEl) { var nSel = getSelection().length; - countEl.textContent = fr.length + ' shown' + (nSel ? (' · ' + nSel + ' selected') : ''); + countEl.textContent = fr.length + ' shown' + (nSel ? ' · ' + nSel + ' selected' : ''); } } return { render: render, renderBody: renderBody, getSelection: getSelection, getFilteredRows: getFilteredRows, - setFilter: setFilter, selectAllFiltered: selectAllFiltered, clear: clearSel, - // test seam: simulate a row click with modifier keys. + setFilter: setFilter, setColFilter: setColFilter, selectAllFiltered: selectAllFiltered, clear: clearSel, clickRow: function (id, mods) { var fr = filtered(); var row = fr.filter(function (r) { return String(rowId(r)) === String(id); })[0];