// 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; // preview module is loaded later (concat order); look it up at // call time, not at IIFE-eval time. function previewMod() { return window.app.modules.preview; } 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(); 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'); applySourceUI(); } // Visual state of the "Select Directory" button + the refresh // button depends on the source. In server mode the user is // already viewing a server-backed listing — Select Directory // becomes a quiet "switch to local" affordance (subtle styling), // and the refresh button is shown. In FS mode the button is // primary (it's how you got here) and refresh is hidden (the // listing was already a fresh enumeration). function applySourceUI() { var add = document.getElementById('addDirectoryBtn'); var refresh = document.getElementById('refreshHeaderBtn'); if (add) { if (state.source === 'server') { add.classList.remove('btn-primary'); add.classList.add('btn--subtle'); } else { add.classList.add('btn-primary'); add.classList.remove('btn--subtle'); } } if (refresh) { if (state.source) { refresh.classList.remove('hidden'); } else { refresh.classList.add('hidden'); } } } async function refreshListing() { if (state.source === 'server') { var raw; try { raw = await loader.fetchServerChildren(state.currentPath); } catch (e) { statusError('Refresh failed: ' + e.message); return; } tree.setRoot(raw); tree.render(); statusInfo('Refreshed (' + raw.length + ' item' + (raw.length === 1 ? '' : 's') + ')'); } else if (state.source === 'fs' && state.rootHandle) { var raw2; try { raw2 = await loader.fetchFsChildren(state.rootHandle); } catch (e) { statusError('Refresh failed: ' + e.message); return; } tree.setRoot(raw2); tree.render(); statusInfo('Refreshed'); } } function init() { // Header buttons var btn = document.getElementById('addDirectoryBtn'); if (btn) btn.addEventListener('click', pickLocalDir); var refresh = document.getElementById('refreshHeaderBtn'); if (refresh) refresh.addEventListener('click', refreshListing); // Auto-filter row inputs. There are three of them (file, folder, // ext) — wire each by its `data-filter` attribute. Idempotent // re: re-init. var filterInputs = document.querySelectorAll('input.column-filter[data-filter]'); for (var fi = 0; fi < filterInputs.length; fi++) { (function (input) { var which = input.dataset.filter; input.addEventListener('input', function () { tree.setFilter(which, input.value); }); })(filterInputs[fi]); } // 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 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 id = parseInt(row.dataset.id, 10); var node = state.nodes.get(id); if (!node) return; var isExpandable = row.dataset.isdir === 'true' || row.dataset.iszip === 'true'; var clickedChevron = !!e.target.closest('.tree-name__chevron'); if (isExpandable) { // For folders + zips: click anywhere on the row // toggles. Modifier-click → recursive expand. e.preventDefault(); if (e.shiftKey || e.altKey) { if (node.expanded) tree.collapseSubtree(id); else tree.expandSubtree(id); } else { tree.toggleFolder(id); } return; } // Plain file row. // Modifier-click (ctrl/cmd) and middle-click → fall // through to the tag's natural target=_blank // behavior (open in new tab). For server-backed // files, that opens the real URL via zddc-server. if (e.ctrlKey || e.metaKey || e.shiftKey || e.button === 1) { return; } // Plain click → preview popup. Intercept default nav. e.preventDefault(); var p = previewMod(); if (p) p.showFilePreview(node); }); // Middle-click (auxclick) — same fall-through logic. tbody.addEventListener('auxclick', function (e) { if (e.button !== 1) return; // middle only // Browser handles target=_blank natively for middle // click; don't preventDefault, just don't intercept. }); } } // Public API window.app.modules.events = { init: init, statusError: statusError, statusInfo: statusInfo, statusClear: statusClear, showBrowseRoot: showBrowseRoot }; })();