Three issues from initial v0.0.12 dev/prod testing:
1. Online listings empty.
directory.go was missing Vary: Accept on its responses, so
browser/CDN cached the HTML response (the embedded browse.html)
and served it again when browse's JS later fetched the same URL
with Accept: application/json. JSON parse failed, autoDetect
returned null, empty state showed. Adds Vary: Accept on both
branches and changes browse.html cache-control to no-cache so
deployed updates land immediately.
2. Top-level folder rows tall, shrink as subtree expands.
The .browse-table had flex:1 in a flex column. <table> in flex
doesn't reliably distribute height across rows — with few rows,
each row stretched. Wrap the table in a div with overflow:auto
and drop flex:1 from the table itself.
3. Recursive expand/collapse.
Shift-click (or alt-click) on a folder now expand-all or
collapse-all its subtree. Plain click still toggles just that
folder. Implementation: tree.expandSubtree() walks BFS, loading
each level's children in parallel, re-rendering between levels
so the user sees progress. tree.collapseSubtree() recursively
marks the subtree collapsed (children stay loaded for instant
re-expand).
125 lines
4.5 KiB
JavaScript
125 lines
4.5 KiB
JavaScript
// events.js — wires up DOM listeners. Idempotent so app.js can call
|
|
// init() once on load.
|
|
(function () {
|
|
'use strict';
|
|
|
|
var state = window.app.state;
|
|
var tree = window.app.modules.tree;
|
|
var loader = window.app.modules.loader;
|
|
|
|
function status(msg, kind) {
|
|
var el = document.getElementById('statusBar');
|
|
if (!el) return;
|
|
el.textContent = msg || '';
|
|
el.classList.remove('status-bar--error', 'status-bar--info');
|
|
if (kind === 'error') el.classList.add('status-bar--error');
|
|
if (kind === 'info') el.classList.add('status-bar--info');
|
|
}
|
|
|
|
function statusError(msg) { status(msg, 'error'); }
|
|
function statusInfo(msg) { status(msg, 'info'); }
|
|
function statusClear() { status('', null); }
|
|
|
|
async function pickLocalDir() {
|
|
if (typeof window.showDirectoryPicker !== 'function') {
|
|
statusError('Your browser does not support local folder selection. Use a recent Chromium-based browser, or open this page via zddc-server.');
|
|
return;
|
|
}
|
|
var handle;
|
|
try {
|
|
handle = await window.showDirectoryPicker({ mode: 'read' });
|
|
} catch (e) {
|
|
// User cancelled — silent
|
|
return;
|
|
}
|
|
state.source = 'fs';
|
|
state.rootHandle = handle;
|
|
state.currentPath = handle.name + '/';
|
|
var raw;
|
|
try {
|
|
raw = await loader.fetchFsChildren(handle);
|
|
} catch (e) {
|
|
statusError('Failed to read directory: ' + e.message);
|
|
return;
|
|
}
|
|
tree.setRoot(raw);
|
|
showBrowseRoot();
|
|
document.getElementById('currentPath').textContent = state.currentPath;
|
|
tree.render();
|
|
statusInfo('Loaded ' + raw.length + ' item' + (raw.length === 1 ? '' : 's'));
|
|
}
|
|
|
|
function showBrowseRoot() {
|
|
var empty = document.getElementById('emptyState');
|
|
var root = document.getElementById('browseRoot');
|
|
if (empty) empty.classList.add('hidden');
|
|
if (root) root.classList.remove('hidden');
|
|
}
|
|
|
|
function init() {
|
|
// Header buttons
|
|
var btn = document.getElementById('addDirectoryBtn');
|
|
if (btn) btn.addEventListener('click', pickLocalDir);
|
|
|
|
// Filter input
|
|
var filter = document.getElementById('filterInput');
|
|
if (filter) {
|
|
filter.addEventListener('input', function () {
|
|
tree.setFilter(filter.value);
|
|
});
|
|
}
|
|
|
|
// Sort headers
|
|
var ths = document.querySelectorAll('#browseTable thead th.sortable');
|
|
for (var i = 0; i < ths.length; i++) {
|
|
(function (th) {
|
|
th.addEventListener('click', function () {
|
|
tree.setSort(th.dataset.sort);
|
|
});
|
|
})(ths[i]);
|
|
}
|
|
|
|
// Tree-row clicks (event delegation on tbody).
|
|
// Click semantics on a folder row:
|
|
// - plain click → toggle just this folder
|
|
// - shift-click → recursive expand/collapse of the whole
|
|
// subtree (matches common file-explorer
|
|
// convention; e.g. Finder, VSCode tree,
|
|
// Windows Explorer)
|
|
// - alt-click → ALSO recursive (alt is sometimes the
|
|
// expand-all key on Linux DEs; bind both
|
|
// so muscle memory works either way)
|
|
// File rows: let the <a> tag's natural target=_blank do its
|
|
// job — don't intercept.
|
|
var tbody = document.getElementById('browseTbody');
|
|
if (tbody) {
|
|
tbody.addEventListener('click', function (e) {
|
|
var row = e.target.closest('tr.tree-row');
|
|
if (!row) return;
|
|
var isDir = row.dataset.isdir === 'true';
|
|
if (!isDir) return;
|
|
e.preventDefault();
|
|
var id = parseInt(row.dataset.id, 10);
|
|
if (e.shiftKey || e.altKey) {
|
|
var node = state.nodes.get(id);
|
|
if (node && node.expanded) {
|
|
tree.collapseSubtree(id);
|
|
} else {
|
|
tree.expandSubtree(id);
|
|
}
|
|
} else {
|
|
tree.toggleFolder(id);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Public API
|
|
window.app.modules.events = {
|
|
init: init,
|
|
statusError: statusError,
|
|
statusInfo: statusInfo,
|
|
statusClear: statusClear,
|
|
showBrowseRoot: showBrowseRoot
|
|
};
|
|
})();
|