Optimizing keyboard navigation and selection handling

This commit is contained in:
2026-03-21 18:08:13 +01:00
parent 853bc4abae
commit f887267362
5 changed files with 82 additions and 25 deletions

View File

@@ -1,7 +1,15 @@
/**
* Create keyboard bindings
*/
// Set window.KEYBOARD_DEBUG = true in the browser console to enable traces
window.KEYBOARD_DEBUG = false;
(function () {
function kbLog(...args) {
if (window.KEYBOARD_DEBUG) console.debug('[Keyboard]', ...args);
}
/**
* Global registry to store keyboard shortcuts for multiple elements
*/
@@ -172,8 +180,11 @@
// Add snapshot to history
KeyboardRegistry.snapshotHistory.push(snapshot);
kbLog(`key="${key}" | history length=${KeyboardRegistry.snapshotHistory.length} | registeredElements=${KeyboardRegistry.elements.size}`);
// Cancel any pending timeout
if (KeyboardRegistry.pendingTimeout) {
kbLog(` cancelled pending timeout`);
clearTimeout(KeyboardRegistry.pendingTimeout);
KeyboardRegistry.pendingTimeout = null;
KeyboardRegistry.pendingMatches = [];
@@ -198,8 +209,7 @@
const currentNode = traverseTree(treeRoot, KeyboardRegistry.snapshotHistory);
if (!currentNode) {
// No match in this tree, continue to next element
// console.debug("No match in tree for event", key);
kbLog(` element="${elementId}" → no match in tree`);
continue;
}
@@ -212,6 +222,8 @@
// Check if there are longer sequences possible (node has children)
const hasLongerSequences = currentNode.children.size > 0;
kbLog(` element="${elementId}" | isInside=${isInside} | hasMatch=${hasMatch} | hasLongerSequences=${hasLongerSequences}`);
// Track if ANY element has longer sequences possible
if (hasLongerSequences) {
anyHasLongerSequence = true;
@@ -221,6 +233,7 @@
if (hasMatch) {
const requireInside = currentNode.config["require_inside"] === true;
const enabled = isCombinationEnabled(data.controlDivId, currentNode.combinationStr);
kbLog(` combination="${currentNode.combinationStr}" | requireInside=${requireInside} | enabled=${enabled}`);
if (enabled && (!requireInside || isInside)) {
currentMatches.push({
elementId: elementId,
@@ -228,10 +241,14 @@
combinationStr: currentNode.combinationStr,
isInside: isInside
});
} else {
kbLog(` → skipped (requireInside=${requireInside} but isInside=${isInside}, or disabled)`);
}
}
}
kbLog(` result: matches=${currentMatches.length} | anyHasLongerSequence=${anyHasLongerSequence}`);
// Prevent default if we found any match and not in input context
if (currentMatches.length > 0 && !isInInputContext()) {
event.preventDefault();
@@ -239,6 +256,7 @@
// Decision logic based on matches and longer sequences
if (currentMatches.length > 0 && !anyHasLongerSequence) {
kbLog(` → TRIGGER immediately`);
// We have matches and NO element has longer sequences possible
// Trigger ALL matches immediately
for (const match of currentMatches) {
@@ -249,6 +267,7 @@
KeyboardRegistry.snapshotHistory = [];
} else if (currentMatches.length > 0 && anyHasLongerSequence) {
kbLog(` → WAITING ${KeyboardRegistry.sequenceTimeout}ms (longer sequence possible)`);
// We have matches but AT LEAST ONE element has longer sequences possible
// Wait for timeout - ALL current matches will be triggered if timeout expires
@@ -256,6 +275,7 @@
const savedEvent = event; // Save event for timeout callback
KeyboardRegistry.pendingTimeout = setTimeout(() => {
kbLog(` → TRIGGER after timeout`);
// Timeout expired, trigger ALL pending matches
for (const match of KeyboardRegistry.pendingMatches) {
triggerHtmxAction(match.elementId, match.config, match.combinationStr, match.isInside, savedEvent);
@@ -268,10 +288,12 @@
}, KeyboardRegistry.sequenceTimeout);
} else if (currentMatches.length === 0 && anyHasLongerSequence) {
kbLog(` → WAITING (partial match, no full match yet)`);
// No matches yet but longer sequences are possible
// Just wait, don't trigger anything
} else {
kbLog(` → NO MATCH, clearing history`);
// No matches and no longer sequences possible
// This is an invalid sequence - clear history
KeyboardRegistry.snapshotHistory = [];
@@ -280,11 +302,13 @@
// If we found no match at all, clear the history
// This handles invalid sequences like "A C" when only "A B" exists
if (!foundAnyMatch) {
kbLog(` → foundAnyMatch=false, clearing history`);
KeyboardRegistry.snapshotHistory = [];
}
// Also clear history if it gets too long (prevent memory issues)
if (KeyboardRegistry.snapshotHistory.length > 10) {
kbLog(` → history too long, clearing`);
KeyboardRegistry.snapshotHistory = [];
}
}