chore(embedded): cut v0.0.27-beta
This commit is contained in:
parent
be5b3967ba
commit
8f4e28f86a
7 changed files with 342 additions and 119 deletions
|
|
@ -2717,7 +2717,7 @@ td[data-field="trackingNumber"] {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Archive</span>
|
<span class="app-header__title">ZDDC Archive</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:31:21 · be5b396</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Browse</span>
|
<span class="app-header__title">ZDDC Browse</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:31:22 · be5b396</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing">⟳</button>
|
||||||
|
|
|
||||||
|
|
@ -1183,6 +1183,16 @@ body.is-elevated::after {
|
||||||
color: var(--text-muted); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
|
color: var(--text-muted); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.seltable__table thead tr.seltable__filters th { top: 1.55rem; padding: 0.15rem 0.35rem; }
|
.seltable__table thead tr.seltable__filters th { top: 1.55rem; padding: 0.15rem 0.35rem; }
|
||||||
|
/* Sortable headers + multi-sort indicator. The title th is position:sticky
|
||||||
|
(a positioning context) so the drag-resizer can absolutely anchor to its edge. */
|
||||||
|
.seltable__th--sortable { cursor: pointer; }
|
||||||
|
.seltable__th--sortable:hover { color: var(--text); }
|
||||||
|
.seltable__sortind { color: var(--primary); font-size: 0.7em; font-weight: 700; }
|
||||||
|
.seltable__resizer {
|
||||||
|
position: absolute; top: 0; right: 0; bottom: 0; width: 6px;
|
||||||
|
cursor: col-resize; user-select: none; touch-action: none;
|
||||||
|
}
|
||||||
|
.seltable__resizer:hover { background: var(--primary); opacity: 0.4; }
|
||||||
.seltable__colfilter {
|
.seltable__colfilter {
|
||||||
width: 100%; min-width: 2rem; box-sizing: border-box; padding: 0.15rem 0.35rem;
|
width: 100%; min-width: 2rem; box-sizing: border-box; padding: 0.15rem 0.35rem;
|
||||||
border: 1px solid var(--border); border-radius: var(--radius);
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
|
@ -1403,11 +1413,12 @@ body.is-elevated::after {
|
||||||
.folder-item {
|
.folder-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start; /* toggle/icon sit on the name line; count drops below */
|
align-items: flex-start; /* toggle/icon sit on the name line; count drops below */
|
||||||
padding: 0.5rem;
|
padding: 0.1rem 0.5rem; /* tight: the name + count are already stacked */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: background-color 0.15s;
|
transition: background-color 0.15s;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.folder-item:hover {
|
.folder-item:hover {
|
||||||
|
|
@ -1895,7 +1906,8 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
||||||
.dir-picker__err { color: var(--danger); font-size: 0.78rem; margin-left: 1.1rem; }
|
.dir-picker__err { color: var(--danger); font-size: 0.78rem; margin-left: 1.1rem; }
|
||||||
|
|
||||||
/* ── By-tracking merged-cell table ──────────────────────────────────────── */
|
/* ── By-tracking merged-cell table ──────────────────────────────────────── */
|
||||||
#trackingTree { padding: 0; } /* table reaches the edges; cells carry padding */
|
#trackingTree { padding: 0; min-height: 0; } /* seltable owns its own scroll + padding */
|
||||||
|
#trackingTree .seltable { height: 100%; }
|
||||||
.ttable { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 0.82rem; }
|
.ttable { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 0.82rem; }
|
||||||
.ttable th, .ttable td {
|
.ttable th, .ttable td {
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
|
|
@ -1950,10 +1962,8 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
||||||
.tfile__badge--ok { color: var(--success, #16a34a); }
|
.tfile__badge--ok { color: var(--success, #16a34a); }
|
||||||
.tfile__badge--err { color: var(--danger); }
|
.tfile__badge--err { color: var(--danger); }
|
||||||
|
|
||||||
/* ── By-tracking flat editable grid (one row per file) ──────────────────── */
|
/* ── By-tracking flat editable grid (one row per file) — now on seltable, so
|
||||||
.ttable--grid { width: auto; }
|
the cell classes (.tg-*) ride seltable's th/td; seltable owns layout. ── */
|
||||||
.ttable--grid td.tg-td { padding: 0.1rem 0.35rem; vertical-align: middle; }
|
|
||||||
.ttable--grid th.tg-th { white-space: nowrap; } /* .column-resizer (spreadsheet.css) sits in the sticky th */
|
|
||||||
.tg-input {
|
.tg-input {
|
||||||
width: 100%; min-width: 4rem; box-sizing: border-box;
|
width: 100%; min-width: 4rem; box-sizing: border-box;
|
||||||
padding: 0.12rem 0.3rem; border: 1px solid transparent; border-radius: var(--radius);
|
padding: 0.12rem 0.3rem; border: 1px solid transparent; border-radius: var(--radius);
|
||||||
|
|
@ -1967,8 +1977,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
||||||
.tg-orig__link:hover { text-decoration: underline; }
|
.tg-orig__link:hover { text-decoration: underline; }
|
||||||
.tg-status, .tg-x { text-align: center; }
|
.tg-status, .tg-x { text-align: center; }
|
||||||
.tg-x__btn { opacity: 0.5; }
|
.tg-x__btn { opacity: 0.5; }
|
||||||
.tg-row:hover .tg-x__btn { opacity: 1; }
|
.seltable__row:hover .tg-x__btn { opacity: 1; }
|
||||||
.tg-row--err .tg-status { color: var(--danger); }
|
|
||||||
.tg-drop-hover { outline: 2px dashed var(--primary); outline-offset: -3px; background: var(--primary-light); }
|
.tg-drop-hover { outline: 2px dashed var(--primary); outline-offset: -3px; background: var(--primary-light); }
|
||||||
|
|
||||||
/* "Columns ▾" chooser menu */
|
/* "Columns ▾" chooser menu */
|
||||||
|
|
@ -2465,7 +2474,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Classifier</span>
|
<span class="app-header__title">ZDDC Classifier</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:31:21 · be5b396</span></span>
|
||||||
</div>
|
</div>
|
||||||
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
|
||||||
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;">⟳</button>
|
||||||
|
|
@ -8833,20 +8842,26 @@ X.B(E,Y);return E}return J}())
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ZDDC — shared selectable + autofilter table (used by the classifier catalog
|
* ZDDC — shared selectable + autofilter table (used by the classifier worklist +
|
||||||
* and the tables tool's "Add from archive").
|
* By-tracking grid, and the tables tool's "Add from archive").
|
||||||
*
|
*
|
||||||
* A flat table with PER-COLUMN autofilters (one input per column, AND-combined,
|
* 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
|
* MULTI-COLUMN SORT (click a header to sort; shift/ctrl-click adds a secondary
|
||||||
* filter, and powerful selection for building complex sets quickly:
|
* key), RESIZABLE columns (drag the header edge), an optional programmatic global
|
||||||
|
* filter, and powerful selection:
|
||||||
* 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. Selection is keyed by a stable
|
* Ranges run over the CURRENTLY FILTERED + SORTED order. Selection is keyed by a
|
||||||
* rowId so it survives filtering and re-render.
|
* stable rowId so it survives filtering, sorting, and re-render. Pass
|
||||||
|
* opts.persistKey to remember column widths + sort across reloads (localStorage).
|
||||||
|
*
|
||||||
|
* Column config: { key, title, cls?, get?(row), render?(row, td), filterable?,
|
||||||
|
* sortable?, sortValue?(row) }. sortable/filterable default true; set false for
|
||||||
|
* render-only columns (a remove button, a derived badge).
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -8865,6 +8880,12 @@ X.B(E,Y);return E}return J}())
|
||||||
if (text != null) e.textContent = text;
|
if (text != null) e.textContent = text;
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
function loadPersist(key) { try { return JSON.parse(localStorage.getItem(key)) || {}; } catch (_) { return {}; } }
|
||||||
|
function savePersist(key, patch) {
|
||||||
|
var cur = loadPersist(key);
|
||||||
|
for (var k in patch) cur[k] = patch[k];
|
||||||
|
try { localStorage.setItem(key, JSON.stringify(cur)); } catch (_) { /* private mode */ }
|
||||||
|
}
|
||||||
|
|
||||||
function create(opts) {
|
function create(opts) {
|
||||||
var container = opts.container;
|
var container = opts.container;
|
||||||
|
|
@ -8876,6 +8897,12 @@ X.B(E,Y);return E}return J}())
|
||||||
var globalTerms = []; // programmatic global filter (tests/reveal)
|
var globalTerms = []; // programmatic global filter (tests/reveal)
|
||||||
var colFilters = Object.create(null); // colKey -> terms[] (the per-column autofilters)
|
var colFilters = Object.create(null); // colKey -> terms[] (the per-column autofilters)
|
||||||
|
|
||||||
|
var persistKey = opts.persistKey || null;
|
||||||
|
var saved = persistKey ? loadPersist(persistKey) : {};
|
||||||
|
var sortState = Array.isArray(saved.sort) ? saved.sort.slice() : []; // [{ key, dir }]
|
||||||
|
var colWidths = saved.widths || {}; // colKey -> px
|
||||||
|
var headEls = Object.create(null); // colKey -> { th, ind }
|
||||||
|
|
||||||
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 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]); }
|
||||||
|
|
@ -8890,6 +8917,59 @@ X.B(E,Y);return E}return J}())
|
||||||
}
|
}
|
||||||
function filtered() { return rows().filter(rowMatches); }
|
function filtered() { return rows().filter(rowMatches); }
|
||||||
|
|
||||||
|
// ── multi-column sort ──────────────────────────────────────────────
|
||||||
|
function sortVal(col, row) { return col.sortValue ? col.sortValue(row) : colVal(col, row); }
|
||||||
|
function sorted(list) {
|
||||||
|
if (!sortState.length) return list;
|
||||||
|
var arr = list.slice();
|
||||||
|
arr.sort(function (a, b) {
|
||||||
|
for (var i = 0; i < sortState.length; i++) {
|
||||||
|
var s = sortState[i], col = colByKey(s.key); if (!col) continue;
|
||||||
|
var cmp = String(sortVal(col, a)).localeCompare(String(sortVal(col, b)), undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
if (cmp) return s.dir === 'desc' ? -cmp : cmp;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
function visibleRows() { return sorted(filtered()); }
|
||||||
|
function sortIdx(key) { for (var i = 0; i < sortState.length; i++) { if (sortState[i].key === key) return i; } return -1; }
|
||||||
|
function toggleSort(key, add) {
|
||||||
|
var i = sortIdx(key);
|
||||||
|
if (add) {
|
||||||
|
if (i >= 0) { if (sortState[i].dir === 'asc') sortState[i].dir = 'desc'; else sortState.splice(i, 1); }
|
||||||
|
else sortState.push({ key: key, dir: 'asc' });
|
||||||
|
} else {
|
||||||
|
if (i === 0 && sortState.length === 1) sortState = sortState[0].dir === 'asc' ? [{ key: key, dir: 'desc' }] : [];
|
||||||
|
else sortState = [{ key: key, dir: 'asc' }];
|
||||||
|
}
|
||||||
|
if (persistKey) savePersist(persistKey, { sort: sortState });
|
||||||
|
updateSortIndicators(); renderBody();
|
||||||
|
}
|
||||||
|
function updateSortIndicators() {
|
||||||
|
columns.forEach(function (c) {
|
||||||
|
var h = headEls[c.key]; if (!h) return;
|
||||||
|
var i = sortIdx(c.key);
|
||||||
|
h.ind.textContent = i >= 0 ? ((sortState[i].dir === 'asc' ? ' ▲' : ' ▼') + (sortState.length > 1 ? (i + 1) : '')) : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ── resizable columns ──────────────────────────────────────────────
|
||||||
|
function addResizer(th, key) {
|
||||||
|
var rz = elt('div', 'seltable__resizer');
|
||||||
|
rz.addEventListener('mousedown', function (e) {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
var startX = e.clientX, startW = th.offsetWidth;
|
||||||
|
function mm(ev) { var w = Math.max(40, startW + (ev.clientX - startX)); th.style.width = th.style.minWidth = th.style.maxWidth = w + 'px'; }
|
||||||
|
function mu() {
|
||||||
|
document.removeEventListener('mousemove', mm); document.removeEventListener('mouseup', mu);
|
||||||
|
colWidths[key] = Math.round(th.offsetWidth);
|
||||||
|
if (persistKey) savePersist(persistKey, { widths: colWidths });
|
||||||
|
}
|
||||||
|
document.addEventListener('mousemove', mm); document.addEventListener('mouseup', mu);
|
||||||
|
});
|
||||||
|
th.appendChild(rz);
|
||||||
|
}
|
||||||
|
|
||||||
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()); }
|
||||||
|
|
@ -8929,7 +9009,23 @@ X.B(E,Y);return E}return J}())
|
||||||
var scroll = elt('div', 'seltable__scroll');
|
var scroll = elt('div', 'seltable__scroll');
|
||||||
var table = elt('table', 'seltable__table');
|
var table = elt('table', 'seltable__table');
|
||||||
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)); });
|
headEls = Object.create(null);
|
||||||
|
columns.forEach(function (c) {
|
||||||
|
var th = elt('th', c.cls || null);
|
||||||
|
th.appendChild(document.createTextNode(c.title || c.key));
|
||||||
|
var ind = elt('span', 'seltable__sortind'); th.appendChild(ind);
|
||||||
|
headEls[c.key] = { th: th, ind: ind };
|
||||||
|
if (colWidths[c.key]) { th.style.width = th.style.minWidth = th.style.maxWidth = colWidths[c.key] + 'px'; }
|
||||||
|
if (c.sortable !== false) {
|
||||||
|
th.classList.add('seltable__th--sortable');
|
||||||
|
th.addEventListener('click', function (e) {
|
||||||
|
if (e.target.classList.contains('seltable__resizer')) return;
|
||||||
|
toggleSort(c.key, e.shiftKey || e.ctrlKey || e.metaKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addResizer(th, c.key);
|
||||||
|
htr.appendChild(th);
|
||||||
|
});
|
||||||
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);
|
thead.appendChild(htr);
|
||||||
// Per-column autofilter row.
|
// Per-column autofilter row.
|
||||||
|
|
@ -8939,6 +9035,7 @@ X.B(E,Y);return E}return J}())
|
||||||
if (c.filterable !== false) {
|
if (c.filterable !== false) {
|
||||||
var inp = elt('input', 'seltable__colfilter'); inp.type = 'search'; inp.placeholder = 'filter…'; inp.spellcheck = false;
|
var inp = elt('input', 'seltable__colfilter'); inp.type = 'search'; inp.placeholder = 'filter…'; inp.spellcheck = false;
|
||||||
inp.setAttribute('data-no-select', '');
|
inp.setAttribute('data-no-select', '');
|
||||||
|
if (colFilters[c.key]) inp.value = colFilters[c.key].join(' ');
|
||||||
inp.addEventListener('input', function () { setColFilter(c.key, this.value); });
|
inp.addEventListener('input', function () { setColFilter(c.key, this.value); });
|
||||||
th.appendChild(inp);
|
th.appendChild(inp);
|
||||||
}
|
}
|
||||||
|
|
@ -8955,11 +9052,12 @@ X.B(E,Y);return E}return J}())
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (opts.onActivate) opts.onActivate(getSelection()); }
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (opts.onActivate) opts.onActivate(getSelection()); }
|
||||||
else if (e.key === 'Escape') { clearSel(); }
|
else if (e.key === 'Escape') { clearSel(); }
|
||||||
});
|
});
|
||||||
|
updateSortIndicators();
|
||||||
renderBody();
|
renderBody();
|
||||||
}
|
}
|
||||||
function renderBody() {
|
function renderBody() {
|
||||||
if (!bodyEl) return;
|
if (!bodyEl) return;
|
||||||
var fr = filtered();
|
var fr = visibleRows();
|
||||||
bodyEl.textContent = '';
|
bodyEl.textContent = '';
|
||||||
fr.forEach(function (row) {
|
fr.forEach(function (row) {
|
||||||
var id = rowId(row);
|
var id = rowId(row);
|
||||||
|
|
@ -9000,8 +9098,9 @@ X.B(E,Y);return E}return J}())
|
||||||
render: render, renderBody: renderBody,
|
render: render, renderBody: renderBody,
|
||||||
getSelection: getSelection, getFilteredRows: getFilteredRows,
|
getSelection: getSelection, getFilteredRows: getFilteredRows,
|
||||||
setFilter: setFilter, setColFilter: setColFilter, selectAllFiltered: selectAllFiltered, clear: clearSel,
|
setFilter: setFilter, setColFilter: setColFilter, selectAllFiltered: selectAllFiltered, clear: clearSel,
|
||||||
|
sortBy: toggleSort, getSortState: function () { return sortState.slice(); },
|
||||||
clickRow: function (id, mods) {
|
clickRow: function (id, mods) {
|
||||||
var fr = filtered();
|
var fr = visibleRows();
|
||||||
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];
|
||||||
if (row) onRowClick(Object.assign({ shiftKey: false, ctrlKey: false, metaKey: false }, mods || {}), row, fr);
|
if (row) onRowClick(Object.assign({ shiftKey: false, ctrlKey: false, metaKey: false }, mods || {}), row, fr);
|
||||||
},
|
},
|
||||||
|
|
@ -11392,14 +11491,18 @@ X.B(E,Y);return E}return J}())
|
||||||
return rfHit(orig) || rfHit(C().deriveTarget(f).filename || '');
|
return rfHit(orig) || rfHit(C().deriveTarget(f).filename || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── By-tracking: flat editable grid (one row per file) ──────────────────
|
// ── By-tracking: flat editable grid (one row per file), on the shared
|
||||||
var GRID_COLS = [
|
// seltable — so it gets multi-sort + per-column autofilters + resizable,
|
||||||
{ id: 'status', title: '', cls: 'tg-status', fixed: true },
|
// persisted widths for free. Only `hidden` (the Columns ▾ chooser) is
|
||||||
{ id: 'orig', title: 'Original name', cls: 'tg-orig' },
|
// classifier-specific; widths + sort persist under the same key via
|
||||||
{ id: 'tn', title: 'Tracking number', cls: 'tg-tn' },
|
// seltable's own persistKey storage (merged, not clobbered). ───────────
|
||||||
{ id: 'rev', title: 'Rev (status)', cls: 'tg-rev' },
|
var GRID_COL_META = [
|
||||||
{ id: 'title', title: 'Title', cls: 'tg-title' },
|
{ id: 'status', title: 'Status', fixed: true }, // fixed = never hidden by the chooser
|
||||||
{ id: 'x', title: '', cls: 'tg-x', fixed: true },
|
{ id: 'orig', title: 'Original name' },
|
||||||
|
{ id: 'tn', title: 'Tracking number' },
|
||||||
|
{ id: 'rev', title: 'Rev (status)' },
|
||||||
|
{ id: 'title', title: 'Title' },
|
||||||
|
{ id: 'x', title: '', fixed: true },
|
||||||
];
|
];
|
||||||
var GRID_PREFS_KEY = 'zddc.classifier.trackingCols';
|
var GRID_PREFS_KEY = 'zddc.classifier.trackingCols';
|
||||||
function gridPrefs() { try { return JSON.parse(localStorage.getItem(GRID_PREFS_KEY)) || {}; } catch (_) { return {}; } }
|
function gridPrefs() { try { return JSON.parse(localStorage.getItem(GRID_PREFS_KEY)) || {}; } catch (_) { return {}; } }
|
||||||
|
|
@ -11424,75 +11527,30 @@ X.B(E,Y);return E}return J}())
|
||||||
// different tab, so its "not placed in a transmittal" error doesn't count here.
|
// different tab, so its "not placed in a transmittal" error doesn't count here.
|
||||||
function nameErrors(d) { return (d.errors || []).filter(function (e) { return e.indexOf('transmittal') === -1; }); }
|
function nameErrors(d) { return (d.errors || []).filter(function (e) { return e.indexOf('transmittal') === -1; }); }
|
||||||
|
|
||||||
function renderTrackingGrid(container) {
|
// ── per-column cell renderers ──────────────────────────────────────────
|
||||||
container.textContent = '';
|
function gridStatusCell(td, f) {
|
||||||
var c = C();
|
var c = C(), key = c.srcKeyForFile(f), d = c.deriveTarget(f), conflict = c.hasHashConflict(key);
|
||||||
var files = c.trackingGridKeys().map(fileByKey).filter(Boolean)
|
|
||||||
.filter(function (f) { return !rfActive() || fileRowMatches(f); });
|
|
||||||
if (!files.length) {
|
|
||||||
container.appendChild(el('div', 'target-empty', rfActive() ? 'No matches.'
|
|
||||||
: 'No files yet — drag files here from the left, then type each one’s tracking number, revision, and title. A file that’s already ZDDC-named fills in automatically.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var prefs = gridPrefs(), hidden = prefs.hidden || {}, widths = prefs.widths || {};
|
|
||||||
var cols = GRID_COLS.filter(function (col) { return !hidden[col.id]; });
|
|
||||||
|
|
||||||
var table = el('table', 'ttable ttable--grid');
|
|
||||||
var thead = el('thead'), htr = el('tr');
|
|
||||||
cols.forEach(function (col) {
|
|
||||||
var th = el('th', 'tg-th ' + col.cls, col.title);
|
|
||||||
th.dataset.col = col.id;
|
|
||||||
if (widths[col.id]) { th.style.width = th.style.minWidth = th.style.maxWidth = widths[col.id] + 'px'; }
|
|
||||||
htr.appendChild(th);
|
|
||||||
});
|
|
||||||
thead.appendChild(htr); table.appendChild(thead);
|
|
||||||
|
|
||||||
var tbody = el('tbody');
|
|
||||||
files.forEach(function (f) {
|
|
||||||
var key = c.srcKeyForFile(f), d = c.deriveTarget(f);
|
|
||||||
var bad = nameErrors(d).length || c.hasHashConflict(key);
|
|
||||||
var tr = el('tr', 'tg-row' + (bad ? ' tg-row--err' : ''));
|
|
||||||
tr.dataset.key = key;
|
|
||||||
cols.forEach(function (col) {
|
|
||||||
var td = el('td', 'tg-td ' + col.cls);
|
|
||||||
buildGridCell(col.id, td, f, key);
|
|
||||||
tr.appendChild(td);
|
|
||||||
});
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
});
|
|
||||||
table.appendChild(tbody);
|
|
||||||
container.appendChild(table);
|
|
||||||
if (window.app.modules.resize && window.app.modules.resize.init) window.app.modules.resize.init(table, persistColWidths);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildGridCell(colId, td, f, key) {
|
|
||||||
var c = C(), d = c.deriveTarget(f), conflict = c.hasHashConflict(key);
|
|
||||||
if (colId === 'status') {
|
|
||||||
var ne = nameErrors(d), ok = !ne.length && !conflict;
|
var ne = nameErrors(d), ok = !ne.length && !conflict;
|
||||||
var badge = el('span', 'tfile__badge ' + (ok ? 'tfile__badge--ok' : 'tfile__badge--err'), conflict ? '≠' : (ne.length ? '⚠' : '✓'));
|
var badge = el('span', 'tfile__badge ' + (ok ? 'tfile__badge--ok' : 'tfile__badge--err'), conflict ? '≠' : (ne.length ? '⚠' : '✓'));
|
||||||
badge.title = (conflict ? 'Same tracking+revision as another file but DIFFERENT content. ' : '') + (ne.length ? ne.join('; ') : 'Complete');
|
badge.title = (conflict ? 'Same tracking+revision as another file but DIFFERENT content. ' : '') + (ne.length ? ne.join('; ') : 'Complete');
|
||||||
td.appendChild(badge); return;
|
td.appendChild(badge);
|
||||||
}
|
}
|
||||||
if (colId === 'orig') {
|
function gridOrigCell(td, f) {
|
||||||
|
var key = C().srcKeyForFile(f);
|
||||||
var orig = f.originalFilename + (f.extension ? '.' + f.extension : '');
|
var orig = f.originalFilename + (f.extension ? '.' + f.extension : '');
|
||||||
var link = el('a', 'tg-orig__link', orig);
|
var link = el('a', 'tg-orig__link', orig);
|
||||||
link.href = '#'; link.title = 'Preview ' + orig;
|
link.href = '#'; link.title = 'Preview ' + orig;
|
||||||
link.addEventListener('click', function (e) { e.preventDefault(); previewKey(key); });
|
link.addEventListener('click', function (e) { e.preventDefault(); previewKey(key); });
|
||||||
td.appendChild(link); return;
|
td.appendChild(link);
|
||||||
}
|
}
|
||||||
if (colId === 'x') {
|
function gridEditCell(td, colId, f) {
|
||||||
var rm = el('button', 'tnode__act tg-x__btn', '✕');
|
var c = C(), key = c.srcKeyForFile(f), ident = currentIdent(f);
|
||||||
rm.title = 'Remove from the grid';
|
|
||||||
rm.addEventListener('click', function () { c.removeFromTrackingGrid(key); });
|
|
||||||
td.appendChild(rm); return;
|
|
||||||
}
|
|
||||||
// editable: tn / rev / title
|
|
||||||
var ident = currentIdent(f);
|
|
||||||
var value = colId === 'tn' ? ident.tracking : colId === 'rev' ? ident.rev : ident.title;
|
var value = colId === 'tn' ? ident.tracking : colId === 'rev' ? ident.rev : ident.title;
|
||||||
var ph = colId === 'tn' ? 'ACME-…-0001' : colId === 'rev' ? 'A (IFR)' : 'title';
|
var ph = colId === 'tn' ? 'ACME-…-0001' : colId === 'rev' ? 'A (IFR)' : 'title';
|
||||||
var warn = colId === 'tn' ? gridTnWarn(ident.tracking) : '';
|
var warn = colId === 'tn' ? gridTnWarn(ident.tracking) : '';
|
||||||
var inp = el('input', 'tg-input' + (warn ? ' is-warn' : ''));
|
var inp = el('input', 'tg-input' + (warn ? ' is-warn' : ''));
|
||||||
inp.type = 'text'; inp.value = value || ''; inp.placeholder = ph; inp.spellcheck = false;
|
inp.type = 'text'; inp.value = value || ''; inp.placeholder = ph; inp.spellcheck = false;
|
||||||
|
inp.setAttribute('data-no-select', ''); // a click in the input must not toggle row selection
|
||||||
if (warn) inp.title = warn;
|
if (warn) inp.title = warn;
|
||||||
inp.addEventListener('change', function () {
|
inp.addEventListener('change', function () {
|
||||||
var cur = currentIdent(f); // re-read so a prior edit isn't clobbered
|
var cur = currentIdent(f); // re-read so a prior edit isn't clobbered
|
||||||
|
|
@ -11503,11 +11561,73 @@ X.B(E,Y);return E}return J}())
|
||||||
});
|
});
|
||||||
td.appendChild(inp);
|
td.appendChild(inp);
|
||||||
}
|
}
|
||||||
|
function gridRemoveCell(td, f) {
|
||||||
|
var c = C(), key = c.srcKeyForFile(f);
|
||||||
|
var rm = el('button', 'tnode__act tg-x__btn', '✕');
|
||||||
|
rm.title = 'Remove from the grid';
|
||||||
|
rm.addEventListener('click', function () { c.removeFromTrackingGrid(key); });
|
||||||
|
td.appendChild(rm);
|
||||||
|
}
|
||||||
|
// Build the seltable column array, dropping any the chooser has hidden. Each
|
||||||
|
// column's `get` feeds sort + filter; `render` paints the (editable) cell.
|
||||||
|
function trackingColumns() {
|
||||||
|
var c = C(), hidden = (gridPrefs().hidden || {});
|
||||||
|
var defs = {
|
||||||
|
status: { key: 'status', title: 'Status', cls: 'tg-status', filterable: false,
|
||||||
|
get: function (f) { var d = c.deriveTarget(f); return c.hasHashConflict(c.srcKeyForFile(f)) ? 'conflict' : (nameErrors(d).length ? 'incomplete' : 'ok'); },
|
||||||
|
render: function (f, td) { gridStatusCell(td, f); } },
|
||||||
|
orig: { key: 'orig', title: 'Original name', cls: 'tg-orig',
|
||||||
|
get: function (f) { return f.originalFilename + (f.extension ? '.' + f.extension : ''); },
|
||||||
|
render: function (f, td) { gridOrigCell(td, f); } },
|
||||||
|
tn: { key: 'tn', title: 'Tracking number', cls: 'tg-tn',
|
||||||
|
get: function (f) { return currentIdent(f).tracking; },
|
||||||
|
render: function (f, td) { gridEditCell(td, 'tn', f); } },
|
||||||
|
rev: { key: 'rev', title: 'Rev (status)', cls: 'tg-rev',
|
||||||
|
get: function (f) { return currentIdent(f).rev; },
|
||||||
|
render: function (f, td) { gridEditCell(td, 'rev', f); } },
|
||||||
|
title: { key: 'title', title: 'Title', cls: 'tg-title',
|
||||||
|
get: function (f) { return currentIdent(f).title; },
|
||||||
|
render: function (f, td) { gridEditCell(td, 'title', f); } },
|
||||||
|
x: { key: 'x', title: '', cls: 'tg-x', sortable: false, filterable: false,
|
||||||
|
render: function (f, td) { gridRemoveCell(td, f); } },
|
||||||
|
};
|
||||||
|
return GRID_COL_META.filter(function (m) { return !hidden[m.id]; }).map(function (m) { return defs[m.id]; });
|
||||||
|
}
|
||||||
|
|
||||||
function persistColWidths(table) {
|
var trackingGrid = null, trackingColSig = '';
|
||||||
var p = gridPrefs(); p.widths = p.widths || {};
|
function colSig() { return trackingColumns().map(function (c) { return c.key; }).join(','); }
|
||||||
Array.prototype.forEach.call(table.querySelectorAll('thead th[data-col]'), function (th) { p.widths[th.dataset.col] = Math.round(th.offsetWidth); });
|
function ensureTrackingGrid(container) {
|
||||||
saveGridPrefs(p);
|
if (trackingGrid) return trackingGrid;
|
||||||
|
var c = C();
|
||||||
|
trackingColSig = colSig();
|
||||||
|
trackingGrid = window.app.modules.seltable.create({
|
||||||
|
container: container,
|
||||||
|
rows: function () { return c.trackingGridKeys().map(fileByKey).filter(Boolean); },
|
||||||
|
rowId: function (f) { return c.srcKeyForFile(f); },
|
||||||
|
columns: trackingColumns(),
|
||||||
|
persistKey: GRID_PREFS_KEY,
|
||||||
|
});
|
||||||
|
trackingGrid.render();
|
||||||
|
return trackingGrid;
|
||||||
|
}
|
||||||
|
function renderTrackingGrid(container) {
|
||||||
|
// Empty ↔ populated transition: tear the seltable down for the prompt,
|
||||||
|
// re-create it (create-once) when files arrive — same as the worklist.
|
||||||
|
if (!C().trackingGridKeys().length) {
|
||||||
|
trackingGrid = null;
|
||||||
|
container.textContent = '';
|
||||||
|
container.classList.remove('seltable');
|
||||||
|
container.appendChild(el('div', 'target-empty',
|
||||||
|
'No files yet — drag files here from the left, then type each one’s tracking number, revision, and title. A file that’s already ZDDC-named fills in automatically.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The Columns ▾ chooser changes which columns exist → rebuild on mismatch
|
||||||
|
// (self-correcting, whichever way `hidden` was changed).
|
||||||
|
if (trackingGrid && colSig() !== trackingColSig) trackingGrid = null;
|
||||||
|
ensureTrackingGrid(container);
|
||||||
|
// Mirror the name-filter box above the trees into the grid's global filter
|
||||||
|
// (setFilter re-renders the body, so the rows are always fresh on render).
|
||||||
|
trackingGrid.setFilter(rfTerms.join(' '));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drag files onto the grid → add as rows; auto-fill any already ZDDC-named.
|
// Drag files onto the grid → add as rows; auto-fill any already ZDDC-named.
|
||||||
|
|
@ -11540,14 +11660,16 @@ X.B(E,Y);return E}return J}())
|
||||||
if (open) { open.remove(); return; }
|
if (open) { open.remove(); return; }
|
||||||
var hidden = (gridPrefs().hidden || {});
|
var hidden = (gridPrefs().hidden || {});
|
||||||
var menu = el('div', 'col-chooser');
|
var menu = el('div', 'col-chooser');
|
||||||
GRID_COLS.forEach(function (col) {
|
GRID_COL_META.forEach(function (col) {
|
||||||
if (col.fixed) return;
|
if (col.fixed) return;
|
||||||
var lbl = el('label', 'col-chooser__item');
|
var lbl = el('label', 'col-chooser__item');
|
||||||
var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !hidden[col.id];
|
var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !hidden[col.id];
|
||||||
cb.addEventListener('change', function () {
|
cb.addEventListener('change', function () {
|
||||||
var p = gridPrefs(); p.hidden = p.hidden || {};
|
var p = gridPrefs(); p.hidden = p.hidden || {};
|
||||||
if (cb.checked) delete p.hidden[col.id]; else p.hidden[col.id] = true;
|
if (cb.checked) delete p.hidden[col.id]; else p.hidden[col.id] = true;
|
||||||
saveGridPrefs(p); render();
|
saveGridPrefs(p);
|
||||||
|
trackingGrid = null; // column set changed → rebuild the seltable
|
||||||
|
render();
|
||||||
});
|
});
|
||||||
lbl.appendChild(cb); lbl.appendChild(document.createTextNode(' ' + col.title));
|
lbl.appendChild(cb); lbl.appendChild(document.createTextNode(' ' + col.title));
|
||||||
menu.appendChild(lbl);
|
menu.appendChild(lbl);
|
||||||
|
|
@ -11672,7 +11794,8 @@ X.B(E,Y);return E}return J}())
|
||||||
{ key: 'title', title: 'Title', cls: 'worklist-title', get: function (r) { return r.title || ''; },
|
{ key: 'title', title: 'Title', cls: 'worklist-title', get: function (r) { return r.title || ''; },
|
||||||
render: function (r, td) { editCell(td, 'worklist-title__input', r.title, 'title', function (v) { c.setRowTitle(r.id, v); }); } },
|
render: function (r, td) { editCell(td, 'worklist-title__input', r.title, 'title', function (v) { c.setRowTitle(r.id, v); }); } },
|
||||||
{ key: 'cur', title: 'Current name', cls: 'worklist-cur', get: function (r) { return r.currentName || ''; } },
|
{ key: 'cur', title: 'Current name', cls: 'worklist-cur', get: function (r) { return r.currentName || ''; } },
|
||||||
{ key: 'src', title: 'Source', cls: 'worklist-src', get: function (r) { var s = r.source || {}; return [s.mdl ? 'mdl' : '', s.archive ? 'arch' : '', s.pasted ? 'pasted' : ''].filter(Boolean).join(' '); },
|
{ key: 'src', title: 'Source', cls: 'worklist-src', sortable: false, filterable: false,
|
||||||
|
get: function (r) { var s = r.source || {}; return [s.mdl ? 'mdl' : '', s.archive ? 'arch' : '', s.pasted ? 'pasted' : ''].filter(Boolean).join(' '); },
|
||||||
render: function (r, td) { renderSource(r, td); } },
|
render: function (r, td) { renderSource(r, td); } },
|
||||||
{ key: 'latest', title: 'Latest rev', get: function (r) { return latestRevOf(r.archiveRevisions); } },
|
{ key: 'latest', title: 'Latest rev', get: function (r) { return latestRevOf(r.archiveRevisions); } },
|
||||||
{ key: 'rev', title: 'Revision', cls: 'worklist-rev', get: function (r) { return r.revisionCell; },
|
{ key: 'rev', title: 'Revision', cls: 'worklist-rev', get: function (r) { return r.revisionCell; },
|
||||||
|
|
|
||||||
|
|
@ -1793,7 +1793,7 @@ body {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC</span>
|
<span class="app-header__title">ZDDC</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:31:21 · be5b396</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -2770,7 +2770,7 @@ dialog.modal--narrow {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title">ZDDC Transmittal</span>
|
<span class="app-header__title">ZDDC Transmittal</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:31:21 · be5b396</span></span>
|
||||||
</div>
|
</div>
|
||||||
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
|
||||||
<!-- Publish split-button (Transmittal-specific primary action;
|
<!-- Publish split-button (Transmittal-specific primary action;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
# Generated by build.sh — do not edit. One <app>=<build label> per line.
|
||||||
archive=v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace
|
archive=v0.0.27-beta · 2026-06-16 13:31:21 · be5b396
|
||||||
transmittal=v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace
|
transmittal=v0.0.27-beta · 2026-06-16 13:31:21 · be5b396
|
||||||
classifier=v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace
|
classifier=v0.0.27-beta · 2026-06-16 13:31:21 · be5b396
|
||||||
landing=v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace
|
landing=v0.0.27-beta · 2026-06-16 13:31:21 · be5b396
|
||||||
form=v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace
|
form=v0.0.27-beta · 2026-06-16 13:31:21 · be5b396
|
||||||
tables=v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace
|
tables=v0.0.27-beta · 2026-06-16 13:31:22 · be5b396
|
||||||
browse=v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace
|
browse=v0.0.27-beta · 2026-06-16 13:31:22 · be5b396
|
||||||
|
|
|
||||||
|
|
@ -1260,6 +1260,16 @@ body.is-elevated::after {
|
||||||
color: var(--text-muted); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
|
color: var(--text-muted); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.seltable__table thead tr.seltable__filters th { top: 1.55rem; padding: 0.15rem 0.35rem; }
|
.seltable__table thead tr.seltable__filters th { top: 1.55rem; padding: 0.15rem 0.35rem; }
|
||||||
|
/* Sortable headers + multi-sort indicator. The title th is position:sticky
|
||||||
|
(a positioning context) so the drag-resizer can absolutely anchor to its edge. */
|
||||||
|
.seltable__th--sortable { cursor: pointer; }
|
||||||
|
.seltable__th--sortable:hover { color: var(--text); }
|
||||||
|
.seltable__sortind { color: var(--primary); font-size: 0.7em; font-weight: 700; }
|
||||||
|
.seltable__resizer {
|
||||||
|
position: absolute; top: 0; right: 0; bottom: 0; width: 6px;
|
||||||
|
cursor: col-resize; user-select: none; touch-action: none;
|
||||||
|
}
|
||||||
|
.seltable__resizer:hover { background: var(--primary); opacity: 0.4; }
|
||||||
.seltable__colfilter {
|
.seltable__colfilter {
|
||||||
width: 100%; min-width: 2rem; box-sizing: border-box; padding: 0.15rem 0.35rem;
|
width: 100%; min-width: 2rem; box-sizing: border-box; padding: 0.15rem 0.35rem;
|
||||||
border: 1px solid var(--border); border-radius: var(--radius);
|
border: 1px solid var(--border); border-radius: var(--radius);
|
||||||
|
|
@ -1770,7 +1780,7 @@ body.is-elevated::after {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 04:53:19 · 2b32ace</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-16 13:31:22 · be5b396</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
@ -4099,20 +4109,26 @@ body.is-elevated::after {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ZDDC — shared selectable + autofilter table (used by the classifier catalog
|
* ZDDC — shared selectable + autofilter table (used by the classifier worklist +
|
||||||
* and the tables tool's "Add from archive").
|
* By-tracking grid, and the tables tool's "Add from archive").
|
||||||
*
|
*
|
||||||
* A flat table with PER-COLUMN autofilters (one input per column, AND-combined,
|
* 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
|
* MULTI-COLUMN SORT (click a header to sort; shift/ctrl-click adds a secondary
|
||||||
* filter, and powerful selection for building complex sets quickly:
|
* key), RESIZABLE columns (drag the header edge), an optional programmatic global
|
||||||
|
* filter, and powerful selection:
|
||||||
* 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. Selection is keyed by a stable
|
* Ranges run over the CURRENTLY FILTERED + SORTED order. Selection is keyed by a
|
||||||
* rowId so it survives filtering and re-render.
|
* stable rowId so it survives filtering, sorting, and re-render. Pass
|
||||||
|
* opts.persistKey to remember column widths + sort across reloads (localStorage).
|
||||||
|
*
|
||||||
|
* Column config: { key, title, cls?, get?(row), render?(row, td), filterable?,
|
||||||
|
* sortable?, sortValue?(row) }. sortable/filterable default true; set false for
|
||||||
|
* render-only columns (a remove button, a derived badge).
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -4131,6 +4147,12 @@ body.is-elevated::after {
|
||||||
if (text != null) e.textContent = text;
|
if (text != null) e.textContent = text;
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
function loadPersist(key) { try { return JSON.parse(localStorage.getItem(key)) || {}; } catch (_) { return {}; } }
|
||||||
|
function savePersist(key, patch) {
|
||||||
|
var cur = loadPersist(key);
|
||||||
|
for (var k in patch) cur[k] = patch[k];
|
||||||
|
try { localStorage.setItem(key, JSON.stringify(cur)); } catch (_) { /* private mode */ }
|
||||||
|
}
|
||||||
|
|
||||||
function create(opts) {
|
function create(opts) {
|
||||||
var container = opts.container;
|
var container = opts.container;
|
||||||
|
|
@ -4142,6 +4164,12 @@ body.is-elevated::after {
|
||||||
var globalTerms = []; // programmatic global filter (tests/reveal)
|
var globalTerms = []; // programmatic global filter (tests/reveal)
|
||||||
var colFilters = Object.create(null); // colKey -> terms[] (the per-column autofilters)
|
var colFilters = Object.create(null); // colKey -> terms[] (the per-column autofilters)
|
||||||
|
|
||||||
|
var persistKey = opts.persistKey || null;
|
||||||
|
var saved = persistKey ? loadPersist(persistKey) : {};
|
||||||
|
var sortState = Array.isArray(saved.sort) ? saved.sort.slice() : []; // [{ key, dir }]
|
||||||
|
var colWidths = saved.widths || {}; // colKey -> px
|
||||||
|
var headEls = Object.create(null); // colKey -> { th, ind }
|
||||||
|
|
||||||
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 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]); }
|
||||||
|
|
@ -4156,6 +4184,59 @@ body.is-elevated::after {
|
||||||
}
|
}
|
||||||
function filtered() { return rows().filter(rowMatches); }
|
function filtered() { return rows().filter(rowMatches); }
|
||||||
|
|
||||||
|
// ── multi-column sort ──────────────────────────────────────────────
|
||||||
|
function sortVal(col, row) { return col.sortValue ? col.sortValue(row) : colVal(col, row); }
|
||||||
|
function sorted(list) {
|
||||||
|
if (!sortState.length) return list;
|
||||||
|
var arr = list.slice();
|
||||||
|
arr.sort(function (a, b) {
|
||||||
|
for (var i = 0; i < sortState.length; i++) {
|
||||||
|
var s = sortState[i], col = colByKey(s.key); if (!col) continue;
|
||||||
|
var cmp = String(sortVal(col, a)).localeCompare(String(sortVal(col, b)), undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
if (cmp) return s.dir === 'desc' ? -cmp : cmp;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
function visibleRows() { return sorted(filtered()); }
|
||||||
|
function sortIdx(key) { for (var i = 0; i < sortState.length; i++) { if (sortState[i].key === key) return i; } return -1; }
|
||||||
|
function toggleSort(key, add) {
|
||||||
|
var i = sortIdx(key);
|
||||||
|
if (add) {
|
||||||
|
if (i >= 0) { if (sortState[i].dir === 'asc') sortState[i].dir = 'desc'; else sortState.splice(i, 1); }
|
||||||
|
else sortState.push({ key: key, dir: 'asc' });
|
||||||
|
} else {
|
||||||
|
if (i === 0 && sortState.length === 1) sortState = sortState[0].dir === 'asc' ? [{ key: key, dir: 'desc' }] : [];
|
||||||
|
else sortState = [{ key: key, dir: 'asc' }];
|
||||||
|
}
|
||||||
|
if (persistKey) savePersist(persistKey, { sort: sortState });
|
||||||
|
updateSortIndicators(); renderBody();
|
||||||
|
}
|
||||||
|
function updateSortIndicators() {
|
||||||
|
columns.forEach(function (c) {
|
||||||
|
var h = headEls[c.key]; if (!h) return;
|
||||||
|
var i = sortIdx(c.key);
|
||||||
|
h.ind.textContent = i >= 0 ? ((sortState[i].dir === 'asc' ? ' ▲' : ' ▼') + (sortState.length > 1 ? (i + 1) : '')) : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ── resizable columns ──────────────────────────────────────────────
|
||||||
|
function addResizer(th, key) {
|
||||||
|
var rz = elt('div', 'seltable__resizer');
|
||||||
|
rz.addEventListener('mousedown', function (e) {
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
var startX = e.clientX, startW = th.offsetWidth;
|
||||||
|
function mm(ev) { var w = Math.max(40, startW + (ev.clientX - startX)); th.style.width = th.style.minWidth = th.style.maxWidth = w + 'px'; }
|
||||||
|
function mu() {
|
||||||
|
document.removeEventListener('mousemove', mm); document.removeEventListener('mouseup', mu);
|
||||||
|
colWidths[key] = Math.round(th.offsetWidth);
|
||||||
|
if (persistKey) savePersist(persistKey, { widths: colWidths });
|
||||||
|
}
|
||||||
|
document.addEventListener('mousemove', mm); document.addEventListener('mouseup', mu);
|
||||||
|
});
|
||||||
|
th.appendChild(rz);
|
||||||
|
}
|
||||||
|
|
||||||
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()); }
|
||||||
|
|
@ -4195,7 +4276,23 @@ body.is-elevated::after {
|
||||||
var scroll = elt('div', 'seltable__scroll');
|
var scroll = elt('div', 'seltable__scroll');
|
||||||
var table = elt('table', 'seltable__table');
|
var table = elt('table', 'seltable__table');
|
||||||
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)); });
|
headEls = Object.create(null);
|
||||||
|
columns.forEach(function (c) {
|
||||||
|
var th = elt('th', c.cls || null);
|
||||||
|
th.appendChild(document.createTextNode(c.title || c.key));
|
||||||
|
var ind = elt('span', 'seltable__sortind'); th.appendChild(ind);
|
||||||
|
headEls[c.key] = { th: th, ind: ind };
|
||||||
|
if (colWidths[c.key]) { th.style.width = th.style.minWidth = th.style.maxWidth = colWidths[c.key] + 'px'; }
|
||||||
|
if (c.sortable !== false) {
|
||||||
|
th.classList.add('seltable__th--sortable');
|
||||||
|
th.addEventListener('click', function (e) {
|
||||||
|
if (e.target.classList.contains('seltable__resizer')) return;
|
||||||
|
toggleSort(c.key, e.shiftKey || e.ctrlKey || e.metaKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addResizer(th, c.key);
|
||||||
|
htr.appendChild(th);
|
||||||
|
});
|
||||||
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);
|
thead.appendChild(htr);
|
||||||
// Per-column autofilter row.
|
// Per-column autofilter row.
|
||||||
|
|
@ -4205,6 +4302,7 @@ body.is-elevated::after {
|
||||||
if (c.filterable !== false) {
|
if (c.filterable !== false) {
|
||||||
var inp = elt('input', 'seltable__colfilter'); inp.type = 'search'; inp.placeholder = 'filter…'; inp.spellcheck = false;
|
var inp = elt('input', 'seltable__colfilter'); inp.type = 'search'; inp.placeholder = 'filter…'; inp.spellcheck = false;
|
||||||
inp.setAttribute('data-no-select', '');
|
inp.setAttribute('data-no-select', '');
|
||||||
|
if (colFilters[c.key]) inp.value = colFilters[c.key].join(' ');
|
||||||
inp.addEventListener('input', function () { setColFilter(c.key, this.value); });
|
inp.addEventListener('input', function () { setColFilter(c.key, this.value); });
|
||||||
th.appendChild(inp);
|
th.appendChild(inp);
|
||||||
}
|
}
|
||||||
|
|
@ -4221,11 +4319,12 @@ body.is-elevated::after {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (opts.onActivate) opts.onActivate(getSelection()); }
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (opts.onActivate) opts.onActivate(getSelection()); }
|
||||||
else if (e.key === 'Escape') { clearSel(); }
|
else if (e.key === 'Escape') { clearSel(); }
|
||||||
});
|
});
|
||||||
|
updateSortIndicators();
|
||||||
renderBody();
|
renderBody();
|
||||||
}
|
}
|
||||||
function renderBody() {
|
function renderBody() {
|
||||||
if (!bodyEl) return;
|
if (!bodyEl) return;
|
||||||
var fr = filtered();
|
var fr = visibleRows();
|
||||||
bodyEl.textContent = '';
|
bodyEl.textContent = '';
|
||||||
fr.forEach(function (row) {
|
fr.forEach(function (row) {
|
||||||
var id = rowId(row);
|
var id = rowId(row);
|
||||||
|
|
@ -4266,8 +4365,9 @@ body.is-elevated::after {
|
||||||
render: render, renderBody: renderBody,
|
render: render, renderBody: renderBody,
|
||||||
getSelection: getSelection, getFilteredRows: getFilteredRows,
|
getSelection: getSelection, getFilteredRows: getFilteredRows,
|
||||||
setFilter: setFilter, setColFilter: setColFilter, selectAllFiltered: selectAllFiltered, clear: clearSel,
|
setFilter: setFilter, setColFilter: setColFilter, selectAllFiltered: selectAllFiltered, clear: clearSel,
|
||||||
|
sortBy: toggleSort, getSortState: function () { return sortState.slice(); },
|
||||||
clickRow: function (id, mods) {
|
clickRow: function (id, mods) {
|
||||||
var fr = filtered();
|
var fr = visibleRows();
|
||||||
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];
|
||||||
if (row) onRowClick(Object.assign({ shiftKey: false, ctrlKey: false, metaKey: false }, mods || {}), row, fr);
|
if (row) onRowClick(Object.assign({ shiftKey: false, ctrlKey: false, metaKey: false }, mods || {}), row, fr);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue