ngx-keys 1.2.0 → 1.3.1
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/README.md +450 -15
- package/fesm2022/ngx-keys.mjs +684 -63
- package/fesm2022/ngx-keys.mjs.map +1 -1
- package/index.d.ts +293 -13
- package/package.json +1 -1
package/fesm2022/ngx-keys.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { DestroyRef, inject, DOCUMENT, signal, computed, afterNextRender, Injectable } from '@angular/core';
|
|
2
|
+
import { InjectionToken, DestroyRef, inject, DOCUMENT, signal, computed, afterNextRender, Injectable, ElementRef, output, HostBinding, Input, Directive } from '@angular/core';
|
|
3
3
|
import { Observable, take } from 'rxjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -99,6 +99,204 @@ class KeyboardShortcutsErrorFactory {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Injection token for providing KeyboardShortcuts configuration.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* providers: [
|
|
108
|
+
* provideKeyboardShortcutsConfig({ sequenceTimeoutMs: Infinity })
|
|
109
|
+
* ]
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
const KEYBOARD_SHORTCUTS_CONFIG = new InjectionToken('KEYBOARD_SHORTCUTS_CONFIG', {
|
|
113
|
+
providedIn: 'root',
|
|
114
|
+
factory: () => ({})
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* Provides KeyboardShortcuts configuration using Angular's modern provider pattern.
|
|
118
|
+
*
|
|
119
|
+
* @param config - Configuration options for KeyboardShortcuts service
|
|
120
|
+
* @returns Provider array for use in Angular dependency injection
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* bootstrapApplication(AppComponent, {
|
|
125
|
+
* providers: [
|
|
126
|
+
* provideKeyboardShortcutsConfig({ sequenceTimeoutMs: Infinity })
|
|
127
|
+
* ]
|
|
128
|
+
* });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
function provideKeyboardShortcutsConfig(config) {
|
|
132
|
+
return [{ provide: KEYBOARD_SHORTCUTS_CONFIG, useValue: config }];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Initial version number for state change detection.
|
|
136
|
+
* Used internally to track state updates efficiently.
|
|
137
|
+
* @internal
|
|
138
|
+
*/
|
|
139
|
+
const INITIAL_STATE_VERSION = 0;
|
|
140
|
+
/**
|
|
141
|
+
* Increment value for state version updates.
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
144
|
+
const STATE_VERSION_INCREMENT = 1;
|
|
145
|
+
/**
|
|
146
|
+
* Index of the first element in an array.
|
|
147
|
+
* Used for readability when checking array structure.
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
const FIRST_INDEX = 0;
|
|
151
|
+
/**
|
|
152
|
+
* Index representing the first step in a multi-step sequence.
|
|
153
|
+
* @internal
|
|
154
|
+
*/
|
|
155
|
+
const FIRST_STEP_INDEX = 0;
|
|
156
|
+
/**
|
|
157
|
+
* Index representing the second step in a multi-step sequence (after first step completes).
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
const SECOND_STEP_INDEX = 1;
|
|
161
|
+
/**
|
|
162
|
+
* Threshold for determining if a shortcut is a single-step sequence.
|
|
163
|
+
* @internal
|
|
164
|
+
*/
|
|
165
|
+
const SINGLE_STEP_LENGTH = 1;
|
|
166
|
+
/**
|
|
167
|
+
* Minimum count indicating that at least one item exists.
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
const MIN_COUNT_ONE = 1;
|
|
171
|
+
/**
|
|
172
|
+
* Minimum length for a valid key string.
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
const MIN_KEY_LENGTH = 0;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Utility class for keyboard shortcut key matching and normalization.
|
|
179
|
+
* Provides methods for comparing key combinations and normalizing key input.
|
|
180
|
+
*/
|
|
181
|
+
class KeyMatcher {
|
|
182
|
+
static MODIFIER_KEYS = new Set(['ctrl', 'control', 'alt', 'shift', 'meta']);
|
|
183
|
+
/**
|
|
184
|
+
* Normalize a key string to lowercase for consistent comparison.
|
|
185
|
+
*
|
|
186
|
+
* @param key - The key string to normalize
|
|
187
|
+
* @returns The normalized (lowercase) key string
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* KeyMatcher.normalizeKey('Ctrl'); // returns 'ctrl'
|
|
192
|
+
* KeyMatcher.normalizeKey('A'); // returns 'a'
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
static normalizeKey(key) {
|
|
196
|
+
return key.toLowerCase();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if a key is a modifier key (ctrl, alt, shift, meta).
|
|
200
|
+
*
|
|
201
|
+
* @param key - The key to check
|
|
202
|
+
* @returns true if the key is a modifier key
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* KeyMatcher.isModifierKey('ctrl'); // returns true
|
|
207
|
+
* KeyMatcher.isModifierKey('a'); // returns false
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
static isModifierKey(key) {
|
|
211
|
+
return this.MODIFIER_KEYS.has(key.toLowerCase());
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Compare two key combinations for equality.
|
|
215
|
+
* Supports both Set<string> and string[] as input.
|
|
216
|
+
* Keys are normalized (lowercased) before comparison.
|
|
217
|
+
*
|
|
218
|
+
* @param a - First key combination (Set or array)
|
|
219
|
+
* @param b - Second key combination (array)
|
|
220
|
+
* @returns true if both combinations contain the same keys
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* KeyMatcher.keysMatch(['ctrl', 's'], ['ctrl', 's']); // returns true
|
|
225
|
+
* KeyMatcher.keysMatch(['ctrl', 's'], ['Ctrl', 'S']); // returns true (normalized)
|
|
226
|
+
* KeyMatcher.keysMatch(['ctrl', 's'], ['alt', 's']); // returns false
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
static keysMatch(a, b) {
|
|
230
|
+
const setA = a instanceof Set ? a : this.normalizeKeysToSet(a);
|
|
231
|
+
const setB = this.normalizeKeysToSet(b);
|
|
232
|
+
if (setA.size !== setB.size) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
for (const key of setA) {
|
|
236
|
+
if (!setB.has(key)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Compare two multi-step sequences for equality.
|
|
244
|
+
* Each sequence is an array of steps, where each step is an array of keys.
|
|
245
|
+
*
|
|
246
|
+
* @param a - First multi-step sequence
|
|
247
|
+
* @param b - Second multi-step sequence
|
|
248
|
+
* @returns true if both sequences are identical
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* const seq1 = [['ctrl', 'k'], ['s']];
|
|
253
|
+
* const seq2 = [['ctrl', 'k'], ['s']];
|
|
254
|
+
* KeyMatcher.stepsMatch(seq1, seq2); // returns true
|
|
255
|
+
*
|
|
256
|
+
* const seq3 = [['ctrl', 'k'], ['t']];
|
|
257
|
+
* KeyMatcher.stepsMatch(seq1, seq3); // returns false (different final step)
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
static stepsMatch(a, b) {
|
|
261
|
+
if (a.length !== b.length) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
for (let i = 0; i < a.length; i++) {
|
|
265
|
+
if (!this.keysMatch(a[i], b[i])) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Normalize an array of keys to a Set of lowercase keys.
|
|
273
|
+
*
|
|
274
|
+
* @param keys - Array of keys to normalize
|
|
275
|
+
* @returns Set of normalized (lowercase) keys
|
|
276
|
+
*
|
|
277
|
+
* @internal
|
|
278
|
+
*/
|
|
279
|
+
static normalizeKeysToSet(keys) {
|
|
280
|
+
return new Set(keys.map(k => k.toLowerCase()));
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get the set of modifier key names.
|
|
284
|
+
* Useful for filtering or checking if a key is a modifier.
|
|
285
|
+
*
|
|
286
|
+
* @returns ReadonlySet of modifier key names
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```typescript
|
|
290
|
+
* const modifiers = KeyMatcher.getModifierKeys();
|
|
291
|
+
* console.log(modifiers.has('ctrl')); // true
|
|
292
|
+
* console.log(modifiers.has('a')); // false
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
static getModifierKeys() {
|
|
296
|
+
return this.MODIFIER_KEYS;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
102
300
|
/**
|
|
103
301
|
* Type guard to detect KeyboardShortcutGroupOptions at runtime.
|
|
104
302
|
* Centralising this logic keeps registerGroup simpler and less fragile.
|
|
@@ -129,9 +327,9 @@ function isDestroyRefLike(obj) {
|
|
|
129
327
|
return typeof o['onDestroy'] === 'function';
|
|
130
328
|
}
|
|
131
329
|
class KeyboardShortcuts {
|
|
132
|
-
static MODIFIER_KEYS = new Set(['control', 'alt', 'shift', 'meta']);
|
|
133
330
|
document = inject(DOCUMENT);
|
|
134
331
|
window = this.document.defaultView;
|
|
332
|
+
config = inject(KEYBOARD_SHORTCUTS_CONFIG);
|
|
135
333
|
shortcuts = new Map();
|
|
136
334
|
groups = new Map();
|
|
137
335
|
activeShortcuts = new Set();
|
|
@@ -150,7 +348,7 @@ class KeyboardShortcuts {
|
|
|
150
348
|
groups: new Map(),
|
|
151
349
|
activeShortcuts: new Set(),
|
|
152
350
|
activeGroups: new Set(),
|
|
153
|
-
version:
|
|
351
|
+
version: INITIAL_STATE_VERSION // for change detection optimization
|
|
154
352
|
}, ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
155
353
|
// Primary computed signal - consumers derive what they need from this
|
|
156
354
|
shortcuts$ = computed(() => {
|
|
@@ -185,8 +383,6 @@ class KeyboardShortcuts {
|
|
|
185
383
|
blurListener = this.handleWindowBlur.bind(this);
|
|
186
384
|
visibilityListener = this.handleVisibilityChange.bind(this);
|
|
187
385
|
isListening = false;
|
|
188
|
-
/** Default timeout (ms) for completing a multi-step sequence */
|
|
189
|
-
sequenceTimeout = 2000;
|
|
190
386
|
/** Runtime state for multi-step sequences */
|
|
191
387
|
pendingSequence = null;
|
|
192
388
|
constructor() {
|
|
@@ -206,7 +402,7 @@ class KeyboardShortcuts {
|
|
|
206
402
|
groups: new Map(this.groups),
|
|
207
403
|
activeShortcuts: new Set(this.activeShortcuts),
|
|
208
404
|
activeGroups: new Set(this.activeGroups),
|
|
209
|
-
version: current.version +
|
|
405
|
+
version: current.version + STATE_VERSION_INCREMENT
|
|
210
406
|
}));
|
|
211
407
|
}
|
|
212
408
|
/**
|
|
@@ -253,17 +449,17 @@ class KeyboardShortcuts {
|
|
|
253
449
|
return '';
|
|
254
450
|
// If the first element is an array, assume steps is string[][]
|
|
255
451
|
const normalized = this.normalizeToSteps(steps);
|
|
256
|
-
if (normalized.length ===
|
|
452
|
+
if (normalized.length === MIN_KEY_LENGTH)
|
|
257
453
|
return '';
|
|
258
|
-
if (normalized.length ===
|
|
259
|
-
return this.formatKeysForDisplay(normalized[
|
|
454
|
+
if (normalized.length === SINGLE_STEP_LENGTH)
|
|
455
|
+
return this.formatKeysForDisplay(normalized[FIRST_INDEX], isMac);
|
|
260
456
|
return normalized.map(step => this.formatKeysForDisplay(step, isMac)).join(', ');
|
|
261
457
|
}
|
|
262
458
|
normalizeToSteps(input) {
|
|
263
459
|
if (!input)
|
|
264
460
|
return [];
|
|
265
461
|
// If first element is an array, assume already KeyStep[]
|
|
266
|
-
if (Array.isArray(input[
|
|
462
|
+
if (Array.isArray(input[FIRST_INDEX])) {
|
|
267
463
|
return input;
|
|
268
464
|
}
|
|
269
465
|
// Single step array
|
|
@@ -293,6 +489,32 @@ class KeyboardShortcuts {
|
|
|
293
489
|
if (newShortcut.macSteps && existing.macSteps && this.stepsMatch(newShortcut.macSteps, existing.macSteps)) {
|
|
294
490
|
return existing.id;
|
|
295
491
|
}
|
|
492
|
+
// Check if new single-step shortcut conflicts with first step of existing multi-step shortcut
|
|
493
|
+
if (newShortcut.keys && existing.steps) {
|
|
494
|
+
const firstStep = existing.steps[FIRST_INDEX];
|
|
495
|
+
if (firstStep && this.keysMatch(newShortcut.keys, firstStep)) {
|
|
496
|
+
return existing.id;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (newShortcut.macKeys && existing.macSteps) {
|
|
500
|
+
const firstStep = existing.macSteps[FIRST_INDEX];
|
|
501
|
+
if (firstStep && this.keysMatch(newShortcut.macKeys, firstStep)) {
|
|
502
|
+
return existing.id;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Check if new multi-step shortcut's first step conflicts with existing single-step shortcut
|
|
506
|
+
if (newShortcut.steps && existing.keys) {
|
|
507
|
+
const firstStep = newShortcut.steps[FIRST_INDEX];
|
|
508
|
+
if (firstStep && this.keysMatch(firstStep, existing.keys)) {
|
|
509
|
+
return existing.id;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (newShortcut.macSteps && existing.macKeys) {
|
|
513
|
+
const firstStep = newShortcut.macSteps[FIRST_INDEX];
|
|
514
|
+
if (firstStep && this.keysMatch(firstStep, existing.macKeys)) {
|
|
515
|
+
return existing.id;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
296
518
|
}
|
|
297
519
|
return null;
|
|
298
520
|
}
|
|
@@ -304,7 +526,7 @@ class KeyboardShortcuts {
|
|
|
304
526
|
const shortcut = this.shortcuts.get(shortcutId);
|
|
305
527
|
if (!shortcut)
|
|
306
528
|
return [];
|
|
307
|
-
const
|
|
529
|
+
const conflictsSet = new Set();
|
|
308
530
|
for (const existing of this.shortcuts.values()) {
|
|
309
531
|
// Skip self and inactive shortcuts
|
|
310
532
|
if (existing.id === shortcutId || !this.activeShortcuts.has(existing.id)) {
|
|
@@ -315,10 +537,41 @@ class KeyboardShortcuts {
|
|
|
315
537
|
(shortcut.macKeys && existing.macKeys && this.keysMatch(shortcut.macKeys, existing.macKeys)) ||
|
|
316
538
|
(shortcut.steps && existing.steps && this.stepsMatch(shortcut.steps, existing.steps)) ||
|
|
317
539
|
(shortcut.macSteps && existing.macSteps && this.stepsMatch(shortcut.macSteps, existing.macSteps))) {
|
|
318
|
-
|
|
540
|
+
conflictsSet.add(existing.id);
|
|
541
|
+
continue; // Skip further checks for this shortcut to avoid duplicate adds
|
|
542
|
+
}
|
|
543
|
+
// Check if shortcut's single-step conflicts with first step of existing multi-step shortcut
|
|
544
|
+
if (shortcut.keys && existing.steps) {
|
|
545
|
+
const firstStep = existing.steps[FIRST_INDEX];
|
|
546
|
+
if (firstStep && this.keysMatch(shortcut.keys, firstStep)) {
|
|
547
|
+
conflictsSet.add(existing.id);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (shortcut.macKeys && existing.macSteps) {
|
|
552
|
+
const firstStep = existing.macSteps[FIRST_INDEX];
|
|
553
|
+
if (firstStep && this.keysMatch(shortcut.macKeys, firstStep)) {
|
|
554
|
+
conflictsSet.add(existing.id);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// Check if shortcut's multi-step first step conflicts with existing single-step shortcut
|
|
559
|
+
if (shortcut.steps && existing.keys) {
|
|
560
|
+
const firstStep = shortcut.steps[FIRST_INDEX];
|
|
561
|
+
if (firstStep && this.keysMatch(firstStep, existing.keys)) {
|
|
562
|
+
conflictsSet.add(existing.id);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (shortcut.macSteps && existing.macKeys) {
|
|
567
|
+
const firstStep = shortcut.macSteps[FIRST_INDEX];
|
|
568
|
+
if (firstStep && this.keysMatch(firstStep, existing.macKeys)) {
|
|
569
|
+
conflictsSet.add(existing.id);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
319
572
|
}
|
|
320
573
|
}
|
|
321
|
-
return
|
|
574
|
+
return Array.from(conflictsSet);
|
|
322
575
|
}
|
|
323
576
|
/**
|
|
324
577
|
* Register a single keyboard shortcut
|
|
@@ -365,10 +618,10 @@ class KeyboardShortcuts {
|
|
|
365
618
|
keyConflicts.push(`"${shortcut.id}" conflicts with active shortcut "${conflictId}"`);
|
|
366
619
|
}
|
|
367
620
|
});
|
|
368
|
-
if (duplicateIds.length >
|
|
621
|
+
if (duplicateIds.length > MIN_KEY_LENGTH) {
|
|
369
622
|
throw KeyboardShortcutsErrorFactory.shortcutIdsAlreadyRegistered(duplicateIds);
|
|
370
623
|
}
|
|
371
|
-
if (keyConflicts.length >
|
|
624
|
+
if (keyConflicts.length > MIN_KEY_LENGTH) {
|
|
372
625
|
throw KeyboardShortcutsErrorFactory.keyConflictsInGroup(keyConflicts);
|
|
373
626
|
}
|
|
374
627
|
// Validate that all shortcuts have unique IDs within the group
|
|
@@ -382,7 +635,7 @@ class KeyboardShortcuts {
|
|
|
382
635
|
groupIds.add(shortcut.id);
|
|
383
636
|
}
|
|
384
637
|
});
|
|
385
|
-
if (duplicatesInGroup.length >
|
|
638
|
+
if (duplicatesInGroup.length > MIN_KEY_LENGTH) {
|
|
386
639
|
throw KeyboardShortcutsErrorFactory.duplicateShortcutsInGroup(duplicatesInGroup);
|
|
387
640
|
}
|
|
388
641
|
// Use batch update to reduce signal updates
|
|
@@ -446,7 +699,7 @@ class KeyboardShortcuts {
|
|
|
446
699
|
}
|
|
447
700
|
// Check for conflicts that would be created by activation
|
|
448
701
|
const conflicts = this.findActivationConflicts(shortcutId);
|
|
449
|
-
if (conflicts.length >
|
|
702
|
+
if (conflicts.length > MIN_KEY_LENGTH) {
|
|
450
703
|
throw KeyboardShortcutsErrorFactory.activationKeyConflict(shortcutId, conflicts);
|
|
451
704
|
}
|
|
452
705
|
this.activeShortcuts.add(shortcutId);
|
|
@@ -478,7 +731,7 @@ class KeyboardShortcuts {
|
|
|
478
731
|
const conflicts = this.findActivationConflicts(shortcut.id);
|
|
479
732
|
allConflicts.push(...conflicts);
|
|
480
733
|
});
|
|
481
|
-
if (allConflicts.length >
|
|
734
|
+
if (allConflicts.length > MIN_KEY_LENGTH) {
|
|
482
735
|
throw KeyboardShortcutsErrorFactory.groupActivationKeyConflict(groupId, allConflicts);
|
|
483
736
|
}
|
|
484
737
|
this.batchUpdate(() => {
|
|
@@ -608,6 +861,147 @@ class KeyboardShortcuts {
|
|
|
608
861
|
hasFilter(name) {
|
|
609
862
|
return this.globalFilters.has(name);
|
|
610
863
|
}
|
|
864
|
+
/**
|
|
865
|
+
* Remove the filter from a specific group
|
|
866
|
+
* @param groupId - The group ID
|
|
867
|
+
* @throws KeyboardShortcutError if group doesn't exist
|
|
868
|
+
*/
|
|
869
|
+
removeGroupFilter(groupId) {
|
|
870
|
+
const group = this.groups.get(groupId);
|
|
871
|
+
if (!group) {
|
|
872
|
+
throw KeyboardShortcutsErrorFactory.cannotDeactivateGroup(groupId);
|
|
873
|
+
}
|
|
874
|
+
if (!group.filter) {
|
|
875
|
+
// No filter to remove, silently succeed
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
this.groups.set(groupId, {
|
|
879
|
+
...group,
|
|
880
|
+
filter: undefined
|
|
881
|
+
});
|
|
882
|
+
this.updateState();
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Remove the filter from a specific shortcut
|
|
886
|
+
* @param shortcutId - The shortcut ID
|
|
887
|
+
* @throws KeyboardShortcutError if shortcut doesn't exist
|
|
888
|
+
*/
|
|
889
|
+
removeShortcutFilter(shortcutId) {
|
|
890
|
+
const shortcut = this.shortcuts.get(shortcutId);
|
|
891
|
+
if (!shortcut) {
|
|
892
|
+
throw KeyboardShortcutsErrorFactory.cannotDeactivateShortcut(shortcutId);
|
|
893
|
+
}
|
|
894
|
+
if (!shortcut.filter) {
|
|
895
|
+
// No filter to remove, silently succeed
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
this.shortcuts.set(shortcutId, {
|
|
899
|
+
...shortcut,
|
|
900
|
+
filter: undefined
|
|
901
|
+
});
|
|
902
|
+
this.updateState();
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Remove all filters from all groups
|
|
906
|
+
*/
|
|
907
|
+
clearAllGroupFilters() {
|
|
908
|
+
this.batchUpdate(() => {
|
|
909
|
+
this.groups.forEach((group, id) => {
|
|
910
|
+
if (group.filter) {
|
|
911
|
+
this.removeGroupFilter(id);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Remove all filters from all shortcuts
|
|
918
|
+
*/
|
|
919
|
+
clearAllShortcutFilters() {
|
|
920
|
+
this.batchUpdate(() => {
|
|
921
|
+
this.shortcuts.forEach((shortcut, id) => {
|
|
922
|
+
if (shortcut.filter) {
|
|
923
|
+
this.removeShortcutFilter(id);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Check if a group has a filter
|
|
930
|
+
* @param groupId - The group ID
|
|
931
|
+
* @returns True if the group has a filter
|
|
932
|
+
*/
|
|
933
|
+
hasGroupFilter(groupId) {
|
|
934
|
+
const group = this.groups.get(groupId);
|
|
935
|
+
return !!group?.filter;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Check if a shortcut has a filter
|
|
939
|
+
* @param shortcutId - The shortcut ID
|
|
940
|
+
* @returns True if the shortcut has a filter
|
|
941
|
+
*/
|
|
942
|
+
hasShortcutFilter(shortcutId) {
|
|
943
|
+
const shortcut = this.shortcuts.get(shortcutId);
|
|
944
|
+
return !!shortcut?.filter;
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Register multiple shortcuts in a single batch update
|
|
948
|
+
* @param shortcuts - Array of shortcuts to register
|
|
949
|
+
*/
|
|
950
|
+
registerMany(shortcuts) {
|
|
951
|
+
this.batchUpdate(() => {
|
|
952
|
+
shortcuts.forEach(shortcut => this.register(shortcut));
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Unregister multiple shortcuts in a single batch update
|
|
957
|
+
* @param ids - Array of shortcut IDs to unregister
|
|
958
|
+
*/
|
|
959
|
+
unregisterMany(ids) {
|
|
960
|
+
this.batchUpdate(() => {
|
|
961
|
+
ids.forEach(id => this.unregister(id));
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Unregister multiple groups in a single batch update
|
|
966
|
+
* @param ids - Array of group IDs to unregister
|
|
967
|
+
*/
|
|
968
|
+
unregisterGroups(ids) {
|
|
969
|
+
this.batchUpdate(() => {
|
|
970
|
+
ids.forEach(id => this.unregisterGroup(id));
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Clear all shortcuts and groups (nuclear reset)
|
|
975
|
+
*/
|
|
976
|
+
clearAll() {
|
|
977
|
+
this.batchUpdate(() => {
|
|
978
|
+
this.shortcuts.clear();
|
|
979
|
+
this.groups.clear();
|
|
980
|
+
this.activeShortcuts.clear();
|
|
981
|
+
this.activeGroups.clear();
|
|
982
|
+
this.shortcutToGroup.clear();
|
|
983
|
+
this.globalFilters.clear();
|
|
984
|
+
this.clearCurrentlyDownKeys();
|
|
985
|
+
this.clearPendingSequence();
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Get all shortcuts belonging to a specific group
|
|
990
|
+
* @param groupId - The group ID
|
|
991
|
+
* @returns Array of shortcuts in the group
|
|
992
|
+
*/
|
|
993
|
+
getGroupShortcuts(groupId) {
|
|
994
|
+
const group = this.groups.get(groupId);
|
|
995
|
+
if (!group)
|
|
996
|
+
return [];
|
|
997
|
+
return [...group.shortcuts];
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Normalize a key to lowercase for consistent comparison
|
|
1001
|
+
*/
|
|
1002
|
+
normalizeKey(key) {
|
|
1003
|
+
return key.toLowerCase();
|
|
1004
|
+
}
|
|
611
1005
|
/**
|
|
612
1006
|
* Find the group that contains a specific shortcut.
|
|
613
1007
|
*
|
|
@@ -677,7 +1071,7 @@ class KeyboardShortcuts {
|
|
|
677
1071
|
// Fast path: if any global filter blocks this event, bail out before
|
|
678
1072
|
// scanning all active shortcuts. This drastically reduces per-event work
|
|
679
1073
|
// when filters are commonly blocking (e.g., while typing in inputs).
|
|
680
|
-
if (this.globalFilters.size >
|
|
1074
|
+
if (this.globalFilters.size > MIN_KEY_LENGTH) {
|
|
681
1075
|
for (const f of this.globalFilters.values()) {
|
|
682
1076
|
if (!f(event)) {
|
|
683
1077
|
// Also clear any pending multi-step sequence – entering a globally
|
|
@@ -716,7 +1110,7 @@ class KeyboardShortcuts {
|
|
|
716
1110
|
if (expected && this.keysMatch(stepPressed, expected)) {
|
|
717
1111
|
// Advance sequence
|
|
718
1112
|
clearTimeout(pending.timerId);
|
|
719
|
-
pending.stepIndex +=
|
|
1113
|
+
pending.stepIndex += STATE_VERSION_INCREMENT;
|
|
720
1114
|
if (pending.stepIndex >= normalizedSteps.length) {
|
|
721
1115
|
// Completed - check filters before executing
|
|
722
1116
|
if (!this.shouldProcessEvent(event, shortcut)) {
|
|
@@ -734,8 +1128,10 @@ class KeyboardShortcuts {
|
|
|
734
1128
|
this.pendingSequence = null;
|
|
735
1129
|
return;
|
|
736
1130
|
}
|
|
737
|
-
// Reset timer for next step
|
|
738
|
-
|
|
1131
|
+
// Reset timer for next step (only if shortcut has timeout configured)
|
|
1132
|
+
if (shortcut.sequenceTimeout !== undefined) {
|
|
1133
|
+
pending.timerId = setTimeout(() => { this.pendingSequence = null; }, shortcut.sequenceTimeout);
|
|
1134
|
+
}
|
|
739
1135
|
return;
|
|
740
1136
|
}
|
|
741
1137
|
else {
|
|
@@ -762,14 +1158,14 @@ class KeyboardShortcuts {
|
|
|
762
1158
|
? (shortcut.macSteps ?? shortcut.macKeys ?? shortcut.steps ?? shortcut.keys ?? [])
|
|
763
1159
|
: (shortcut.steps ?? shortcut.keys ?? shortcut.macSteps ?? shortcut.macKeys ?? []);
|
|
764
1160
|
const normalizedSteps = this.normalizeToSteps(steps);
|
|
765
|
-
const firstStep = normalizedSteps[
|
|
1161
|
+
const firstStep = normalizedSteps[FIRST_INDEX];
|
|
766
1162
|
// Decide which pressed-keys representation to use for this shortcut's
|
|
767
1163
|
// expected step: if it requires multiple non-modifier keys, treat it as
|
|
768
1164
|
// a chord and use accumulated keys; otherwise use per-event keys to avoid
|
|
769
1165
|
// interference from previously pressed non-modifier keys.
|
|
770
|
-
const nonModifierCount = firstStep.filter(k => !
|
|
1166
|
+
const nonModifierCount = firstStep.filter(k => !KeyMatcher.isModifierKey(k)).length;
|
|
771
1167
|
// Normalize pressed keys to a Set<string> for consistent typing
|
|
772
|
-
const pressedForStep = nonModifierCount >
|
|
1168
|
+
const pressedForStep = nonModifierCount > MIN_COUNT_ONE
|
|
773
1169
|
? this.buildPressedKeysForMatch(event)
|
|
774
1170
|
: this.getPressedKeys(event);
|
|
775
1171
|
if (this.keysMatch(pressedForStep, firstStep)) {
|
|
@@ -777,7 +1173,7 @@ class KeyboardShortcuts {
|
|
|
777
1173
|
if (!this.shouldProcessEvent(event, shortcut)) {
|
|
778
1174
|
continue; // Skip this shortcut due to filters
|
|
779
1175
|
}
|
|
780
|
-
if (normalizedSteps.length ===
|
|
1176
|
+
if (normalizedSteps.length === SINGLE_STEP_LENGTH) {
|
|
781
1177
|
// single-step
|
|
782
1178
|
event.preventDefault();
|
|
783
1179
|
event.stopPropagation();
|
|
@@ -794,10 +1190,13 @@ class KeyboardShortcuts {
|
|
|
794
1190
|
if (this.pendingSequence) {
|
|
795
1191
|
this.clearPendingSequence();
|
|
796
1192
|
}
|
|
1193
|
+
const timerId = shortcut.sequenceTimeout !== undefined
|
|
1194
|
+
? setTimeout(() => { this.pendingSequence = null; }, shortcut.sequenceTimeout)
|
|
1195
|
+
: null;
|
|
797
1196
|
this.pendingSequence = {
|
|
798
1197
|
shortcutId: shortcut.id,
|
|
799
|
-
stepIndex:
|
|
800
|
-
timerId
|
|
1198
|
+
stepIndex: SECOND_STEP_INDEX,
|
|
1199
|
+
timerId
|
|
801
1200
|
};
|
|
802
1201
|
event.preventDefault();
|
|
803
1202
|
event.stopPropagation();
|
|
@@ -807,9 +1206,9 @@ class KeyboardShortcuts {
|
|
|
807
1206
|
}
|
|
808
1207
|
}
|
|
809
1208
|
handleKeyup(event) {
|
|
810
|
-
// Remove the key from
|
|
1209
|
+
// Remove the key from currently DownKeys on keyup
|
|
811
1210
|
const key = event.key ? event.key.toLowerCase() : '';
|
|
812
|
-
if (key && !
|
|
1211
|
+
if (key && !KeyMatcher.isModifierKey(key)) {
|
|
813
1212
|
this.currentlyDownKeys.delete(key);
|
|
814
1213
|
}
|
|
815
1214
|
}
|
|
@@ -834,6 +1233,11 @@ class KeyboardShortcuts {
|
|
|
834
1233
|
this.clearCurrentlyDownKeys();
|
|
835
1234
|
this.clearPendingSequence();
|
|
836
1235
|
}
|
|
1236
|
+
else if (this.document.visibilityState === 'visible') {
|
|
1237
|
+
// When returning to visibility, clear keys to avoid stale state
|
|
1238
|
+
// from keys that may have been released while document was hidden
|
|
1239
|
+
this.clearCurrentlyDownKeys();
|
|
1240
|
+
}
|
|
837
1241
|
}
|
|
838
1242
|
/**
|
|
839
1243
|
* Update the currentlyDownKeys set when keydown events happen.
|
|
@@ -843,7 +1247,7 @@ class KeyboardShortcuts {
|
|
|
843
1247
|
updateCurrentlyDownKeysOnKeydown(event) {
|
|
844
1248
|
const key = event.key ? event.key.toLowerCase() : '';
|
|
845
1249
|
// Ignore modifier-only keydown entries
|
|
846
|
-
if (
|
|
1250
|
+
if (KeyMatcher.isModifierKey(key)) {
|
|
847
1251
|
return;
|
|
848
1252
|
}
|
|
849
1253
|
// Normalize some special cases similar to the demo component's recording logic
|
|
@@ -863,7 +1267,7 @@ class KeyboardShortcuts {
|
|
|
863
1267
|
this.currentlyDownKeys.add('enter');
|
|
864
1268
|
return;
|
|
865
1269
|
}
|
|
866
|
-
if (key && key.length >
|
|
1270
|
+
if (key && key.length > MIN_KEY_LENGTH) {
|
|
867
1271
|
this.currentlyDownKeys.add(key);
|
|
868
1272
|
}
|
|
869
1273
|
}
|
|
@@ -886,17 +1290,17 @@ class KeyboardShortcuts {
|
|
|
886
1290
|
if (event.metaKey)
|
|
887
1291
|
modifiers.add('meta');
|
|
888
1292
|
// Collect non-modifier keys from currentlyDownKeys (excluding modifiers)
|
|
889
|
-
const nonModifierKeys = Array.from(this.currentlyDownKeys).filter(k => !
|
|
1293
|
+
const nonModifierKeys = Array.from(this.currentlyDownKeys).filter(k => !KeyMatcher.isModifierKey(k));
|
|
890
1294
|
const result = new Set();
|
|
891
1295
|
// Add modifiers first
|
|
892
1296
|
modifiers.forEach(m => result.add(m));
|
|
893
|
-
if (nonModifierKeys.length >
|
|
1297
|
+
if (nonModifierKeys.length > MIN_KEY_LENGTH) {
|
|
894
1298
|
nonModifierKeys.forEach(k => result.add(k.toLowerCase()));
|
|
895
1299
|
return result;
|
|
896
1300
|
}
|
|
897
1301
|
// Fallback: single main key from the event (existing behavior)
|
|
898
1302
|
const key = event.key.toLowerCase();
|
|
899
|
-
if (!
|
|
1303
|
+
if (!KeyMatcher.isModifierKey(key)) {
|
|
900
1304
|
result.add(key);
|
|
901
1305
|
}
|
|
902
1306
|
return result;
|
|
@@ -917,7 +1321,7 @@ class KeyboardShortcuts {
|
|
|
917
1321
|
result.add('meta');
|
|
918
1322
|
// Add the main key (normalize to lowercase) if it's not a modifier
|
|
919
1323
|
const key = (event.key ?? '').toLowerCase();
|
|
920
|
-
if (key && !
|
|
1324
|
+
if (key && !KeyMatcher.isModifierKey(key)) {
|
|
921
1325
|
result.add(key);
|
|
922
1326
|
}
|
|
923
1327
|
return result;
|
|
@@ -927,40 +1331,28 @@ class KeyboardShortcuts {
|
|
|
927
1331
|
* Accepts either a Set<string> (preferred) or an array for backwards compatibility.
|
|
928
1332
|
* Uses Set-based comparison: sizes must match and every element in target must exist in pressed.
|
|
929
1333
|
*/
|
|
1334
|
+
/**
|
|
1335
|
+
* @deprecated Use KeyMatcher.keysMatch() instead
|
|
1336
|
+
*/
|
|
930
1337
|
keysMatch(pressedKeys, targetKeys) {
|
|
931
|
-
|
|
932
|
-
const normalizedTarget = new Set(targetKeys.map(k => k.toLowerCase()));
|
|
933
|
-
// Normalize pressedKeys into a Set<string> if it's an array
|
|
934
|
-
const pressedSet = Array.isArray(pressedKeys)
|
|
935
|
-
? new Set(pressedKeys.map(k => k.toLowerCase()))
|
|
936
|
-
: new Set(Array.from(pressedKeys).map(k => k.toLowerCase()));
|
|
937
|
-
if (pressedSet.size !== normalizedTarget.size) {
|
|
938
|
-
return false;
|
|
939
|
-
}
|
|
940
|
-
// Check if every element in normalizedTarget exists in pressedSet
|
|
941
|
-
for (const key of normalizedTarget) {
|
|
942
|
-
if (!pressedSet.has(key)) {
|
|
943
|
-
return false;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
return true;
|
|
1338
|
+
return KeyMatcher.keysMatch(pressedKeys, targetKeys);
|
|
947
1339
|
}
|
|
948
|
-
/**
|
|
1340
|
+
/**
|
|
1341
|
+
* Compare two multi-step sequences for equality
|
|
1342
|
+
* @deprecated Use KeyMatcher.stepsMatch() instead
|
|
1343
|
+
*/
|
|
949
1344
|
stepsMatch(a, b) {
|
|
950
|
-
|
|
951
|
-
return false;
|
|
952
|
-
for (let i = 0; i < a.length; i++) {
|
|
953
|
-
if (!this.keysMatch(a[i], b[i]))
|
|
954
|
-
return false;
|
|
955
|
-
}
|
|
956
|
-
return true;
|
|
1345
|
+
return KeyMatcher.stepsMatch(a, b);
|
|
957
1346
|
}
|
|
958
1347
|
/** Safely clear any pending multi-step sequence */
|
|
959
1348
|
clearPendingSequence() {
|
|
960
1349
|
if (!this.pendingSequence)
|
|
961
1350
|
return;
|
|
962
1351
|
try {
|
|
963
|
-
|
|
1352
|
+
// Only clear timeout if one was set
|
|
1353
|
+
if (this.pendingSequence.timerId !== null) {
|
|
1354
|
+
clearTimeout(this.pendingSequence.timerId);
|
|
1355
|
+
}
|
|
964
1356
|
}
|
|
965
1357
|
catch { /* ignore */ }
|
|
966
1358
|
this.pendingSequence = null;
|
|
@@ -993,7 +1385,7 @@ class KeyboardShortcuts {
|
|
|
993
1385
|
*/
|
|
994
1386
|
precomputeBlockedGroups(event) {
|
|
995
1387
|
const blocked = new Set();
|
|
996
|
-
if (this.activeGroups.size ===
|
|
1388
|
+
if (this.activeGroups.size === MIN_KEY_LENGTH)
|
|
997
1389
|
return blocked;
|
|
998
1390
|
for (const groupId of this.activeGroups) {
|
|
999
1391
|
const group = this.groups.get(groupId);
|
|
@@ -1013,6 +1405,235 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImpor
|
|
|
1013
1405
|
}]
|
|
1014
1406
|
}], ctorParameters: () => [] });
|
|
1015
1407
|
|
|
1408
|
+
/**
|
|
1409
|
+
* Directive for registering keyboard shortcuts directly on elements in templates.
|
|
1410
|
+
*
|
|
1411
|
+
* This directive automatically:
|
|
1412
|
+
* - Registers the shortcut when the directive initializes
|
|
1413
|
+
* - Triggers the host element's click event (default) or executes a custom action
|
|
1414
|
+
* - Unregisters the shortcut when the component is destroyed
|
|
1415
|
+
*
|
|
1416
|
+
* @example
|
|
1417
|
+
* Basic usage with click trigger:
|
|
1418
|
+
* ```html
|
|
1419
|
+
* <button ngxKeys
|
|
1420
|
+
* keys="ctrl,s"
|
|
1421
|
+
* description="Save document"
|
|
1422
|
+
* (click)="save()">
|
|
1423
|
+
* Save
|
|
1424
|
+
* </button>
|
|
1425
|
+
* ```
|
|
1426
|
+
*
|
|
1427
|
+
* @example
|
|
1428
|
+
* With custom action:
|
|
1429
|
+
* ```html
|
|
1430
|
+
* <button ngxKeys
|
|
1431
|
+
* keys="ctrl,s"
|
|
1432
|
+
* description="Save document"
|
|
1433
|
+
* [action]="customSaveAction">
|
|
1434
|
+
* Save
|
|
1435
|
+
* </button>
|
|
1436
|
+
* ```
|
|
1437
|
+
*
|
|
1438
|
+
* @example
|
|
1439
|
+
* Multi-step shortcuts:
|
|
1440
|
+
* ```html
|
|
1441
|
+
* <button ngxKeys
|
|
1442
|
+
* [steps]="[['ctrl', 'k'], ['ctrl', 's']]"
|
|
1443
|
+
* description="Format document"
|
|
1444
|
+
* (click)="format()">
|
|
1445
|
+
* Format
|
|
1446
|
+
* </button>
|
|
1447
|
+
* ```
|
|
1448
|
+
*
|
|
1449
|
+
* @example
|
|
1450
|
+
* Mac-specific keys:
|
|
1451
|
+
* ```html
|
|
1452
|
+
* <button ngxKeys
|
|
1453
|
+
* keys="ctrl,s"
|
|
1454
|
+
* macKeys="cmd,s"
|
|
1455
|
+
* description="Save document"
|
|
1456
|
+
* (click)="save()">
|
|
1457
|
+
* Save
|
|
1458
|
+
* </button>
|
|
1459
|
+
* ```
|
|
1460
|
+
*
|
|
1461
|
+
* @example
|
|
1462
|
+
* On non-interactive elements:
|
|
1463
|
+
* ```html
|
|
1464
|
+
* <div ngxKeys
|
|
1465
|
+
* keys="?"
|
|
1466
|
+
* description="Show help"
|
|
1467
|
+
* [action]="showHelp">
|
|
1468
|
+
* Help content
|
|
1469
|
+
* </div>
|
|
1470
|
+
* ```
|
|
1471
|
+
*/
|
|
1472
|
+
class KeyboardShortcutDirective {
|
|
1473
|
+
keyboardShortcuts = inject(KeyboardShortcuts);
|
|
1474
|
+
elementRef = inject((ElementRef));
|
|
1475
|
+
/**
|
|
1476
|
+
* Comma-separated string of keys for the shortcut (e.g., "ctrl,s" or "alt,shift,k")
|
|
1477
|
+
* Use either `keys` for single-step shortcuts or `steps` for multi-step shortcuts.
|
|
1478
|
+
*/
|
|
1479
|
+
keys;
|
|
1480
|
+
/**
|
|
1481
|
+
* Comma-separated string of keys for Mac users (e.g., "cmd,s")
|
|
1482
|
+
* If not provided, `keys` will be used for all platforms.
|
|
1483
|
+
*/
|
|
1484
|
+
macKeys;
|
|
1485
|
+
/**
|
|
1486
|
+
* Multi-step shortcut as an array of key arrays.
|
|
1487
|
+
* Each inner array represents one step in the sequence.
|
|
1488
|
+
* Example: [['ctrl', 'k'], ['ctrl', 's']] for Ctrl+K followed by Ctrl+S
|
|
1489
|
+
*/
|
|
1490
|
+
steps;
|
|
1491
|
+
/**
|
|
1492
|
+
* Multi-step shortcut for Mac users.
|
|
1493
|
+
* Example: [['cmd', 'k'], ['cmd', 's']]
|
|
1494
|
+
*/
|
|
1495
|
+
macSteps;
|
|
1496
|
+
/**
|
|
1497
|
+
* Description of what the shortcut does (displayed in help/documentation)
|
|
1498
|
+
*/
|
|
1499
|
+
description;
|
|
1500
|
+
/**
|
|
1501
|
+
* Optional custom action to execute when the shortcut is triggered.
|
|
1502
|
+
* If not provided, the directive will trigger a click event on the host element.
|
|
1503
|
+
*/
|
|
1504
|
+
action;
|
|
1505
|
+
/**
|
|
1506
|
+
* Optional custom ID for the shortcut. If not provided, a unique ID will be generated.
|
|
1507
|
+
* Useful for programmatically referencing the shortcut or for debugging.
|
|
1508
|
+
*/
|
|
1509
|
+
shortcutId;
|
|
1510
|
+
/**
|
|
1511
|
+
* Event emitted when the keyboard shortcut is triggered.
|
|
1512
|
+
* This fires in addition to the action or click trigger.
|
|
1513
|
+
*/
|
|
1514
|
+
triggered = output();
|
|
1515
|
+
/**
|
|
1516
|
+
* Adds a data attribute to the host element for styling or testing purposes
|
|
1517
|
+
*/
|
|
1518
|
+
get dataAttribute() {
|
|
1519
|
+
return this.generatedId;
|
|
1520
|
+
}
|
|
1521
|
+
generatedId = '';
|
|
1522
|
+
isRegistered = false;
|
|
1523
|
+
ngOnInit() {
|
|
1524
|
+
// Generate unique ID if not provided
|
|
1525
|
+
this.generatedId = this.shortcutId || this.generateUniqueId();
|
|
1526
|
+
// Validate inputs
|
|
1527
|
+
this.validateInputs();
|
|
1528
|
+
// Parse keys from comma-separated strings
|
|
1529
|
+
const parsedKeys = this.keys ? this.parseKeys(this.keys) : undefined;
|
|
1530
|
+
const parsedMacKeys = this.macKeys ? this.parseKeys(this.macKeys) : undefined;
|
|
1531
|
+
// Define the action: custom action or default click behavior
|
|
1532
|
+
const shortcutAction = () => {
|
|
1533
|
+
if (this.action) {
|
|
1534
|
+
this.action();
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
// Trigger click on the host element
|
|
1538
|
+
this.elementRef.nativeElement.click();
|
|
1539
|
+
}
|
|
1540
|
+
// Emit the triggered event
|
|
1541
|
+
this.triggered.emit();
|
|
1542
|
+
};
|
|
1543
|
+
// Register the shortcut
|
|
1544
|
+
try {
|
|
1545
|
+
this.keyboardShortcuts.register({
|
|
1546
|
+
id: this.generatedId,
|
|
1547
|
+
keys: parsedKeys,
|
|
1548
|
+
macKeys: parsedMacKeys,
|
|
1549
|
+
steps: this.steps,
|
|
1550
|
+
macSteps: this.macSteps,
|
|
1551
|
+
action: shortcutAction,
|
|
1552
|
+
description: this.description,
|
|
1553
|
+
});
|
|
1554
|
+
this.isRegistered = true;
|
|
1555
|
+
}
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
console.error(`[ngxKeys] Failed to register shortcut:`, error);
|
|
1558
|
+
throw error;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
ngOnDestroy() {
|
|
1562
|
+
// Automatically unregister the shortcut when the directive is destroyed
|
|
1563
|
+
if (this.isRegistered) {
|
|
1564
|
+
try {
|
|
1565
|
+
this.keyboardShortcuts.unregister(this.generatedId);
|
|
1566
|
+
}
|
|
1567
|
+
catch (error) {
|
|
1568
|
+
// Silently handle unregister errors (shortcut might have been manually removed)
|
|
1569
|
+
console.warn(`[ngxKeys] Failed to unregister shortcut ${this.generatedId}:`, error);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Parse comma-separated key string into an array
|
|
1575
|
+
* Example: "ctrl,s" -> ["ctrl", "s"]
|
|
1576
|
+
*/
|
|
1577
|
+
parseKeys(keysString) {
|
|
1578
|
+
return keysString
|
|
1579
|
+
.split(',')
|
|
1580
|
+
.map(key => key.trim())
|
|
1581
|
+
.filter(key => key.length > 0);
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Generate a unique ID for the shortcut based on the element and keys
|
|
1585
|
+
*/
|
|
1586
|
+
generateUniqueId() {
|
|
1587
|
+
const timestamp = Date.now();
|
|
1588
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
1589
|
+
const keysStr = this.keys || this.steps?.flat().join('-') || 'unknown';
|
|
1590
|
+
return `ngx-shortcut-${keysStr}-${timestamp}-${random}`;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Validate that required inputs are provided correctly
|
|
1594
|
+
*/
|
|
1595
|
+
validateInputs() {
|
|
1596
|
+
const hasSingleStep = this.keys || this.macKeys;
|
|
1597
|
+
const hasMultiStep = this.steps || this.macSteps;
|
|
1598
|
+
if (!hasSingleStep && !hasMultiStep) {
|
|
1599
|
+
throw new Error(`[ngxKeys] Must provide either 'keys'/'macKeys' for single-step shortcuts or 'steps'/'macSteps' for multi-step shortcuts.`);
|
|
1600
|
+
}
|
|
1601
|
+
if (hasSingleStep && hasMultiStep) {
|
|
1602
|
+
throw new Error(`[ngxKeys] Cannot use both single-step ('keys'/'macKeys') and multi-step ('steps'/'macSteps') inputs simultaneously. Choose one approach.`);
|
|
1603
|
+
}
|
|
1604
|
+
if (!this.description) {
|
|
1605
|
+
throw new Error(`[ngxKeys] 'description' input is required.`);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: KeyboardShortcutDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1609
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.4", type: KeyboardShortcutDirective, isStandalone: true, selector: "[ngxKeys]", inputs: { keys: "keys", macKeys: "macKeys", steps: "steps", macSteps: "macSteps", description: "description", action: "action", shortcutId: "shortcutId" }, outputs: { triggered: "triggered" }, host: { properties: { "attr.data-keyboard-shortcut": "this.dataAttribute" } }, ngImport: i0 });
|
|
1610
|
+
}
|
|
1611
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: KeyboardShortcutDirective, decorators: [{
|
|
1612
|
+
type: Directive,
|
|
1613
|
+
args: [{
|
|
1614
|
+
selector: '[ngxKeys]',
|
|
1615
|
+
standalone: true,
|
|
1616
|
+
}]
|
|
1617
|
+
}], propDecorators: { keys: [{
|
|
1618
|
+
type: Input
|
|
1619
|
+
}], macKeys: [{
|
|
1620
|
+
type: Input
|
|
1621
|
+
}], steps: [{
|
|
1622
|
+
type: Input
|
|
1623
|
+
}], macSteps: [{
|
|
1624
|
+
type: Input
|
|
1625
|
+
}], description: [{
|
|
1626
|
+
type: Input,
|
|
1627
|
+
args: [{ required: true }]
|
|
1628
|
+
}], action: [{
|
|
1629
|
+
type: Input
|
|
1630
|
+
}], shortcutId: [{
|
|
1631
|
+
type: Input
|
|
1632
|
+
}], dataAttribute: [{
|
|
1633
|
+
type: HostBinding,
|
|
1634
|
+
args: ['attr.data-keyboard-shortcut']
|
|
1635
|
+
}] } });
|
|
1636
|
+
|
|
1016
1637
|
/*
|
|
1017
1638
|
* Public API Surface of ngx-keys
|
|
1018
1639
|
*/
|
|
@@ -1021,5 +1642,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImpor
|
|
|
1021
1642
|
* Generated bundle index. Do not edit.
|
|
1022
1643
|
*/
|
|
1023
1644
|
|
|
1024
|
-
export { KeyboardShortcutError, KeyboardShortcuts, KeyboardShortcutsErrorFactory, KeyboardShortcutsErrors };
|
|
1645
|
+
export { KEYBOARD_SHORTCUTS_CONFIG, KeyboardShortcutDirective, KeyboardShortcutError, KeyboardShortcuts, KeyboardShortcutsErrorFactory, KeyboardShortcutsErrors, provideKeyboardShortcutsConfig };
|
|
1025
1646
|
//# sourceMappingURL=ngx-keys.mjs.map
|