Major upgrade to the browse tool's UX, plus a few shared modules other tools can adopt. User-facing: - Right-click context menu on tree rows AND empty pane space. Traditional file-manager grouping (Open / Download / New / Rename-Delete / Copy / Tree ops / View). Items stay visible but disabled when not applicable so muscle memory carries. Generic shared/context-menu.js framework supports normal items, toggles, submenus, separators, danger styling. - YAML editor for .yaml / .yml / .zddc files (CodeMirror 5 vendored at shared/vendor/codemirror-yaml.min.*). js-yaml lint on every change for parse errors. For .zddc cascade files, an additional schema-aware lint pass flags unknown keys, bad enum values, and wrong types. - Per-row drag-drop upload using webkitGetAsEntry (folder uploads work recursively). Per-row drop indicator; doc-level overlay still fires for blank-space drops at drop_target scopes. - New folder / New markdown file context-menu items (server mode). Rename + Delete with native confirm() dialog. File-API helpers removeNode / renameNode use the existing PUT/POST/DELETE endpoints. - Hover info card with the row's full metadata (ZDDC fields + filesystem info + path/URL). Interactive — mouse into it, drag-select text, Ctrl/Cmd-C or right-click → Copy. 200ms grace before dismiss. - Autofilter input at the top of the tree pane. Same grammar as archive's column filters (zddc.filter.parse / matches). Filters files; folders without matches collapse out. Non-matching folders force-open visually when descendants match, without mutating the user's actual expand state. - Two-line ZDDC label: title-first, tracking/rev/status as monospace meta below. Icon column anchors to the title line. Chevron is a Lucide outline `chevron-right` SVG, rotated 90° on `.expanded`. - File-type Lucide icon sprite (shared/icons.js — 16 outline glyphs, ~5 KB). PDF / Word / Spreadsheet / Slides / Image / Video / Audio / CAD / Web / Config / Code / Archive get distinct icons; folders tinted with --primary. - Header wraps gracefully at narrow viewports (shared/base.css flex-wrap + title min-width:0 ellipsis). Body becomes flex column in browse so a wrapping header doesn't break #appMain height. - Markdown editor opens in WYSIWYG mode by default. YAML front-matter + TOC sidebar reworked: flexbox layout (single visible resizer between FM and TOC), both bodies overflow:auto for X+Y scrollbars. - `?file=<path>` deep links open browse pre-positioned at a specific file. Multi-segment paths walk into subdirectories on the way. Auto-flips Show hidden when a segment is dot/underscore-prefixed. - Refresh + show-hidden toggle preserve expansion / selection / preview pinning. Path-keyed snapshot survives a re-fetched listing. - "Add Local Directory" → "Use Local Directory" across the four tools that have it (browse, archive, classifier, +transmittal comment). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
6.8 KiB
JavaScript
150 lines
6.8 KiB
JavaScript
// app.js — bootstrap. Runs after every other module's IIFE has
|
|
// registered its functions on window.app.modules.
|
|
(function () {
|
|
'use strict';
|
|
|
|
var state = window.app.state;
|
|
var loader = window.app.modules.loader;
|
|
var tree = window.app.modules.tree;
|
|
var events = window.app.modules.events;
|
|
|
|
// Virtual canonical folder injection used to live here (browse
|
|
// appended archive/working/staging/reviewing entries at a project
|
|
// root when missing). zddc-server now emits them in the listing
|
|
// directly so the .zddc `display:` map can override their labels
|
|
// the same as real entries. This pass-through stub keeps the
|
|
// events.js rescope contract intact without doing any merging.
|
|
function passThroughEntries(entries) { return entries; }
|
|
|
|
// Expose for events.js's client-side rescope on dblclick.
|
|
window.app.modules.augmentRoot = passThroughEntries;
|
|
|
|
// Walk a `?file=` path segment-by-segment from the current root.
|
|
// Each non-leaf segment is matched against the parent's children
|
|
// by name; if found and it's a folder, expand+load it (so its
|
|
// children populate state.nodes) and recurse into them. The leaf
|
|
// segment becomes the selected/previewed entry. Silently no-ops
|
|
// when any segment doesn't resolve — deep links aren't a hard
|
|
// contract, just an affordance.
|
|
async function openDeepLink(path) {
|
|
var segs = path.split('/').filter(Boolean);
|
|
if (segs.length === 0) return;
|
|
var tree = window.app.modules.tree;
|
|
var prev = window.app.modules.preview;
|
|
|
|
// Lookup helper: find a node by name within a given parent's
|
|
// immediate children. Top-level walk uses state.rootIds.
|
|
function findChild(parentIds, name) {
|
|
for (var i = 0; i < parentIds.length; i++) {
|
|
var n = window.app.state.nodes.get(parentIds[i]);
|
|
if (n && n.name === name) return n;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
var ids = window.app.state.rootIds;
|
|
for (var i = 0; i < segs.length; i++) {
|
|
var node = findChild(ids, segs[i]);
|
|
if (!node) return; // segment not present in this listing
|
|
if (i === segs.length - 1) {
|
|
// Leaf — select + preview.
|
|
window.app.state.selectedId = node.id;
|
|
window.app.state.lastPreviewedNodeId = node.id;
|
|
tree.render();
|
|
if (prev && !node.isDir) prev.showFilePreview(node);
|
|
return;
|
|
}
|
|
// Intermediate — must be a folder we can expand into.
|
|
if (!(node.isDir || node.isZip)) return;
|
|
if (!node.loaded) {
|
|
await tree.toggleFolder(node.id); // loads + sets expanded
|
|
} else if (!node.expanded) {
|
|
node.expanded = true;
|
|
}
|
|
ids = node.childIds;
|
|
}
|
|
}
|
|
|
|
async function bootstrap() {
|
|
events.init();
|
|
|
|
// Honor ?file=<path> deep links: external clients (the profile
|
|
// page's "edit your .zddc files" list, future bookmarks, etc.)
|
|
// can link directly to "open browse at <dir>, with this entry
|
|
// selected and previewed". Single-segment names (?file=foo.md)
|
|
// match in the current directory; multi-segment paths
|
|
// (?file=a/b/foo.md) walk into a/ then b/ then open foo.md,
|
|
// loading intermediate directories on the way.
|
|
//
|
|
// When the LEAF (or any intermediate segment) is hidden
|
|
// (.zddc, .form.yaml, …), flip showHidden ON BEFORE the
|
|
// initial listing fetch so dotfiles appear in the tree.
|
|
var qs = new URLSearchParams(location.search);
|
|
var deepFile = qs.get('file');
|
|
if (deepFile) {
|
|
var segs = deepFile.split('/').filter(Boolean);
|
|
for (var si = 0; si < segs.length; si++) {
|
|
var c = segs[si].charAt(0);
|
|
if (c === '.' || c === '_') { state.showHidden = true; break; }
|
|
}
|
|
}
|
|
|
|
// Try server auto-detect. If this page is served by zddc-server
|
|
// (or any server with a Caddy-shaped JSON listing), load the
|
|
// current directory automatically. Otherwise show the empty
|
|
// state with the "Select Directory" button.
|
|
var detected = await loader.autoDetectServerMode();
|
|
if (detected) {
|
|
tree.setRoot(detected.entries);
|
|
events.showBrowseRoot();
|
|
tree.render();
|
|
events.statusInfo('Loaded ' + detected.entries.length + ' item'
|
|
+ (detected.entries.length === 1 ? '' : 's')
|
|
+ ' from ' + detected.path);
|
|
// The initial events.init() applied view mode before the
|
|
// cascade headers were available (no fetch yet). Now that
|
|
// state.scopeDefaultTool is set from the detection
|
|
// response, re-resolve so an /incoming URL auto-activates
|
|
// grid mode.
|
|
if (events.applyResolvedViewMode) events.applyResolvedViewMode();
|
|
|
|
// Final step of the deep link: walk the path segment by
|
|
// segment, expanding + loading intermediate directories
|
|
// before opening the leaf. Single-segment names use the
|
|
// same code path with one iteration.
|
|
if (deepFile) {
|
|
await openDeepLink(deepFile);
|
|
}
|
|
}
|
|
// Else: empty state stays visible; user can click Select Directory.
|
|
|
|
// Browser back / forward: client-side rescope when the URL
|
|
// changes via popstate. We can't tell server-vs-fs mode from
|
|
// popstate alone, so only honor it in server mode.
|
|
window.addEventListener('popstate', async function () {
|
|
if (window.app.state.source !== 'server') return;
|
|
var path = location.pathname;
|
|
if (!path.endsWith('/')) path += '/';
|
|
try {
|
|
var es = await loader.fetchServerChildren(path);
|
|
window.app.state.currentPath = path;
|
|
window.app.state.selectedId = null;
|
|
window.app.state.lastPreviewedNodeId = null;
|
|
tree.setRoot(es);
|
|
tree.render();
|
|
var previewBody = document.getElementById('previewBody');
|
|
if (previewBody) previewBody.innerHTML = '';
|
|
var previewTitle = document.getElementById('previewTitle');
|
|
if (previewTitle) previewTitle.textContent = 'No file selected';
|
|
// Reapply view mode for the new URL (incoming/ → grid, etc).
|
|
if (events.applyResolvedViewMode) events.applyResolvedViewMode();
|
|
} catch (_e) { /* swallow — leave the tree as-is */ }
|
|
});
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', bootstrap);
|
|
} else {
|
|
bootstrap();
|
|
}
|
|
})();
|