Add explorer typeahead navigation

This commit is contained in:
Mikei386
2026-06-23 13:42:15 +02:00
parent d5f278aae2
commit c7b9bf0a41
5 changed files with 69 additions and 3 deletions
+66
View File
@@ -25,6 +25,11 @@ const state = {
transferPanelClosing: false,
transferAutoHideTimer: null,
settingsPanelVisible: false,
typeAhead: {
key: '',
windowId: null,
at: 0
},
nextWindowId: 1,
zIndex: 20
};
@@ -53,6 +58,7 @@ window.addEventListener('error', (event) => {
window.addEventListener('unhandledrejection', (event) => {
recordDebug('window.unhandledrejection', { reason: serializeError(event.reason) });
});
document.addEventListener('keydown', handleTypeAheadKey);
init();
@@ -1015,6 +1021,59 @@ function updateSelectedRows() {
}
}
function handleTypeAheadKey(event) {
if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
if (state.contextMenu || state.settingsPanelVisible) return;
if (isTypingTarget(document.activeElement)) return;
if (event.key.length !== 1) return;
const key = event.key.toLocaleLowerCase();
if (!/^[\p{L}\p{N}]$/u.test(key)) return;
const win = state.windows.find((item) => item.id === state.focusedId && item.kind === 'explorer');
if (!win || !win.data.entries?.length) return;
const now = Date.now();
const repeated = state.typeAhead.windowId === win.id && state.typeAhead.key === key && now - state.typeAhead.at < 1000;
const entries = win.data.entries;
const selectedIndex = entries.findIndex((entry) => entry.path === win.data.selectedPath);
const startIndex = repeated ? selectedIndex + 1 : Math.max(0, selectedIndex + 1);
const match = findTypeAheadMatch(entries, key, startIndex);
if (!match) return;
event.preventDefault();
state.typeAhead = { key, windowId: win.id, at: now };
win.data.selectedPath = match.path;
state.selectedEntry = match;
focusWindow(win);
updateSelectedRows();
updatePropertiesWindows();
scrollEntryIntoView(win, match.path);
recordDebug('typeahead.match', { key, path: match.path });
}
function findTypeAheadMatch(entries, key, startIndex) {
for (let offset = 0; offset < entries.length; offset += 1) {
const index = (startIndex + offset) % entries.length;
const entry = entries[index];
if (entry.name?.toLocaleLowerCase().startsWith(key)) {
return entry;
}
}
return null;
}
function scrollEntryIntoView(win, path) {
const row = workspace.querySelector(`.window[data-window-id="${win.id}"] .file-row[data-path="${cssEscape(path)}"]`);
row?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
function isTypingTarget(element) {
if (!element) return false;
const tag = element.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || element.isContentEditable;
}
function updatePropertiesWindows() {
for (const win of state.windows.filter((item) => item.kind === 'properties')) {
const body = workspace.querySelector(`.window[data-window-id="${win.id}"] .window-body`);
@@ -1419,6 +1478,13 @@ function escapeAttr(value) {
return escapeHtml(value);
}
function cssEscape(value) {
if (window.CSS?.escape) {
return window.CSS.escape(String(value));
}
return String(value).replace(/["\\]/g, '\\$&');
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}