@brftech/filex-core 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ArchiveViewer-B_0IQExT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
- package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
- package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-BvriJ42M.js} +2 -2
- package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
- package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-B_bSPsWX.js} +2 -2
- package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
- package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-hl68h4gG.js} +2 -2
- package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
- package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-lhwOjXK7.js} +2 -2
- package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
- package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-H2Z6AYXg.js} +2 -2
- package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
- package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Bsk-11yH.js} +2 -2
- package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
- package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-D-j9vycG.js} +2 -2
- package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
- package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-Bcr70rTe.js} +2 -2
- package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
- package/dist/filex-core.js +50 -25
- package/dist/filex-core.umd.cjs +59 -58
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-C_ZSnk_I.js +11109 -0
- package/dist/index-C_ZSnk_I.js.map +1 -0
- package/dist/index.d.ts +597 -4
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +636 -15
- package/src/components/CommandPalette.vue +24 -1
- package/src/components/ContextMenu.vue +114 -8
- package/src/components/GalleryView.vue +214 -0
- package/src/components/GridView.vue +34 -1
- package/src/components/InspectorPanel.vue +170 -0
- package/src/components/ListView.vue +40 -6
- package/src/components/OnboardingTour.vue +332 -0
- package/src/components/OperationsCenter.vue +284 -0
- package/src/components/PendingOpsTray.vue +50 -66
- package/src/components/QuickLook.vue +142 -0
- package/src/components/SecondaryPane.vue +411 -0
- package/src/components/ShortcutSettings.vue +270 -0
- package/src/components/ShortcutsHelp.vue +30 -9
- package/src/components/TabBar.vue +174 -0
- package/src/components/ThemeGallery.vue +253 -0
- package/src/components/Toolbar.vue +75 -11
- package/src/components/UploadProgress.vue +49 -57
- package/src/composables/useFileApi.ts +53 -0
- package/src/composables/useKeyboardShortcuts.ts +340 -137
- package/src/composables/useOperations.ts +291 -0
- package/src/composables/useTabs.ts +222 -0
- package/src/index.ts +51 -0
- package/src/lib/dragGhost.ts +41 -0
- package/src/lib/themes.ts +505 -0
- package/src/locales/en.ts +125 -0
- package/src/locales/tr.ts +125 -0
- package/src/modals/Modal.vue +41 -5
- package/src/styles/base.css +1210 -0
- package/src/types/FileNode.ts +1 -1
- package/dist/index-BLocdbmc.js +0 -7993
- package/dist/index-BLocdbmc.js.map +0 -1
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useKeyboardShortcuts — binds FileExplorer's keyboard affordances.
|
|
3
3
|
*
|
|
4
|
+
* wiring:c2 — rewritten as a REGISTRY: every action is declared once
|
|
5
|
+
* (id + default combo + i18n label + category) and the user may remap
|
|
6
|
+
* the customizable ones. Overrides persist in localStorage under
|
|
7
|
+
* `filex.shortcuts` as `{ "<actionId>": "<combo>" }` — only deviations
|
|
8
|
+
* from the defaults are stored; an empty string means "unbound".
|
|
9
|
+
*
|
|
10
|
+
* Canonical combo format (also the display format): modifiers in
|
|
11
|
+
* `Ctrl+Alt+Shift` order followed by a single key token, joined with
|
|
12
|
+
* `+`. Key tokens: uppercase letters/digits, symbols as typed (`/`,
|
|
13
|
+
* `?`), `F1`–`F12`, `Space`, `Enter`, `Esc`, `Del`, `Backspace`, `Tab`,
|
|
14
|
+
* `Home`, `End`, `PgUp`, `PgDn` and arrows as `↑ ↓ ← →`. Meta (Cmd) is
|
|
15
|
+
* folded into `Ctrl` so one saved combo works on macOS too. For
|
|
16
|
+
* printable symbols Shift is omitted (`?` already encodes it).
|
|
17
|
+
*
|
|
4
18
|
* Activated whenever the explorer is mounted (only one instance per
|
|
5
19
|
* page is expected). Skips events that originate inside form controls
|
|
6
20
|
* so the user can type filenames in the search box, modals, etc.
|
|
7
21
|
*/
|
|
8
22
|
|
|
9
|
-
import { onMounted, onBeforeUnmount, type Ref } from 'vue';
|
|
23
|
+
import { computed, onMounted, onBeforeUnmount, ref, type ComputedRef, type Ref } from 'vue';
|
|
10
24
|
|
|
11
25
|
export interface ShortcutHandlers {
|
|
12
26
|
onDelete?: () => void;
|
|
@@ -18,47 +32,311 @@ export interface ShortcutHandlers {
|
|
|
18
32
|
onOpen?: () => void; // Enter
|
|
19
33
|
onClose?: () => void; // Escape
|
|
20
34
|
onFocusSearch?: () => void; // /
|
|
21
|
-
onDuplicate?: () => void; //
|
|
35
|
+
onDuplicate?: () => void; // (unwired — kept for API compat)
|
|
22
36
|
onPathJump?: () => void; // Cmd+K / Ctrl+K
|
|
23
37
|
onGoUp?: () => void; // Alt+Up / Backspace (when nothing selected)
|
|
24
38
|
onShowHelp?: () => void; // ? (Shift+/ on most layouts)
|
|
25
39
|
onToggleInspector?: () => void; // i (koru:k1 details panel)
|
|
40
|
+
onQuickLook?: () => void; // Space (wiring:c2 quick-look overlay)
|
|
41
|
+
/* wiring:d1 — tab strip actions */
|
|
42
|
+
onTabNew?: () => void; // Ctrl+T
|
|
43
|
+
onTabClose?: () => void; // Ctrl+W
|
|
44
|
+
onTabNext?: () => void; // Ctrl+Tab
|
|
45
|
+
onTabPrev?: () => void; // Ctrl+Shift+Tab
|
|
46
|
+
/* /wiring:d1 */
|
|
26
47
|
hasSelection?: () => boolean; // disambiguates Backspace
|
|
27
48
|
}
|
|
28
49
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
// --------------------------------------------------------------------
|
|
51
|
+
// Registry
|
|
52
|
+
// --------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/** One remappable action in the shortcut registry. */
|
|
55
|
+
export interface ShortcutActionDef {
|
|
56
|
+
/** Stable id — also the localStorage override key. */
|
|
57
|
+
id: string;
|
|
58
|
+
/** Default combo in canonical form. */
|
|
59
|
+
defaultCombo: string;
|
|
60
|
+
/**
|
|
61
|
+
* Extra built-in combos that also trigger the action but are NOT
|
|
62
|
+
* remappable (e.g. Backspace on go-up, which carries the
|
|
63
|
+
* selection-dependent dual behaviour). Shown in the help sheet and
|
|
64
|
+
* reserved in conflict detection.
|
|
65
|
+
*/
|
|
66
|
+
fixedCombos?: string[];
|
|
38
67
|
labelKey: string;
|
|
39
68
|
groupKey: string;
|
|
69
|
+
/** false → the combo can't be remapped (Esc). Default true. */
|
|
70
|
+
customizable?: boolean;
|
|
71
|
+
/** false → don't preventDefault on match (Enter / Esc). Default true. */
|
|
72
|
+
prevent?: boolean;
|
|
73
|
+
/** true → fires even when focus sits in a form control (Esc). */
|
|
74
|
+
inForms?: boolean;
|
|
40
75
|
}
|
|
41
76
|
|
|
42
|
-
|
|
77
|
+
/** Registry order = help/settings display order. */
|
|
78
|
+
export const SHORTCUT_ACTIONS: ShortcutActionDef[] = [
|
|
43
79
|
// Navigation
|
|
44
|
-
{
|
|
45
|
-
{
|
|
46
|
-
{
|
|
47
|
-
{
|
|
48
|
-
{
|
|
49
|
-
{
|
|
50
|
-
{
|
|
80
|
+
{ id: 'palette', defaultCombo: 'Ctrl+K', labelKey: 'shortcuts.palette', groupKey: 'shortcuts.group.nav' },
|
|
81
|
+
{ id: 'search', defaultCombo: '/', labelKey: 'shortcuts.search', groupKey: 'shortcuts.group.nav' },
|
|
82
|
+
{ id: 'go-up', defaultCombo: 'Alt+↑', fixedCombos: ['Backspace'], labelKey: 'shortcuts.go_up', groupKey: 'shortcuts.group.nav' },
|
|
83
|
+
{ id: 'open', defaultCombo: 'Enter', prevent: false, labelKey: 'shortcuts.open', groupKey: 'shortcuts.group.nav' },
|
|
84
|
+
{ id: 'quicklook', defaultCombo: 'Space', labelKey: 'shortcuts.quicklook', groupKey: 'shortcuts.group.nav' },
|
|
85
|
+
{ id: 'close', defaultCombo: 'Esc', customizable: false, prevent: false, inForms: true, labelKey: 'shortcuts.close', groupKey: 'shortcuts.group.nav' },
|
|
86
|
+
{ id: 'inspector', defaultCombo: 'I', labelKey: 'shortcuts.inspector', groupKey: 'shortcuts.group.nav' } /* koru:k1 */,
|
|
87
|
+
{ id: 'help', defaultCombo: '?', labelKey: 'shortcuts.help', groupKey: 'shortcuts.group.nav' },
|
|
51
88
|
// Selection
|
|
52
|
-
{
|
|
89
|
+
{ id: 'select-all', defaultCombo: 'Ctrl+A', labelKey: 'shortcuts.select_all', groupKey: 'shortcuts.group.selection' },
|
|
53
90
|
// File operations
|
|
54
|
-
{
|
|
55
|
-
{
|
|
56
|
-
{
|
|
57
|
-
{
|
|
58
|
-
{
|
|
91
|
+
{ id: 'rename', defaultCombo: 'F2', labelKey: 'shortcuts.rename', groupKey: 'shortcuts.group.file' },
|
|
92
|
+
{ id: 'delete', defaultCombo: 'Del', labelKey: 'shortcuts.delete', groupKey: 'shortcuts.group.file' },
|
|
93
|
+
{ id: 'cut', defaultCombo: 'Ctrl+X', labelKey: 'shortcuts.cut', groupKey: 'shortcuts.group.file' },
|
|
94
|
+
{ id: 'copy', defaultCombo: 'Ctrl+C', labelKey: 'shortcuts.copy', groupKey: 'shortcuts.group.file' },
|
|
95
|
+
{ id: 'paste', defaultCombo: 'Ctrl+V', labelKey: 'shortcuts.paste', groupKey: 'shortcuts.group.file' },
|
|
96
|
+
/* wiring:d1 — tabs. Note: browsers reserve Ctrl+T/W/Tab in normal pages
|
|
97
|
+
* (preventDefault can't stop them there); they work in webcomponent/PWA/
|
|
98
|
+
* kiosk contexts and stay remappable through the settings modal. */
|
|
99
|
+
{ id: 'tab-new', defaultCombo: 'Ctrl+T', labelKey: 'shortcuts.tab_new', groupKey: 'shortcuts.group.tabs' },
|
|
100
|
+
{ id: 'tab-close', defaultCombo: 'Ctrl+W', labelKey: 'shortcuts.tab_close', groupKey: 'shortcuts.group.tabs' },
|
|
101
|
+
{ id: 'tab-next', defaultCombo: 'Ctrl+Tab', labelKey: 'shortcuts.tab_next', groupKey: 'shortcuts.group.tabs' },
|
|
102
|
+
{ id: 'tab-prev', defaultCombo: 'Ctrl+Shift+Tab', labelKey: 'shortcuts.tab_prev', groupKey: 'shortcuts.group.tabs' },
|
|
103
|
+
/* /wiring:d1 */
|
|
59
104
|
];
|
|
60
105
|
|
|
106
|
+
/** action id → ShortcutHandlers callback name. */
|
|
107
|
+
const HANDLER_KEY: Record<string, keyof ShortcutHandlers> = {
|
|
108
|
+
palette: 'onPathJump',
|
|
109
|
+
search: 'onFocusSearch',
|
|
110
|
+
'go-up': 'onGoUp',
|
|
111
|
+
open: 'onOpen',
|
|
112
|
+
quicklook: 'onQuickLook',
|
|
113
|
+
close: 'onClose',
|
|
114
|
+
inspector: 'onToggleInspector',
|
|
115
|
+
help: 'onShowHelp',
|
|
116
|
+
'select-all': 'onSelectAll',
|
|
117
|
+
rename: 'onRename',
|
|
118
|
+
delete: 'onDelete',
|
|
119
|
+
cut: 'onCut',
|
|
120
|
+
copy: 'onCopy',
|
|
121
|
+
paste: 'onPaste',
|
|
122
|
+
/* wiring:d1 */
|
|
123
|
+
'tab-new': 'onTabNew',
|
|
124
|
+
'tab-close': 'onTabClose',
|
|
125
|
+
'tab-next': 'onTabNext',
|
|
126
|
+
'tab-prev': 'onTabPrev',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// --------------------------------------------------------------------
|
|
130
|
+
// Overrides (localStorage `filex.shortcuts`)
|
|
131
|
+
// --------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
const SHORTCUTS_LS_KEY = 'filex.shortcuts';
|
|
134
|
+
|
|
135
|
+
function readOverrides(): Record<string, string> {
|
|
136
|
+
try {
|
|
137
|
+
const raw = localStorage.getItem(SHORTCUTS_LS_KEY);
|
|
138
|
+
if (!raw) return {};
|
|
139
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
140
|
+
if (!parsed || typeof parsed !== 'object') return {};
|
|
141
|
+
const out: Record<string, string> = {};
|
|
142
|
+
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
|
143
|
+
if (typeof v === 'string' && SHORTCUT_ACTIONS.some((a) => a.id === k)) out[k] = v;
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
} catch {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Module-level so the key handler, the "?" help sheet and the settings
|
|
152
|
+
// modal all observe the same reactive mapping.
|
|
153
|
+
const overrides = ref<Record<string, string>>(readOverrides());
|
|
154
|
+
|
|
155
|
+
function writeOverrides(): void {
|
|
156
|
+
try {
|
|
157
|
+
if (Object.keys(overrides.value).length === 0) localStorage.removeItem(SHORTCUTS_LS_KEY);
|
|
158
|
+
else localStorage.setItem(SHORTCUTS_LS_KEY, JSON.stringify(overrides.value));
|
|
159
|
+
} catch {
|
|
160
|
+
/* private mode / quota */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function defOf(id: string): ShortcutActionDef | undefined {
|
|
165
|
+
return SHORTCUT_ACTIONS.find((a) => a.id === id);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Current combo for an action ('' = unbound). */
|
|
169
|
+
export function effectiveCombo(id: string): string {
|
|
170
|
+
const o = overrides.value[id];
|
|
171
|
+
if (o !== undefined) return o;
|
|
172
|
+
return defOf(id)?.defaultCombo ?? '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Set (or clear back to default) a user override. '' unbinds. */
|
|
176
|
+
export function setShortcutOverride(id: string, combo: string): void {
|
|
177
|
+
const def = defOf(id);
|
|
178
|
+
if (!def || def.customizable === false) return;
|
|
179
|
+
const next = { ...overrides.value };
|
|
180
|
+
if (combo === def.defaultCombo) delete next[id];
|
|
181
|
+
else next[id] = combo;
|
|
182
|
+
overrides.value = next;
|
|
183
|
+
writeOverrides();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function resetShortcut(id: string): void {
|
|
187
|
+
if (!(id in overrides.value)) return;
|
|
188
|
+
const next = { ...overrides.value };
|
|
189
|
+
delete next[id];
|
|
190
|
+
overrides.value = next;
|
|
191
|
+
writeOverrides();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function resetAllShortcuts(): void {
|
|
195
|
+
overrides.value = {};
|
|
196
|
+
writeOverrides();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface ShortcutConflict {
|
|
200
|
+
id: string;
|
|
201
|
+
/** true → the clash is with a fixed (non-remappable) combo. */
|
|
202
|
+
fixed: boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Does `combo` already trigger another action? Checks both effective
|
|
207
|
+
* combos and the reserved fixed combos (Backspace, Esc). Returns null
|
|
208
|
+
* when free.
|
|
209
|
+
*/
|
|
210
|
+
export function findShortcutConflict(combo: string, excludeId: string): ShortcutConflict | null {
|
|
211
|
+
if (!combo) return null;
|
|
212
|
+
for (const def of SHORTCUT_ACTIONS) {
|
|
213
|
+
if (def.id === excludeId) continue;
|
|
214
|
+
if (def.fixedCombos?.includes(combo)) return { id: def.id, fixed: true };
|
|
215
|
+
if (effectiveCombo(def.id) === combo) return { id: def.id, fixed: def.customizable === false };
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --------------------------------------------------------------------
|
|
221
|
+
// Event → canonical combo
|
|
222
|
+
// --------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
const KEY_TOKEN_MAP: Record<string, string> = {
|
|
225
|
+
' ': 'Space',
|
|
226
|
+
Spacebar: 'Space',
|
|
227
|
+
Escape: 'Esc',
|
|
228
|
+
Delete: 'Del',
|
|
229
|
+
ArrowUp: '↑',
|
|
230
|
+
ArrowDown: '↓',
|
|
231
|
+
ArrowLeft: '←',
|
|
232
|
+
ArrowRight: '→',
|
|
233
|
+
PageUp: 'PgUp',
|
|
234
|
+
PageDown: 'PgDn',
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const MODIFIER_KEYS = new Set(['Control', 'Alt', 'Shift', 'Meta', 'AltGraph', 'CapsLock', 'NumLock', 'ScrollLock', 'Fn', 'Dead']);
|
|
238
|
+
|
|
239
|
+
function keyToken(e: KeyboardEvent): string | null {
|
|
240
|
+
const k = e.key;
|
|
241
|
+
if (!k || MODIFIER_KEYS.has(k)) return null;
|
|
242
|
+
const mapped = KEY_TOKEN_MAP[k];
|
|
243
|
+
if (mapped) return mapped;
|
|
244
|
+
if (k.length === 1) {
|
|
245
|
+
const upper = k.toUpperCase();
|
|
246
|
+
// Letters normalize to uppercase; symbols pass through as typed.
|
|
247
|
+
return upper;
|
|
248
|
+
}
|
|
249
|
+
return k; // 'Enter', 'F2', 'Tab', 'Home', 'End', 'Backspace', …
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Canonical combo for a keyboard event, or null for a bare modifier
|
|
254
|
+
* press. Meta folds into Ctrl; Shift is dropped for printable symbols
|
|
255
|
+
* because the produced character already encodes it (`?` not `Shift+?`).
|
|
256
|
+
*/
|
|
257
|
+
export function comboFromEvent(e: KeyboardEvent): string | null {
|
|
258
|
+
const key = keyToken(e);
|
|
259
|
+
if (!key) return null;
|
|
260
|
+
const parts: string[] = [];
|
|
261
|
+
if (e.ctrlKey || e.metaKey) parts.push('Ctrl');
|
|
262
|
+
if (e.altKey) parts.push('Alt');
|
|
263
|
+
const printableSymbol = key.length === 1 && key.toUpperCase() === key.toLowerCase();
|
|
264
|
+
if (e.shiftKey && !printableSymbol) parts.push('Shift');
|
|
265
|
+
parts.push(key);
|
|
266
|
+
return parts.join('+');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --------------------------------------------------------------------
|
|
270
|
+
// Reactive views (help sheet + settings modal)
|
|
271
|
+
// --------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
export interface ShortcutView {
|
|
274
|
+
id: string;
|
|
275
|
+
/** Current (possibly remapped) combo, '' = unbound. */
|
|
276
|
+
combo: string;
|
|
277
|
+
/** Non-remappable extra combos (display + reservation only). */
|
|
278
|
+
fixedCombos: string[];
|
|
279
|
+
/** Display combos: [combo, ...fixedCombos] minus empties. */
|
|
280
|
+
keys: string[];
|
|
281
|
+
labelKey: string;
|
|
282
|
+
groupKey: string;
|
|
283
|
+
customizable: boolean;
|
|
284
|
+
overridden: boolean;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Reactive, override-aware view of the registry (registry order). */
|
|
288
|
+
export function useShortcutList(): ComputedRef<ShortcutView[]> {
|
|
289
|
+
return computed(() =>
|
|
290
|
+
SHORTCUT_ACTIONS.map((def) => {
|
|
291
|
+
const combo = effectiveCombo(def.id);
|
|
292
|
+
const fixed = def.fixedCombos ?? [];
|
|
293
|
+
return {
|
|
294
|
+
id: def.id,
|
|
295
|
+
combo,
|
|
296
|
+
fixedCombos: fixed,
|
|
297
|
+
keys: [combo, ...fixed].filter((c) => !!c),
|
|
298
|
+
labelKey: def.labelKey,
|
|
299
|
+
groupKey: def.groupKey,
|
|
300
|
+
customizable: def.customizable !== false,
|
|
301
|
+
overridden: overrides.value[def.id] !== undefined,
|
|
302
|
+
};
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @deprecated Legacy static cheat-sheet shape (pre-registry). Kept for
|
|
309
|
+
* API compatibility; use `useShortcutList()` for the live, remap-aware
|
|
310
|
+
* list.
|
|
311
|
+
*/
|
|
312
|
+
export interface ShortcutDef {
|
|
313
|
+
keys: string[];
|
|
314
|
+
labelKey: string;
|
|
315
|
+
groupKey: string;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** @deprecated See {@link useShortcutList}. Defaults only. */
|
|
319
|
+
export const SHORTCUTS: ShortcutDef[] = SHORTCUT_ACTIONS.map((def) => ({
|
|
320
|
+
keys: [def.defaultCombo, ...(def.fixedCombos ?? [])].filter((c) => !!c),
|
|
321
|
+
labelKey: def.labelKey,
|
|
322
|
+
groupKey: def.groupKey,
|
|
323
|
+
}));
|
|
324
|
+
|
|
325
|
+
// --------------------------------------------------------------------
|
|
326
|
+
// Binding
|
|
327
|
+
// --------------------------------------------------------------------
|
|
328
|
+
|
|
61
329
|
export function useKeyboardShortcuts(rootEl: Ref<HTMLElement | null>, handlers: ShortcutHandlers) {
|
|
330
|
+
// combo → action def, rebuilt when overrides change.
|
|
331
|
+
const comboMap = computed(() => {
|
|
332
|
+
const m = new Map<string, ShortcutActionDef>();
|
|
333
|
+
for (const def of SHORTCUT_ACTIONS) {
|
|
334
|
+
const c = effectiveCombo(def.id);
|
|
335
|
+
if (c) m.set(c, def);
|
|
336
|
+
}
|
|
337
|
+
return m;
|
|
338
|
+
});
|
|
339
|
+
|
|
62
340
|
function onKey(e: KeyboardEvent) {
|
|
63
341
|
const root = rootEl.value;
|
|
64
342
|
if (!root) return;
|
|
@@ -67,125 +345,50 @@ export function useKeyboardShortcuts(rootEl: Ref<HTMLElement | null>, handlers:
|
|
|
67
345
|
// want `Delete` while editing a filename, `/` while typing in the
|
|
68
346
|
// search box, etc. Escape always goes through so modals can close.
|
|
69
347
|
const target = e.target as HTMLElement | null;
|
|
70
|
-
|
|
348
|
+
const inForm = !!(
|
|
71
349
|
target &&
|
|
72
350
|
(target.tagName === 'INPUT' ||
|
|
73
351
|
target.tagName === 'TEXTAREA' ||
|
|
352
|
+
target.tagName === 'SELECT' ||
|
|
74
353
|
target.isContentEditable)
|
|
75
|
-
)
|
|
76
|
-
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const combo = comboFromEvent(e);
|
|
357
|
+
if (!combo) return;
|
|
358
|
+
|
|
359
|
+
// Backspace keeps its FIXED dual behaviour: delete when something is
|
|
360
|
+
// selected (file-manager convention), parent-dir navigation otherwise.
|
|
361
|
+
// Not remappable — the selection-dependent branch doesn't fit the
|
|
362
|
+
// one-combo-one-action registry model.
|
|
363
|
+
if (combo === 'Backspace') {
|
|
364
|
+
if (inForm) return;
|
|
365
|
+
if (handlers.hasSelection?.() && handlers.onDelete) {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
handlers.onDelete();
|
|
368
|
+
} else if (handlers.onGoUp) {
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
handlers.onGoUp();
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
77
373
|
}
|
|
78
374
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// disambiguation Backspace was firing onDelete with an empty
|
|
92
|
-
// selection and nothing happened, leaving users wondering why
|
|
93
|
-
// the obvious 'go back' key did nothing.
|
|
94
|
-
if (handlers.hasSelection?.() && handlers.onDelete) {
|
|
95
|
-
e.preventDefault();
|
|
96
|
-
handlers.onDelete();
|
|
97
|
-
} else if (handlers.onGoUp) {
|
|
98
|
-
e.preventDefault();
|
|
99
|
-
handlers.onGoUp();
|
|
100
|
-
}
|
|
101
|
-
break;
|
|
102
|
-
case 'F2':
|
|
103
|
-
if (handlers.onRename) {
|
|
104
|
-
e.preventDefault();
|
|
105
|
-
handlers.onRename();
|
|
106
|
-
}
|
|
107
|
-
break;
|
|
108
|
-
case 'Enter':
|
|
109
|
-
if (handlers.onOpen) {
|
|
110
|
-
handlers.onOpen();
|
|
111
|
-
}
|
|
112
|
-
break;
|
|
113
|
-
case 'Escape':
|
|
114
|
-
if (handlers.onClose) {
|
|
115
|
-
handlers.onClose();
|
|
116
|
-
}
|
|
117
|
-
break;
|
|
118
|
-
case '/':
|
|
119
|
-
if (handlers.onFocusSearch && !ctrl) {
|
|
120
|
-
e.preventDefault();
|
|
121
|
-
handlers.onFocusSearch();
|
|
122
|
-
}
|
|
123
|
-
break;
|
|
124
|
-
case '?':
|
|
125
|
-
if (handlers.onShowHelp && !ctrl && !e.altKey) {
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
handlers.onShowHelp();
|
|
128
|
-
}
|
|
129
|
-
break;
|
|
130
|
-
case 'a':
|
|
131
|
-
case 'A':
|
|
132
|
-
if (ctrl && handlers.onSelectAll) {
|
|
133
|
-
e.preventDefault();
|
|
134
|
-
handlers.onSelectAll();
|
|
135
|
-
}
|
|
136
|
-
break;
|
|
137
|
-
case 'x':
|
|
138
|
-
case 'X':
|
|
139
|
-
if (ctrl && handlers.onCut) {
|
|
140
|
-
e.preventDefault();
|
|
141
|
-
handlers.onCut();
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
case 'c':
|
|
145
|
-
case 'C':
|
|
146
|
-
if (ctrl && handlers.onCopy) {
|
|
147
|
-
e.preventDefault();
|
|
148
|
-
handlers.onCopy();
|
|
149
|
-
}
|
|
150
|
-
break;
|
|
151
|
-
case 'v':
|
|
152
|
-
case 'V':
|
|
153
|
-
if (ctrl && handlers.onPaste) {
|
|
154
|
-
e.preventDefault();
|
|
155
|
-
handlers.onPaste();
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
case 'd':
|
|
159
|
-
case 'D':
|
|
160
|
-
if (ctrl && handlers.onDuplicate) {
|
|
161
|
-
e.preventDefault();
|
|
162
|
-
handlers.onDuplicate();
|
|
163
|
-
}
|
|
164
|
-
break;
|
|
165
|
-
/* koru:k1 — plain `i` toggles the inspector (details) panel. Ctrl+I /
|
|
166
|
-
Alt+I stay untouched so browser/OS combos keep working. */
|
|
167
|
-
case 'i':
|
|
168
|
-
case 'I':
|
|
169
|
-
if (!ctrl && !e.altKey && handlers.onToggleInspector) {
|
|
170
|
-
e.preventDefault();
|
|
171
|
-
handlers.onToggleInspector();
|
|
172
|
-
}
|
|
173
|
-
break;
|
|
174
|
-
case 'k':
|
|
175
|
-
case 'K':
|
|
176
|
-
if (ctrl && handlers.onPathJump) {
|
|
177
|
-
e.preventDefault();
|
|
178
|
-
handlers.onPathJump();
|
|
179
|
-
}
|
|
180
|
-
break;
|
|
181
|
-
case 'ArrowUp':
|
|
182
|
-
// Alt+Up = parent dir (matches Finder / Files / Explorer).
|
|
183
|
-
if (e.altKey && handlers.onGoUp) {
|
|
184
|
-
e.preventDefault();
|
|
185
|
-
handlers.onGoUp();
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
375
|
+
const def = comboMap.value.get(combo);
|
|
376
|
+
if (!def) return;
|
|
377
|
+
if (inForm && !def.inForms) return;
|
|
378
|
+
// Space on a focused button/link must keep its native activation
|
|
379
|
+
// (quick-look would otherwise fire alongside the click).
|
|
380
|
+
if (
|
|
381
|
+
combo === 'Space' &&
|
|
382
|
+
target &&
|
|
383
|
+
typeof target.closest === 'function' &&
|
|
384
|
+
target.closest('button, a, select, summary, [role="button"], [role="menuitem"]')
|
|
385
|
+
) {
|
|
386
|
+
return;
|
|
188
387
|
}
|
|
388
|
+
const fn = handlers[HANDLER_KEY[def.id]] as (() => void) | undefined;
|
|
389
|
+
if (!fn) return;
|
|
390
|
+
if (def.prevent !== false) e.preventDefault();
|
|
391
|
+
fn();
|
|
189
392
|
}
|
|
190
393
|
|
|
191
394
|
onMounted(() => window.addEventListener('keydown', onKey));
|