diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html
index e713d6b..f13ef68 100644
--- a/zddc/internal/apps/embedded/archive.html
+++ b/zddc/internal/apps/embedded/archive.html
@@ -2717,7 +2717,7 @@ td[data-field="trackingNumber"] {
@@ -7474,6 +7518,7 @@ X.B(E,Y);return E}return J}())
outputName: null, // remembered output directory display name
config: defaultConfig(), // tracking-number pattern (fields/statuses/modifiers)
worklist: [], // "From a list" scratch rows: [ { id, trackingNumber, title, revisionCell, source, archiveRevisions } ]
+ trackingWorkset: Object.create(null), // srcKeys shown as rows in the By-tracking grid (set: key->true)
};
// id -> { node, kind:'tracking'|'party'|'slot'|'transmittal', parent }
@@ -7834,6 +7879,7 @@ X.B(E,Y);return E}return J}())
worklist: state.worklist.map(function (r) {
return { id: r.id, party: r.party, trackingNumber: r.trackingNumber, title: r.title, revisionCell: r.revisionCell, source: r.source, archiveRevisions: r.archiveRevisions };
}),
+ trackingWorkset: Object.keys(state.trackingWorkset),
};
}
function load(obj) {
@@ -7844,6 +7890,8 @@ X.B(E,Y);return E}return J}())
state.outputName = obj.outputName || null;
state.config = normalizeConfig(obj.config);
state.worklist = (Array.isArray(obj.worklist) ? obj.worklist : []).map(normalizeRow);
+ state.trackingWorkset = Object.create(null);
+ (Array.isArray(obj.trackingWorkset) ? obj.trackingWorkset : []).forEach(function (k) { state.trackingWorkset[k] = true; });
rebuildIndex();
migrateLegacyMdl(obj.worklist); // BEFORE anything can prune; materializes old mdl placements
notify();
@@ -7875,10 +7923,57 @@ X.B(E,Y);return E}return J}())
function reset() {
state.assignments = {}; state.trackingTree = []; state.transmittalTree = [];
state.outputName = null;
+ state.trackingWorkset = Object.create(null);
rebuildIndex();
notify();
}
+ // ── By-tracking grid (one editable row per file) ─────────────────────────
+ // The grid is a flat presentation over the tracking-tree placement model.
+ // `trackingWorkset` tracks files put on the grid so a dropped file shows as a
+ // row before it has a tracking number; a file with a real tracking placement
+ // (named here OR via the "From a list" tab) is always a row too.
+ function addToTrackingGrid(keys) {
+ var changed = false;
+ (keys || []).forEach(function (k) { if (!state.trackingWorkset[k]) { state.trackingWorkset[k] = true; changed = true; } });
+ if (changed) notify();
+ }
+ function removeFromTrackingGrid(key) {
+ var a = state.assignments[key], old = a ? a.trackingNodeId : null;
+ delete state.trackingWorkset[key];
+ place([key], null, 'tracking');
+ if (old) pruneEmptyTrackingChain(old);
+ notify();
+ }
+ function trackingGridKeys() {
+ var set = Object.create(null);
+ Object.keys(state.trackingWorkset).forEach(function (k) { set[k] = true; });
+ Object.keys(state.assignments).forEach(function (k) { if (state.assignments[k].trackingNodeId) set[k] = true; });
+ return Object.keys(set);
+ }
+ // Re-materialize a file's tracking placement from a full identity. The caller
+ // passes ALL three fields (current values for the ones it didn't edit), read
+ // from deriveTarget — so this module needs no file objects. A blank revision
+ // lands on the PENDING_REV placeholder leaf (incomplete until set); a blank
+ // tracking number clears the placement (the row stays, unfilled).
+ function setFileIdentity(key, ident) {
+ ident = ident || {};
+ var tracking = (ident.tracking == null ? '' : String(ident.tracking)).trim();
+ var rev = (ident.rev == null ? '' : String(ident.rev)).trim();
+ var a = state.assignments[key], old = a ? a.trackingNodeId : null;
+ if (tracking) {
+ var leaf = addTrackingPath(null, parseFolderLevels(tracking + '_' + (rev || PENDING_REV)));
+ place([key], leaf, 'tracking');
+ if (old && old !== leaf) pruneEmptyTrackingChain(old);
+ } else {
+ place([key], null, 'tracking');
+ if (old) pruneEmptyTrackingChain(old);
+ }
+ setTitleOverride(key, ident.title || '');
+ state.trackingWorkset[key] = true;
+ notify();
+ }
+
// ── pattern config ───────────────────────────────────────────────────────
function normalizeConfig(c) {
var d = defaultConfig();
@@ -8302,6 +8397,9 @@ X.B(E,Y);return E}return J}())
transmittalRecord: transmittalRecord,
findOrAddParty: findOrAddParty, findOrAddTransmittalBin: findOrAddTransmittalBin,
getConfig: getConfig, setConfig: setConfig, getTrackingFields: getTrackingFields,
+ // By-tracking grid
+ addToTrackingGrid: addToTrackingGrid, removeFromTrackingGrid: removeFromTrackingGrid,
+ trackingGridKeys: trackingGridKeys, setFileIdentity: setFileIdentity,
setWorklist: setWorklist, appendWorklist: appendWorklist, clearWorklist: clearWorklist,
getWorklist: getWorklist, getWorklistRow: getWorklistRow,
assignFromRow: assignFromRow, unassignRowFile: unassignRowFile,
@@ -10308,18 +10406,24 @@ X.B(E,Y);return E}return J}())
if (agg === 'excluded') item.classList.add('excluded');
}
- // Folder name
+ // Name + counts stacked vertically: the count badge sits BELOW the name
+ // rather than right-aligned on the row.
+ const namebox = document.createElement('div');
+ namebox.className = 'folder-namebox';
+
const name = document.createElement('span');
name.className = 'folder-name';
name.textContent = folder.name;
- item.appendChild(name);
+ namebox.appendChild(name);
// Subfolder / file counts (immediate). Greyed via the row's .scanning
// class until the subtree is fully scanned.
const count = document.createElement('span');
count.className = 'folder-count';
populateCount(count, folder);
- item.appendChild(count);
+ namebox.appendChild(count);
+
+ item.appendChild(namebox);
// Extract button for ZIP roots
if (folder.isZipRoot) {
@@ -10975,7 +11079,9 @@ X.B(E,Y);return E}return J}())
btns.appendChild(go); btns.appendChild(cancel);
box.appendChild(h); box.appendChild(p); box.appendChild(treeWrap); box.appendChild(btns);
back.appendChild(box);
- back.addEventListener('click', function (e) { if (e.target === back) finish([]); });
+ var pressedBackdrop = false;
+ back.addEventListener('mousedown', function (e) { pressedBackdrop = (e.target === back); });
+ back.addEventListener('click', function (e) { if (e.target === back && pressedBackdrop) finish([]); });
document.addEventListener('keydown', onKey);
document.body.appendChild(back);
@@ -11092,7 +11198,7 @@ X.B(E,Y);return E}return J}())
matchNamesBtn: document.getElementById('matchNamesBtn'),
clearListBtn: document.getElementById('clearListBtn'),
hideAssignedToggle: document.getElementById('hideAssignedToggle'),
- addTrackingRootBtn: document.getElementById('addTrackingRootBtn'),
+ trackingColsBtn: document.getElementById('trackingColsBtn'),
addPartyBtn: document.getElementById('addPartyBtn'),
stats: document.getElementById('classifyStats'),
};
@@ -11127,22 +11233,16 @@ X.B(E,Y);return E}return J}())
var text = t ? t.getData('text') : '';
if (text) { e.preventDefault(); openPasteDialog(text); }
});
- els.addTrackingRootBtn.addEventListener('click', function () {
- var name = prompt('Root folder name (a tracking-number segment, e.g. "ACME-PROJ").\n'
- + 'Brace patterns expand: BMB-{PM,EL}-{0001-0002,0005}_A (IFR)', '');
- addFoldersFromPattern(null, name);
- });
+ if (els.trackingColsBtn) els.trackingColsBtn.addEventListener('click', openColumnChooser);
els.addPartyBtn.addEventListener('click', function () {
var name = prompt('Party name (also the transmittal-number prefix):', '');
if (name && name.trim()) C().addParty(name.trim());
});
- els.trackingTree.addEventListener('click', onTrackingClick);
els.transmittalTree.addEventListener('click', onTransmittalClick);
- els.trackingTree.addEventListener('change', onFileNameChange);
els.transmittalTree.addEventListener('change', onFileNameChange);
- setupDropZone(els.trackingTree, 'tracking');
+ setupGridDrop(els.trackingTree);
setupDropZone(els.transmittalTree, 'transmittal');
C().on(render);
@@ -11200,28 +11300,13 @@ X.B(E,Y);return E}return J}())
function activeAxis() { return currentTab === 'transmittal' ? 'transmittal' : 'tracking'; }
function reRenderSource() { if (window.app.modules.tree && window.app.modules.tree.render) window.app.modules.tree.render(); }
- // Expand a brace pattern into folder names and create them (confirming a
- // multi-create first). parentId null = root folders. See expandFolderPattern.
- function addFoldersFromPattern(parentId, raw) {
- if (!raw || !raw.trim()) return;
- var names = C().expandFolderPattern(raw);
- if (!names.length) return;
- if (names.length > 1) {
- var shown = names.slice(0, 8).join('\n');
- if (names.length > 8) shown += '\n…and ' + (names.length - 8) + ' more';
- if (!confirm('Create ' + names.length + ' folders?\n\n' + shown)) return;
- }
- // Each expanded name is parsed into nested tracking levels (split on
- // "-", final "_" splits the leaf rev), reusing shared ancestors.
- names.forEach(function (nm) { C().addTrackingPath(parentId, C().parseFolderLevels(nm)); });
- }
// ── render ───────────────────────────────────────────────────────────────
function render() {
if (!initialized || !C().isEnabled()) return;
var files = allFiles();
var placed = buildPlaced(files);
- renderTrackingInto(els.trackingTree, C().getTrackingTree(), placed.tracking);
+ renderTrackingGrid(els.trackingTree);
renderTransmittalInto(els.transmittalTree, C().getTransmittalTree(), placed.transmittal);
renderWorklist(placed.byTracking);
renderStats(files);
@@ -11307,165 +11392,173 @@ X.B(E,Y);return E}return J}())
return rfHit(orig) || rfHit(C().deriveTarget(f).filename || '');
}
- // ── By-tracking: merged-cell table ──────────────────────────────────────
- // The positional hierarchy reads left-to-right as columns (one per configured
- // field), ancestor cells span their descendants' rows, and the revision (the
- // leaf) gets its own aligned column. Each placed file is a row.
+ // ── By-tracking: flat editable grid (one row per file) ──────────────────
+ var GRID_COLS = [
+ { id: 'status', title: '', cls: 'tg-status', fixed: true },
+ { id: 'orig', title: 'Original name', cls: 'tg-orig' },
+ { id: 'tn', title: 'Tracking number', cls: 'tg-tn' },
+ { id: 'rev', title: 'Rev (status)', cls: 'tg-rev' },
+ { id: 'title', title: 'Title', cls: 'tg-title' },
+ { id: 'x', title: '', cls: 'tg-x', fixed: true },
+ ];
+ var GRID_PREFS_KEY = 'zddc.classifier.trackingCols';
+ function gridPrefs() { try { return JSON.parse(localStorage.getItem(GRID_PREFS_KEY)) || {}; } catch (_) { return {}; } }
+ function saveGridPrefs(p) { try { localStorage.setItem(GRID_PREFS_KEY, JSON.stringify(p)); } catch (_) { /* private mode */ } }
- // A node is a revision leaf when its name ends in a "(STATUS)" we recognise —
- // tracking field codes never carry a parenthesised status, so this cleanly
- // separates "0001" (a SEQ field) from "A (IFR)" (a revision).
- function revStatusOf(name) {
- var m = /\(\s*([A-Za-z0-9-]{1,5})\s*\)\s*$/.exec(name || '');
- return (m && window.zddc.isValidStatus(m[1])) ? m[1] : null;
- }
- function isRevisionLeaf(node) {
- return !(node.children || []).length && revStatusOf(node.name) != null;
- }
- // Flatten the tree into rows: { path:[fieldNodes], rev:revNode|null, file }.
- function buildTrackingRows(nodes, placedMap) {
- var rows = [];
- function emit(path, rev, files) {
- var fs = (files && files.length) ? files : [null];
- fs.forEach(function (f) { rows.push({ path: path, rev: rev, file: f }); });
- }
- function walk(node, ancestors) {
- var placed = placedMap[node.id] || [];
- if (isRevisionLeaf(node)) { emit(ancestors, node, placed); return; }
- var myPath = ancestors.concat(node); // node is a tracking field segment
- if (placed.length) emit(myPath, null, placed); // files dropped on a partial number
- var kids = node.children || [];
- if (kids.length) kids.forEach(function (c) { walk(c, myPath); });
- else if (!placed.length) emit(myPath, null, []); // empty leaf = drop target
- }
- nodes.forEach(function (n) { walk(n, []); });
- return rows;
- }
- function rowMatches(row) {
- if (!rfActive()) return true;
- if (row.file && fileRowMatches(row.file)) return true;
- if (row.rev && rfHit(row.rev.name)) return true;
- for (var i = 0; i < row.path.length; i++) { if (rfHit(row.path[i].name)) return true; }
- return false;
- }
-
- function fieldCellContent(node) {
- var inner = el('div', 'tcell__inner');
- inner.appendChild(el('span', 'tcell__name', node.name));
- inner.appendChild(nodeActions([
- { act: 'add', label: '+', title: 'Add child segment / revision' },
- { act: 'rename', label: '✎', title: 'Rename' },
- { act: 'del', label: '🗑', title: 'Delete' },
- ]));
- return inner;
- }
- function revCellContent(node, placedMap) {
- var inner = el('div', 'tcell__inner trev__inner');
- // The revision name doubles as a preview link for its placed file (the
- // common case is one file per revision). No count bubble.
- var files = placedMap[node.id] || [];
- if (files.length) {
- var link = el('a', 'tcell__name tcell__preview', node.name);
- link.href = '#';
- link.dataset.previewKey = C().srcKeyForFile(files[0]);
- link.title = 'Preview ' + files[0].originalFilename + (files[0].extension ? '.' + files[0].extension : '');
- inner.appendChild(link);
- } else {
- inner.appendChild(el('span', 'tcell__name', node.name));
- }
- inner.appendChild(nodeActions([
- { act: 'rename', label: '✎', title: 'Rename revision' },
- { act: 'del', label: '🗑', title: 'Delete' },
- ]));
- return inner;
- }
- // A placed-file cell: editable ZDDC name + validation badge; the original
- // filename is on hover, not shown inline. Reuses .tfile/.tfile__name so the
- // delegated preview + name-edit handlers apply.
- function fileCellContent(f) {
+ // A file's current identity, read from the placement model so per-field edits
+ // keep the fields they didn't touch.
+ function currentIdent(f) {
var d = C().deriveTarget(f);
- var conflict = C().hasHashConflict(d.key); // same name, different bytes
- var bad = d.errors.length || conflict;
- var row = el('div', 'tfile' + (bad ? ' tfile--err' : ''));
- row.dataset.key = d.key;
- var orig = f.originalFilename + (f.extension ? '.' + f.extension : '');
- var name = el('input', 'tfile__name' + (bad ? ' tfile__name--err' : ''));
- name.type = 'text';
- name.value = d.filename || '';
- name.placeholder = '(incomplete)';
- name.title = (conflict ? 'Same tracking+revision as another file but DIFFERENT content — fix before copying · ' : '')
- + (d.errors.length ? d.errors.join('; ') + ' · ' : '') + 'original: ' + orig;
- row.appendChild(name);
- row.appendChild(el('span', 'tfile__badge' + (bad ? ' tfile__badge--err' : ' tfile__badge--ok'),
- conflict ? '≠' : (d.errors.length ? '⚠' : '✓')));
- return row;
+ return { tracking: d.tracking || '', rev: (d.revision || '') + (d.status ? ' (' + d.status + ')' : ''), title: d.title || '' };
}
+ function gridTnWarn(tn) {
+ tn = (tn || '').trim(); if (!tn) return '';
+ var n = tn.split('-').length, want = C().getTrackingFields().length;
+ return (n < want - 1 || n > want) ? ('Has ' + n + ' segments; the pattern expects ' + want + '.') : '';
+ }
+ function previewKey(key) {
+ var f = fileByKey(key);
+ if (f && window.app.modules.preview && window.app.modules.preview.previewFile) window.app.modules.preview.previewFile(f);
+ }
+ // The By-tracking grid validates the NAME only — the transmittal (path) is a
+ // 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 renderTrackingInto(container, nodes, placedMap) {
+ function renderTrackingGrid(container) {
container.textContent = '';
- if (!nodes.length) {
- container.appendChild(el('div', 'target-empty', 'No tracking numbers yet — “+ Root folder” to start.'));
+ var c = C();
+ 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 rows = buildTrackingRows(nodes, placedMap).filter(rowMatches);
- if (!rows.length) {
- container.appendChild(el('div', 'target-empty', rfActive() ? 'No matches in the tracking tree.' : 'No tracking numbers yet.'));
- return;
- }
- var fields = C().getTrackingFields();
- var maxPath = rows.reduce(function (m, r) { return Math.max(m, r.path.length); }, 0);
- var nCols = Math.max(fields.length, maxPath);
+ var prefs = gridPrefs(), hidden = prefs.hidden || {}, widths = prefs.widths || {};
+ var cols = GRID_COLS.filter(function (col) { return !hidden[col.id]; });
- function cellId(row, col) {
- if (col < nCols) { var n = row.path[col]; return n ? n.id : null; }
- return row.rev ? row.rev.id : null; // col === nCols → revision
- }
- // Rowspan run starting at row i for column col (0 = covered from above).
- function spanAt(col, i) {
- var id = cellId(rows[i], col);
- if (id == null) return 1;
- if (i > 0 && cellId(rows[i - 1], col) === id) return 0;
- var span = 1;
- for (var j = i + 1; j < rows.length; j++) { if (cellId(rows[j], col) === id) span++; else break; }
- return span;
- }
-
- var table = el('table', 'ttable');
+ var table = el('table', 'ttable ttable--grid');
var thead = el('thead'), htr = el('tr');
- for (var c = 0; c < nCols; c++) {
- htr.appendChild(el('th', 'ttable__fh', fields[c] ? fields[c].name + (fields[c].optional ? ' ?' : '') : '·'));
- }
- htr.appendChild(el('th', 'ttable__rh', 'REVISION'));
- htr.appendChild(el('th', 'ttable__fileh', 'Files'));
+ 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');
- rows.forEach(function (row, i) {
- var tr = el('tr');
- for (var col = 0; col < nCols; col++) {
- var span = spanAt(col, i);
- if (span === 0) continue; // merged from the row above
- var node = row.path[col] || null;
- var td = el('td', 'ttable__cell' + (node ? '' : ' ttable__cell--empty'));
- if (span > 1) td.rowSpan = span;
- if (node) { td.dataset.id = node.id; td.appendChild(fieldCellContent(node)); }
+ 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);
- }
- var rspan = spanAt(nCols, i);
- if (rspan !== 0) {
- var rtd = el('td', 'ttable__rev' + (row.rev ? '' : ' ttable__cell--empty'));
- if (rspan > 1) rtd.rowSpan = rspan;
- if (row.rev) { rtd.dataset.id = row.rev.id; rtd.appendChild(revCellContent(row.rev, placedMap)); }
- tr.appendChild(rtd);
- }
- var ftd = el('td', 'ttable__file');
- if (row.file) ftd.appendChild(fileCellContent(row.file));
- else ftd.appendChild(el('span', 'ttable__drop', 'drop a file here'));
- tr.appendChild(ftd);
+ });
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 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');
+ td.appendChild(badge); return;
+ }
+ if (colId === 'orig') {
+ var orig = f.originalFilename + (f.extension ? '.' + f.extension : '');
+ var link = el('a', 'tg-orig__link', orig);
+ link.href = '#'; link.title = 'Preview ' + orig;
+ link.addEventListener('click', function (e) { e.preventDefault(); previewKey(key); });
+ td.appendChild(link); return;
+ }
+ if (colId === 'x') {
+ 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); return;
+ }
+ // editable: tn / rev / title
+ var ident = currentIdent(f);
+ var value = colId === 'tn' ? ident.tracking : colId === 'rev' ? ident.rev : ident.title;
+ var ph = colId === 'tn' ? 'ACME-…-0001' : colId === 'rev' ? 'A (IFR)' : 'title';
+ var warn = colId === 'tn' ? gridTnWarn(ident.tracking) : '';
+ var inp = el('input', 'tg-input' + (warn ? ' is-warn' : ''));
+ inp.type = 'text'; inp.value = value || ''; inp.placeholder = ph; inp.spellcheck = false;
+ if (warn) inp.title = warn;
+ inp.addEventListener('change', function () {
+ var cur = currentIdent(f); // re-read so a prior edit isn't clobbered
+ if (colId === 'tn') cur.tracking = inp.value.trim();
+ else if (colId === 'rev') cur.rev = inp.value.trim();
+ else cur.title = inp.value;
+ c.setFileIdentity(key, cur);
+ });
+ td.appendChild(inp);
+ }
+
+ function persistColWidths(table) {
+ var p = gridPrefs(); p.widths = p.widths || {};
+ Array.prototype.forEach.call(table.querySelectorAll('thead th[data-col]'), function (th) { p.widths[th.dataset.col] = Math.round(th.offsetWidth); });
+ saveGridPrefs(p);
+ }
+
+ // Drag files onto the grid → add as rows; auto-fill any already ZDDC-named.
+ function setupGridDrop(container) {
+ container.addEventListener('dragover', function (e) {
+ if (!window.app.modules.dnd.active()) return;
+ e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; container.classList.add('tg-drop-hover');
+ });
+ container.addEventListener('dragleave', function (e) { if (e.target === container) container.classList.remove('tg-drop-hover'); });
+ container.addEventListener('drop', function (e) {
+ container.classList.remove('tg-drop-hover');
+ e.preventDefault();
+ var keys = window.app.modules.dnd.getDrag(); window.app.modules.dnd.clearDrag();
+ if (keys.length) onGridDrop(keys);
+ });
+ }
+ function onGridDrop(keys) {
+ var c = C();
+ c.addToTrackingGrid(keys);
+ keys.forEach(function (k) {
+ var f = fileByKey(k); if (!f) return;
+ var p = window.zddc.parseFilename(f.originalFilename + (f.extension ? '.' + f.extension : ''));
+ if (p && p.valid && p.trackingNumber) c.setFileIdentity(k, { tracking: p.trackingNumber, rev: p.revision + (p.status ? ' (' + p.status + ')' : ''), title: p.title || '' });
+ });
+ }
+
+ // "Columns ▾" chooser — hide/show grid columns (status + ✕ always shown).
+ function openColumnChooser() {
+ var open = document.querySelector('.col-chooser');
+ if (open) { open.remove(); return; }
+ var hidden = (gridPrefs().hidden || {});
+ var menu = el('div', 'col-chooser');
+ GRID_COLS.forEach(function (col) {
+ if (col.fixed) return;
+ var lbl = el('label', 'col-chooser__item');
+ var cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !hidden[col.id];
+ cb.addEventListener('change', function () {
+ var p = gridPrefs(); p.hidden = p.hidden || {};
+ if (cb.checked) delete p.hidden[col.id]; else p.hidden[col.id] = true;
+ saveGridPrefs(p); render();
+ });
+ lbl.appendChild(cb); lbl.appendChild(document.createTextNode(' ' + col.title));
+ menu.appendChild(lbl);
+ });
+ var r = els.trackingColsBtn.getBoundingClientRect();
+ menu.style.top = (r.bottom + 2) + 'px'; menu.style.left = r.left + 'px';
+ document.body.appendChild(menu);
+ setTimeout(function () {
+ function off(e) { if (!menu.contains(e.target) && e.target !== els.trackingColsBtn) { menu.remove(); document.removeEventListener('mousedown', off); } }
+ document.addEventListener('mousedown', off);
+ }, 0);
}
// Transmittal tree
@@ -11775,7 +11868,11 @@ X.B(E,Y);return E}return J}())
var body = el('div', 'scratch-modal__body'); box.appendChild(body);
var foot = el('div', 'copy-choice__btns'); box.appendChild(foot);
back.appendChild(box);
- back.addEventListener('click', function (e) { if (e.target === back) close(); });
+ // Close on a genuine backdrop click only — not when a drag that began in
+ // the paste textarea (selecting text) ends out on the backdrop.
+ var pressedBackdrop = false;
+ back.addEventListener('mousedown', function (e) { pressedBackdrop = (e.target === back); });
+ back.addEventListener('click', function (e) { if (e.target === back && pressedBackdrop) close(); });
document.addEventListener('keydown', onKey);
document.body.appendChild(back);
return { body: body, foot: foot, close: close };
@@ -11946,40 +12043,6 @@ X.B(E,Y);return E}return J}())
if (parsed.title != null) C().setTitleOverride(tf.dataset.key, parsed.title);
// place/setTitleOverride fire classify.notify → re-render.
}
- // Collapse/expand a node and its whole subtree (ctrl/cmd-click a toggle).
- function setSubtreeCollapsed(nodeId, collapse) {
- var node = C().getNode(nodeId);
- if (!node) return;
- (function walk(n) {
- if ((n.children || []).length) { if (collapse) collapsed[n.id] = true; else delete collapsed[n.id]; }
- (n.children || []).forEach(walk);
- })(node);
- }
- function onTrackingClick(e) {
- if (previewFromTarget(e)) return;
- var btn = e.target.closest('[data-act]');
- if (!btn) return;
- var act = btn.dataset.act;
- var id = closestNodeId(btn);
- if (act === 'toggle') {
- var collapse = !collapsed[id];
- if (e.ctrlKey || e.metaKey) setSubtreeCollapsed(id, collapse);
- else if (collapse) collapsed[id] = true; else delete collapsed[id];
- render();
- return;
- }
- if (act === 'add') {
- var name = prompt('Child folder name (next tracking segment, or a leaf revision like "A (IFR)").\n'
- + 'Brace patterns expand: {PM,EL,EM}-MOM-{0001-0002,0005}_A (IFR)', '');
- addFoldersFromPattern(id, name);
- } else if (act === 'rename') {
- var node = C().getNode(id);
- var nn = prompt('Rename folder:', node ? node.name : '');
- if (nn && nn.trim()) C().renameNode(id, nn.trim());
- } else if (act === 'del') {
- if (confirm('Delete this folder and everything under it? Files placed here become unassigned.')) C().deleteNode(id);
- }
- }
function onTransmittalClick(e) {
if (previewFromTarget(e)) return;
var btn = e.target.closest('[data-act]');
@@ -12071,25 +12134,10 @@ X.B(E,Y);return E}return J}())
var keys = window.app.modules.dnd.getDrag();
window.app.modules.dnd.clearDrag();
if (!keys.length) return;
- if (axis === 'tracking') placeTrackingDrop(keys, t.id);
- else C().place(keys, t.id, axis);
+ C().place(keys, t.id, axis);
});
}
- // Tracking drop: if the target is already a complete leaf, assign directly;
- // otherwise prompt for the remaining levels (parsed + nested under it) so a
- // file can be dropped on an existing partial tracking number and completed.
- function placeTrackingDrop(keys, nodeId) {
- if (C().trackingNodeComplete(nodeId)) { C().place(keys, nodeId, 'tracking'); return; }
- var label = C().trackingPathLabel(nodeId);
- var input = prompt('Dropping under "' + label + '".\n'
- + 'Add the remaining tracking levels (e.g. "0001_0 (IFU)"), or leave blank to drop here:', '');
- if (input === null) return; // cancelled
- var levels = C().parseFolderLevels(input.trim());
- var target = levels.length ? C().addTrackingPath(nodeId, levels) : nodeId;
- C().place(keys, target, 'tracking');
- }
-
// Reveal a source key's placement in the target pane (source → target).
function reveal(key) {
var a = C().getAssignment(key);
@@ -12453,7 +12501,9 @@ X.B(E,Y);return E}return J}())
row.appendChild(go); row.appendChild(cancel);
box.appendChild(h); box.appendChild(p); box.appendChild(sel); box.appendChild(row);
back.appendChild(box);
- back.addEventListener('click', function (e) { if (e.target === back) finish(null); });
+ var pressedBackdrop = false;
+ back.addEventListener('mousedown', function (e) { pressedBackdrop = (e.target === back); });
+ back.addEventListener('click', function (e) { if (e.target === back && pressedBackdrop) finish(null); });
document.addEventListener('keydown', onKey);
document.body.appendChild(back);
});
@@ -12486,7 +12536,9 @@ X.B(E,Y);return E}return J}())
row.appendChild(btn('Cancel', 'btn-secondary', null));
box.appendChild(h); box.appendChild(p); box.appendChild(row);
back.appendChild(box);
- back.addEventListener('click', function (e) { if (e.target === back) finish(null); });
+ var pressedBackdrop = false;
+ back.addEventListener('mousedown', function (e) { pressedBackdrop = (e.target === back); });
+ back.addEventListener('click', function (e) { if (e.target === back && pressedBackdrop) finish(null); });
document.addEventListener('keydown', onKey);
document.body.appendChild(back);
});
@@ -14804,32 +14856,39 @@ X.B(E,Y);return E}return J}())
let resizingColumn = null;
let startX = 0;
let startWidth = 0;
+ let activeTable = null;
+ let activeOnResize = null;
/**
- * Initialize column resizing
+ * Initialize column resizing on a table. Defaults to the rename-in-place
+ * spreadsheet when no table is passed (back-compatible). onResize(table) is
+ * called after each drag ends, so a caller can persist the new widths.
*/
- function init() {
- const table = window.app.dom.spreadsheet;
+ function init(table, onResize) {
+ table = table || (window.app.dom && window.app.dom.spreadsheet);
+ if (!table) return;
const headers = table.querySelectorAll('thead th');
-
+
headers.forEach(th => {
// Skip if resize handle already exists
if (th.querySelector('.column-resizer')) return;
-
+
// Add resize handle
const resizer = document.createElement('div');
resizer.className = 'column-resizer';
th.appendChild(resizer);
-
+
// Mouse down on resizer
resizer.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
-
+
resizingColumn = th;
startX = e.pageX;
startWidth = th.offsetWidth;
-
+ activeTable = table;
+ activeOnResize = onResize || null;
+
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
});
@@ -14857,6 +14916,8 @@ X.B(E,Y);return E}return J}())
resizingColumn = null;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
+ if (activeOnResize && activeTable) { try { activeOnResize(activeTable); } catch (_) { /* ignore */ } }
+ activeTable = null; activeOnResize = null;
}
// Export module
diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html
index cc9b1ee..3526e8b 100644
--- a/zddc/internal/apps/embedded/index.html
+++ b/zddc/internal/apps/embedded/index.html
@@ -1793,7 +1793,7 @@ body {