feat: function key mapping
This commit is contained in:
@@ -0,0 +1,288 @@
|
|||||||
|
# UI_ACTION_SHORTCUTS_V1_DESIGN.md
|
||||||
|
|
||||||
|
## 1. Doel en scope
|
||||||
|
|
||||||
|
Action shortcuts v1 maken de bestaande functiebalk sneller bruikbaar zonder de UI-semantiek te veranderen. Het doel is niet om nieuwe acties te ontwerpen, maar om de bestaande acties via toetsen te starten op exact dezelfde manier als via de functiebalk.
|
||||||
|
|
||||||
|
Doel:
|
||||||
|
- de huidige functiebalkacties sneller bereikbaar maken
|
||||||
|
- de dual-pane workflow dichter bij een Midnight Commander-achtige bediening brengen
|
||||||
|
- een bruikbare eerste set bieden die ook op Mac praktisch inzetbaar blijft
|
||||||
|
|
||||||
|
In scope:
|
||||||
|
- keyboard-start van bestaande functiebalkacties
|
||||||
|
- centrale shortcut-dispatch op basis van `activePane` en huidige selectie
|
||||||
|
- veilige focusguards zodat invoervelden en modals niet worden verstoord
|
||||||
|
- voorbereiding op latere visuele hints in de functiebalk
|
||||||
|
|
||||||
|
Out of scope:
|
||||||
|
- geen backendwijzigingen
|
||||||
|
- geen nieuwe acties buiten de bestaande functiebalk
|
||||||
|
- geen globale override van agressieve browser- of OS-shortcuts
|
||||||
|
- geen nieuwe dependencies
|
||||||
|
- geen extra keyboardnavigatie buiten action-triggering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Acties in scope
|
||||||
|
|
||||||
|
Deze ontwerpstap werkt uit voor:
|
||||||
|
|
||||||
|
- `View`
|
||||||
|
- `Edit`
|
||||||
|
- `Copy`
|
||||||
|
- `Move`
|
||||||
|
- `Rename`
|
||||||
|
- `MKdir`
|
||||||
|
- `Delete`
|
||||||
|
|
||||||
|
Semantisch uitgangspunt:
|
||||||
|
- elke shortcut triggert exact dezelfde action-handler als de corresponderende knop in de functiebalk
|
||||||
|
- enabled/disabled logica blijft centraal en wordt niet dubbel geïmplementeerd in aparte keyboardlogica
|
||||||
|
- als een knop in de huidige context disabled zou zijn, doet de shortcut geen destructieve fallback en start geen alternatieve flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Toetsmapping
|
||||||
|
|
||||||
|
### Primaire mapping: F3 t/m F8
|
||||||
|
|
||||||
|
Voorgestelde primaire mapping:
|
||||||
|
|
||||||
|
- `F3` = `View`
|
||||||
|
- `F4` = `Edit`
|
||||||
|
- `F5` = `Copy`
|
||||||
|
- `F6` = `Move`
|
||||||
|
- `F7` = `MKdir`
|
||||||
|
- `F8` = `Delete`
|
||||||
|
|
||||||
|
Aanvullend:
|
||||||
|
- `Rename` krijgt in v1 geen vaste F-toets, omdat de klassieke mapping al dicht bezet is en `Rename` functioneel dichter bij contextactie dan bij de vaste MC-kern ligt
|
||||||
|
- `Rename` krijgt daarom een aparte, browser-veilige alternatieve shortcut
|
||||||
|
|
||||||
|
### Mac-vriendelijke alternatieven
|
||||||
|
|
||||||
|
F-toetsen zijn op Mac-toetsenborden vaak verborgen achter systeemfuncties of alleen bruikbaar met extra modifiers. Daarom krijgt v1 ook expliciete alternatieven.
|
||||||
|
|
||||||
|
Voorstel:
|
||||||
|
- `Alt+3` = `View`
|
||||||
|
- `Alt+4` = `Edit`
|
||||||
|
- `Alt+5` = `Copy`
|
||||||
|
- `Alt+6` = `Move`
|
||||||
|
- `Alt+7` = `MKdir`
|
||||||
|
- `Alt+8` = `Delete`
|
||||||
|
- `Alt+R` = `Rename`
|
||||||
|
|
||||||
|
Motivatie:
|
||||||
|
- numerieke mapping blijft mentaal gekoppeld aan `F3`-`F8`
|
||||||
|
- `Alt+R` is kort, semantisch duidelijk en minder conflictgevoelig dan veel andere browsercombinaties
|
||||||
|
- deze combinaties zijn compacter en realistischer op Mac dan het afdwingen van functietoetsgebruik
|
||||||
|
|
||||||
|
### Veiligheid in browsercontext
|
||||||
|
|
||||||
|
V1 kiest bewust geen mappings die sterk botsen met standaard browsergedrag, zoals:
|
||||||
|
- `Cmd+L`
|
||||||
|
- `Cmd+R`
|
||||||
|
- `Cmd+W`
|
||||||
|
- `Cmd+F`
|
||||||
|
- `Cmd+S`
|
||||||
|
- `Cmd+P`
|
||||||
|
- `Cmd+Backspace`
|
||||||
|
|
||||||
|
Ook geen pure lettershortcuts zonder modifier, omdat die te snel botsen met focus, tekstinvoer en toekomstige editorinteractie.
|
||||||
|
|
||||||
|
Belangrijk:
|
||||||
|
- `F3`-`F8` worden alleen gebruikt als extra shortcutlaag waar de browser/het OS dit toelaat
|
||||||
|
- de `Alt+...` varianten vormen de praktische browservriendelijke fallback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Focusregels
|
||||||
|
|
||||||
|
Shortcuts zijn alleen actief als de UI in browse-/paneelmodus zit en focus niet in een interactieve control staat.
|
||||||
|
|
||||||
|
### Shortcuts actief wanneer
|
||||||
|
- focus op `body`, paneelcontainer of bestandslijst staat
|
||||||
|
- geen modal met eigen invoer open is
|
||||||
|
- geen wildcard-popup open is
|
||||||
|
- geen editor-textarea focus heeft
|
||||||
|
- geen inputcontrol focus heeft
|
||||||
|
|
||||||
|
### Shortcuts niet actief wanneer focus in
|
||||||
|
- `input`
|
||||||
|
- `textarea`
|
||||||
|
- `select`
|
||||||
|
- `button`
|
||||||
|
- checkbox
|
||||||
|
- elk element met `contenteditable`
|
||||||
|
|
||||||
|
### Extra modalregels
|
||||||
|
- als `View` modal open is: geen action shortcuts voor onderliggende paneelacties
|
||||||
|
- als `Edit` modal open is: geen action shortcuts voor onderliggende paneelacties
|
||||||
|
- als wildcard popup open is: action shortcuts uitgeschakeld behalve popup-eigen `Enter`/`Escape`
|
||||||
|
|
||||||
|
### Browser/OS-collision mitigatie
|
||||||
|
- alleen exact ondersteunde combinaties afvangen
|
||||||
|
- geen brede `preventDefault()` voor andere toetsen
|
||||||
|
- bij twijfel niet afvangen; browser/OS behoudt dan prioriteit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Relatie met functiebalk en selectie
|
||||||
|
|
||||||
|
Kernregel:
|
||||||
|
- keyboard shortcuts gebruiken exact dezelfde centrale action-functies als de functiebalkknoppen
|
||||||
|
|
||||||
|
Dat betekent:
|
||||||
|
- `View` shortcut roept dezelfde open-viewer flow aan als de `View` knop
|
||||||
|
- `Edit` shortcut roept dezelfde open-editor flow aan als de `Edit` knop
|
||||||
|
- `Copy`/`Move` shortcut gebruiken dezelfde batch-startlogica als de bestaande knoppen
|
||||||
|
- `Rename`, `MKdir`, `Delete` shortcut gebruiken dezelfde validatie- en UI-flow als de bestaande knoppen
|
||||||
|
|
||||||
|
### Geen selectie
|
||||||
|
- `View`: geen actie
|
||||||
|
- `Edit`: geen actie
|
||||||
|
- `Copy`: geen actie
|
||||||
|
- `Move`: geen actie
|
||||||
|
- `Rename`: geen actie
|
||||||
|
- `MKdir`: wel toegestaan, op actief paneel
|
||||||
|
- `Delete`: geen actie
|
||||||
|
|
||||||
|
### Exact 1 selectie
|
||||||
|
- `View`: alleen voor ondersteunde file
|
||||||
|
- `Edit`: alleen voor ondersteunde tekstfile
|
||||||
|
- `Copy`: alleen als selectie backend-geldig is
|
||||||
|
- `Move`: alleen als selectie backend-geldig is
|
||||||
|
- `Rename`: toegestaan
|
||||||
|
- `MKdir`: toegestaan
|
||||||
|
- `Delete`: toegestaan
|
||||||
|
|
||||||
|
### Meerdere selecties
|
||||||
|
- `View`: niet toegestaan
|
||||||
|
- `Edit`: niet toegestaan
|
||||||
|
- `Copy`: toegestaan als alle geselecteerde items voldoen aan huidige backend-scope
|
||||||
|
- `Move`: toegestaan als alle geselecteerde items voldoen aan huidige backend-scope
|
||||||
|
- `Rename`: niet toegestaan
|
||||||
|
- `MKdir`: toegestaan
|
||||||
|
- `Delete`: toegestaan
|
||||||
|
|
||||||
|
### Directory/file-selectie
|
||||||
|
- zolang backend `copy/move` file-only is:
|
||||||
|
- selectie die directories bevat blokkeert `Copy`
|
||||||
|
- selectie die directories bevat blokkeert `Move`
|
||||||
|
- `View` en `Edit` blokkeren directories
|
||||||
|
- `Delete` volgt bestaande backendregels
|
||||||
|
- `Rename` volgt bestaande single-selection regel
|
||||||
|
|
||||||
|
### Disabled gedrag bij toetsgebruik
|
||||||
|
- geen verborgen fallback
|
||||||
|
- geen half-uitgevoerde actie
|
||||||
|
- optioneel compacte feedback via bestaande statusregel, maar v1 mag ook stil no-op blijven als dit consistenter is met huidige knopdisabled-semantiek
|
||||||
|
|
||||||
|
Voorkeur v1:
|
||||||
|
- als actie disabled is, shortcut doet niets en toont hooguit compacte statusfeedback als dat al in bestaande UI-patterns past
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. UX-regels
|
||||||
|
|
||||||
|
### Visuele hints in de functiebalk
|
||||||
|
|
||||||
|
Voorstel v1:
|
||||||
|
- subtiele shortcut-hints per knop toelaten, maar klein houden
|
||||||
|
- voorbeeld later mogelijk:
|
||||||
|
- `F3 View`
|
||||||
|
- `F4 Edit`
|
||||||
|
- `F5 Copy`
|
||||||
|
- Mac-alternatieven hoeven niet permanent zichtbaar in de hoofdbalk om ruis te vermijden
|
||||||
|
|
||||||
|
Voor deze ontwerpstap geldt:
|
||||||
|
- de functiebalk moet voorbereid zijn op zulke hints
|
||||||
|
- de daadwerkelijke implementatie kan starten zonder direct alle hints zichtbaar te maken
|
||||||
|
|
||||||
|
### Niet-beschikbare acties
|
||||||
|
- als actie disabled is in de huidige context, mag de shortcut die actie niet forceren
|
||||||
|
- gedrag moet gelijk zijn aan klikken op een disabled knop: er gebeurt functioneel niets
|
||||||
|
|
||||||
|
### Unsupported acties
|
||||||
|
- `View` of `Edit` op niet-ondersteunde filetypes:
|
||||||
|
- shortcut opent geen alternatieve flow
|
||||||
|
- zelfde fout- of disabled-behandeling als via knop
|
||||||
|
- `Copy`/`Move` op directoryselectie terwijl backend dit niet ondersteunt:
|
||||||
|
- zelfde blokkade als via knop
|
||||||
|
|
||||||
|
### Consistentie-eis
|
||||||
|
- shortcuts zijn geen tweede API-laag
|
||||||
|
- de functiebalk blijft leidend; keyboard is alleen een alternatieve triggerroute
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Impactanalyse
|
||||||
|
|
||||||
|
Waarschijnlijk te wijzigen frontendbestanden:
|
||||||
|
- `webui/html/app.js`
|
||||||
|
- `webui/html/index.html`
|
||||||
|
- `webui/html/style.css`
|
||||||
|
- `webui/backend/tests/golden/test_ui_smoke_golden.py`
|
||||||
|
|
||||||
|
### Verwachte code-impact
|
||||||
|
|
||||||
|
`app.js`:
|
||||||
|
- uitbreiding van centrale keyboard dispatcher
|
||||||
|
- mappingtabel voor action shortcuts
|
||||||
|
- centrale guards voor modal/input/focusstatus
|
||||||
|
- hergebruik van bestaande action handlers, niet dupliceren
|
||||||
|
|
||||||
|
`index.html`:
|
||||||
|
- mogelijk kleine markup-aanpassingen om shortcutlabels in functiebalk te kunnen tonen
|
||||||
|
- geen structurele layoutwijziging nodig
|
||||||
|
|
||||||
|
`style.css`:
|
||||||
|
- eventueel compacte styling voor functietoetsbadges of hints
|
||||||
|
- geen redesign nodig
|
||||||
|
|
||||||
|
### Regressierisico
|
||||||
|
|
||||||
|
Belangrijkste risico's:
|
||||||
|
- conflicten met bestaande keyboard navigation (`Tab`, `Arrow`, `Space`, `Escape`)
|
||||||
|
- shortcuts vuren terwijl input of modal focus heeft
|
||||||
|
- dubbele actie-aanroep als keyboard en knoplogica niet centraal gedeeld worden
|
||||||
|
- browser/OS-shortcuts per platform gedragen zich anders
|
||||||
|
|
||||||
|
Mitigatie:
|
||||||
|
- centrale `shouldHandleActionShortcut(event)` guard
|
||||||
|
- bestaande knophandlers of action-functions als single source of truth
|
||||||
|
- alleen exacte combinaties afvangen
|
||||||
|
- platform-onafhankelijke fallbacklogica beperkt en expliciet houden
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Teststrategie
|
||||||
|
|
||||||
|
### Smoke/regressietests
|
||||||
|
|
||||||
|
Geautomatiseerd uit te breiden waar passend:
|
||||||
|
- UI smoke test blijft controleren dat functiebalk aanwezig is
|
||||||
|
- optioneel extra check dat knoplabels of data-attributes voor actions stabiel aanwezig zijn
|
||||||
|
- geen zware browser-E2E vereist voor deze stap
|
||||||
|
|
||||||
|
### Handmatige validatie
|
||||||
|
|
||||||
|
Essentieel bij implementatie:
|
||||||
|
- `F3`-`F8` triggeren de juiste acties waar ondersteund
|
||||||
|
- `Alt+3`-`Alt+8` werken als Mac-vriendelijke fallback
|
||||||
|
- `Alt+R` triggert `Rename`
|
||||||
|
- shortcuts werken alleen op actief paneel
|
||||||
|
- shortcuts respecteren disabled toestand bij:
|
||||||
|
- geen selectie
|
||||||
|
- meerdere selecties
|
||||||
|
- directoryselectie
|
||||||
|
- unsupported filetypes
|
||||||
|
- editor modal, viewer modal en wildcard popup blokkeren onderliggende action shortcuts correct
|
||||||
|
- bestaande keyboard navigation blijft intact
|
||||||
|
|
||||||
|
### Regressiechecks
|
||||||
|
- klikgedrag in de bestandslijst blijft intact
|
||||||
|
- current-row navigatie blijft intact
|
||||||
|
- wildcard popup shortcuts blijven werken
|
||||||
|
- functiebalkknoppen en keyboardtrigger geven identiek resultaat
|
||||||
Binary file not shown.
@@ -35,6 +35,13 @@ class UiSmokeGoldenTest(unittest.TestCase):
|
|||||||
self.assertIn('id="function-bar"', body)
|
self.assertIn('id="function-bar"', body)
|
||||||
self.assertIn('id="view-btn"', body)
|
self.assertIn('id="view-btn"', body)
|
||||||
self.assertIn('id="edit-btn"', body)
|
self.assertIn('id="edit-btn"', body)
|
||||||
|
self.assertIn("F3", body)
|
||||||
|
self.assertIn("F4", body)
|
||||||
|
self.assertIn("F5", body)
|
||||||
|
self.assertIn("F6", body)
|
||||||
|
self.assertIn("F7", body)
|
||||||
|
self.assertIn("F8", body)
|
||||||
|
self.assertIn("Alt+R", body)
|
||||||
self.assertIn('id="viewer-modal"', body)
|
self.assertIn('id="viewer-modal"', body)
|
||||||
self.assertIn('id="viewer-content"', body)
|
self.assertIn('id="viewer-content"', body)
|
||||||
self.assertIn('id="editor-modal"', body)
|
self.assertIn('id="editor-modal"', body)
|
||||||
|
|||||||
@@ -662,6 +662,72 @@ function shouldHandleShortcut(target) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function actionButton(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerActionButton(id) {
|
||||||
|
const button = actionButton(id);
|
||||||
|
if (!button || button.disabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
button.click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionShortcutHandled(event) {
|
||||||
|
const altOnly = event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey;
|
||||||
|
const noModifiers = !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey;
|
||||||
|
|
||||||
|
if (noModifiers) {
|
||||||
|
if (event.key === "F3") {
|
||||||
|
return triggerActionButton("view-btn");
|
||||||
|
}
|
||||||
|
if (event.key === "F4") {
|
||||||
|
return triggerActionButton("edit-btn");
|
||||||
|
}
|
||||||
|
if (event.key === "F5") {
|
||||||
|
return triggerActionButton("copy-btn");
|
||||||
|
}
|
||||||
|
if (event.key === "F6") {
|
||||||
|
return triggerActionButton("move-btn");
|
||||||
|
}
|
||||||
|
if (event.key === "F7") {
|
||||||
|
return triggerActionButton("mkdir-btn");
|
||||||
|
}
|
||||||
|
if (event.key === "F8") {
|
||||||
|
return triggerActionButton("delete-btn");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (altOnly) {
|
||||||
|
const key = event.key.toLowerCase();
|
||||||
|
if (key === "3") {
|
||||||
|
return triggerActionButton("view-btn");
|
||||||
|
}
|
||||||
|
if (key === "4") {
|
||||||
|
return triggerActionButton("edit-btn");
|
||||||
|
}
|
||||||
|
if (key === "5") {
|
||||||
|
return triggerActionButton("copy-btn");
|
||||||
|
}
|
||||||
|
if (key === "6") {
|
||||||
|
return triggerActionButton("move-btn");
|
||||||
|
}
|
||||||
|
if (key === "7") {
|
||||||
|
return triggerActionButton("mkdir-btn");
|
||||||
|
}
|
||||||
|
if (key === "8") {
|
||||||
|
return triggerActionButton("delete-btn");
|
||||||
|
}
|
||||||
|
if (key === "r") {
|
||||||
|
return triggerActionButton("rename-btn");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function wildcardPopupElements() {
|
function wildcardPopupElements() {
|
||||||
return {
|
return {
|
||||||
overlay: document.getElementById("wildcard-popup"),
|
overlay: document.getElementById("wildcard-popup"),
|
||||||
@@ -958,6 +1024,11 @@ function handleKeyboardShortcuts(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actionShortcutHandled(event)) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.shiftKey && event.key === "+") {
|
if (event.shiftKey && event.key === "+") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openWildcardPopup("select");
|
openWildcardPopup("select");
|
||||||
|
|||||||
@@ -62,13 +62,13 @@
|
|||||||
<section id="footer-bar">
|
<section id="footer-bar">
|
||||||
<div id="function-bar-meta" class="pathline compact-line">Active:<code id="active-pane-label">left</code></div>
|
<div id="function-bar-meta" class="pathline compact-line">Active:<code id="active-pane-label">left</code></div>
|
||||||
<div id="function-bar" class="toolbar compact-toolbar">
|
<div id="function-bar" class="toolbar compact-toolbar">
|
||||||
<button id="view-btn" type="button" disabled>View</button>
|
<button id="view-btn" type="button" disabled><span class="shortcut-hint">F3</span><span>View</span></button>
|
||||||
<button id="edit-btn" type="button" disabled>Edit</button>
|
<button id="edit-btn" type="button" disabled><span class="shortcut-hint">F4</span><span>Edit</span></button>
|
||||||
<button id="copy-btn" type="button" disabled>Copy</button>
|
<button id="copy-btn" type="button" disabled><span class="shortcut-hint">F5</span><span>Copy</span></button>
|
||||||
<button id="move-btn" type="button" disabled>Move</button>
|
<button id="move-btn" type="button" disabled><span class="shortcut-hint">F6</span><span>Move</span></button>
|
||||||
<button id="rename-btn" type="button" disabled>Rename</button>
|
<button id="rename-btn" type="button" disabled><span class="shortcut-hint">Alt+R</span><span>Rename</span></button>
|
||||||
<button id="mkdir-btn" type="button">MKdir</button>
|
<button id="mkdir-btn" type="button"><span class="shortcut-hint">F7</span><span>MKdir</span></button>
|
||||||
<button id="delete-btn" type="button" disabled>Delete</button>
|
<button id="delete-btn" type="button" disabled><span class="shortcut-hint">F8</span><span>Delete</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="actions-error" class="error"></div>
|
<div id="actions-error" class="error"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -318,6 +318,18 @@ button:disabled {
|
|||||||
#function-bar button {
|
#function-bar button {
|
||||||
min-width: 72px;
|
min-width: 72px;
|
||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-hint {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-overlay {
|
.popup-overlay {
|
||||||
|
|||||||
Reference in New Issue
Block a user