diff --git a/browse/js/events.js b/browse/js/events.js index afeb215..6872d0a 100644 --- a/browse/js/events.js +++ b/browse/js/events.js @@ -307,6 +307,109 @@ navigateIntoFolder(node); }); + // Keyboard navigation in the tree. Document-level listener so + // the user doesn't have to click into the tree first; bails + // out cleanly when focus is in an editable field or when a + // modal / context-menu owns the keys. Roving-tabindex-style + // semantics, matching the W3C tree-view pattern: + // + // ↓ / ↑ — move selection (auto-previews files) + // → — expand if collapsed; jump to first child + // if already expanded; no-op otherwise + // ← — collapse if expanded; jump to parent + // if collapsed/leaf + // Enter / Space — preview file / toggle folder + // Home / End — first / last visible row + document.addEventListener('keydown', function (e) { + // Skip editable contexts. + var tag = (e.target && e.target.tagName) || ''; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; + if (e.target && e.target.isContentEditable) return; + // Skip when a modal or context menu is open. + if (document.querySelector('.modal-overlay, .zddc-menu')) return; + // Skip if any modifier is pressed — lets Ctrl-F, Cmd-T, + // Alt-arrow back/forward etc. fall through unchanged. + if (e.ctrlKey || e.metaKey || e.altKey) return; + + var key = e.key; + var navKey = key === 'ArrowDown' || key === 'ArrowUp' + || key === 'ArrowLeft' || key === 'ArrowRight' + || key === 'Home' || key === 'End' + || key === 'Enter' || key === ' '; + if (!navKey) return; + + var visible = tree.visibleIds(); + if (!visible.length) return; + + // Commit to handling this key — preventDefault so the + // browser doesn't also scroll on arrows / page-down on + // Space. Selection / expand actions happen below. + e.preventDefault(); + + var curIdx = visible.indexOf(state.selectedId); + var node = state.selectedId != null + ? state.nodes.get(state.selectedId) : null; + var expandable = !!(node && (node.isDir || node.isZip)); + var nextId = null; + var previewModule = previewMod(); + + if (key === 'ArrowDown') { + nextId = curIdx < 0 + ? visible[0] + : visible[Math.min(curIdx + 1, visible.length - 1)]; + } else if (key === 'ArrowUp') { + nextId = curIdx < 0 + ? visible[visible.length - 1] + : visible[Math.max(curIdx - 1, 0)]; + } else if (key === 'Home') { + nextId = visible[0]; + } else if (key === 'End') { + nextId = visible[visible.length - 1]; + } else if (key === 'ArrowRight' && node) { + if (expandable && !node.expanded) { + tree.toggleFolder(node.id); + return; + } + if (expandable && node.expanded + && node.childIds && node.childIds.length) { + nextId = node.childIds[0]; + } + } else if (key === 'ArrowLeft' && node) { + if (expandable && node.expanded) { + tree.toggleFolder(node.id); + return; + } + if (node.parentId != null) { + nextId = node.parentId; + } + } else if ((key === 'Enter' || key === ' ') && node) { + if (expandable) { + tree.toggleFolder(node.id); + } else if (previewModule) { + previewModule.showFilePreview(node); + state.lastPreviewedNodeId = node.id; + } + return; + } + + if (nextId == null) return; + state.selectedId = nextId; + var nextNode = state.nodes.get(nextId); + tree.render(); + // Auto-preview files as the keyboard cursor lands on them + // so the right pane keeps up with selection. Folders are + // selection-only; their preview is "expand to see inside". + if (nextNode && !nextNode.isDir && !nextNode.isZip + && previewModule) { + previewModule.showFilePreview(nextNode); + state.lastPreviewedNodeId = nextId; + } + // Scroll the now-selected row into view. + var newRow = treeBody.querySelector( + '.tree-row[data-id="' + nextId + '"]'); + if (newRow) newRow.scrollIntoView({ block: 'nearest' }); + }); + // Right-click → context menu. Two surfaces: // - on a tree row: per-row menu (Open, Rename, Delete, …) // - on empty space in the pane: directory-scope menu diff --git a/browse/js/tree.js b/browse/js/tree.js index c9265b2..da8a372 100644 --- a/browse/js/tree.js +++ b/browse/js/tree.js @@ -691,6 +691,7 @@ state.sort.dir = (dir === -1 ? -1 : 1); render(); }, - pathFor: pathFor + pathFor: pathFor, + visibleIds: visibleIds }; })();