ngx-keys 1.0.1 → 1.2.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/README.md +466 -8
- package/fesm2022/ngx-keys.mjs +595 -65
- package/fesm2022/ngx-keys.mjs.map +1 -1
- package/index.d.ts +251 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,8 @@ A lightweight, reactive Angular service for managing keyboard shortcuts with sig
|
|
|
10
10
|
- **🌍 Cross-Platform**: Automatic Mac/PC key display formatting
|
|
11
11
|
- **🔄 Dynamic Management**: Add, remove, activate/deactivate shortcuts at runtime
|
|
12
12
|
- **📁 Group Management**: Organize shortcuts into logical groups
|
|
13
|
-
-
|
|
13
|
+
- **� Smart Conflict Detection**: Register multiple shortcuts with same keys when not simultaneously active
|
|
14
|
+
- **�🪶 Lightweight**: Zero dependencies, minimal bundle impact
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -122,6 +123,79 @@ this.keyboardService.register({
|
|
|
122
123
|
});
|
|
123
124
|
```
|
|
124
125
|
|
|
126
|
+
### Smart Conflict Detection
|
|
127
|
+
> [!IMPORTANT]
|
|
128
|
+
Conflicts are only checked among **active** shortcuts, not all registered shortcuts.
|
|
129
|
+
|
|
130
|
+
ngx-keys allows registering multiple shortcuts with the same key combination, as long as they're not simultaneously active. This enables powerful patterns:
|
|
131
|
+
|
|
132
|
+
- **Context-specific shortcuts**: Same keys for different UI contexts
|
|
133
|
+
- **Alternative shortcuts**: Multiple ways to trigger the same action
|
|
134
|
+
- **Feature toggles**: Same keys for different modes
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Basic conflict handling
|
|
138
|
+
this.keyboardService.register(shortcut1); // Active by default
|
|
139
|
+
this.keyboardService.deactivate('shortcut1');
|
|
140
|
+
this.keyboardService.register(shortcut2); // Same keys, but shortcut1 is inactive ✅
|
|
141
|
+
|
|
142
|
+
// This would fail - conflicts with active shortcut2
|
|
143
|
+
// this.keyboardService.activate('shortcut1'); // ❌ Throws error
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Group Management
|
|
147
|
+
|
|
148
|
+
Organize related shortcuts into groups for easier management:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const editorShortcuts = [
|
|
152
|
+
{
|
|
153
|
+
id: 'bold',
|
|
154
|
+
keys: ['ctrl', 'b'],
|
|
155
|
+
macKeys: ['meta', 'b'],
|
|
156
|
+
action: () => this.makeBold(),
|
|
157
|
+
description: 'Make text bold'
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'italic',
|
|
161
|
+
keys: ['ctrl', 'i'],
|
|
162
|
+
macKeys: ['meta', 'i'],
|
|
163
|
+
action: () => this.makeItalic(),
|
|
164
|
+
description: 'Make text italic'
|
|
165
|
+
}
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
// Register all shortcuts in the group
|
|
169
|
+
this.keyboardService.registerGroup('editor', editorShortcuts);
|
|
170
|
+
|
|
171
|
+
// Control the entire group
|
|
172
|
+
this.keyboardService.deactivateGroup('editor'); // Disable all editor shortcuts
|
|
173
|
+
this.keyboardService.activateGroup('editor'); // Re-enable all editor shortcuts
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Multi-step (sequence) shortcuts
|
|
177
|
+
|
|
178
|
+
In addition to single-step shortcuts using `keys` / `macKeys`, ngx-keys supports ordered multi-step sequences using `steps` and `macSteps` on the `KeyboardShortcut` object. Each element in `steps` is itself an array of key tokens that must be pressed together for that step.
|
|
179
|
+
|
|
180
|
+
Example: register a sequence that requires `Ctrl+K` followed by `S`:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
this.keyboardService.register({
|
|
184
|
+
id: 'open-settings-seq',
|
|
185
|
+
steps: [['ctrl', 'k'], ['s']],
|
|
186
|
+
macSteps: [['meta', 'k'], ['s']],
|
|
187
|
+
action: () => this.openSettings(),
|
|
188
|
+
description: 'Open settings (Ctrl+K then S)'
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Important behavior notes**
|
|
193
|
+
|
|
194
|
+
- Default sequence timeout: the service requires the next step to be entered within 2000ms (2 seconds) of the previous step; otherwise the pending sequence is cleared. This timeout is intentionally conservative and can be changed in future releases or exposed per-shortcut if needed.
|
|
195
|
+
- Steps are order-sensitive. `steps: [['ctrl','k'], ['s']]` is different from `steps: [['s'], ['ctrl','k']]`.
|
|
196
|
+
- Existing single-step `keys` / `macKeys` remain supported and continue to work as before.
|
|
197
|
+
|
|
198
|
+
|
|
125
199
|
Use the `activate()` and `deactivate()` methods for dynamic control after registration:
|
|
126
200
|
|
|
127
201
|
```typescript
|
|
@@ -132,6 +206,155 @@ this.keyboardService.deactivate('save');
|
|
|
132
206
|
this.keyboardService.activate('save');
|
|
133
207
|
```
|
|
134
208
|
|
|
209
|
+
## Advanced Usage
|
|
210
|
+
|
|
211
|
+
### Context-Specific Shortcuts
|
|
212
|
+
|
|
213
|
+
Register different actions for the same keys in different UI contexts:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// Modal context
|
|
217
|
+
this.keyboardService.register({
|
|
218
|
+
id: 'modal-escape',
|
|
219
|
+
keys: ['escape'],
|
|
220
|
+
action: () => this.closeModal(),
|
|
221
|
+
description: 'Close modal'
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Initially deactivate since modal isn't shown
|
|
225
|
+
this.keyboardService.deactivate('modal-escape');
|
|
226
|
+
|
|
227
|
+
// Editor context
|
|
228
|
+
this.keyboardService.register({
|
|
229
|
+
id: 'editor-escape',
|
|
230
|
+
keys: ['escape'], // Same key, different context
|
|
231
|
+
action: () => this.exitEditMode(),
|
|
232
|
+
description: 'Exit edit mode'
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Switch contexts dynamically
|
|
236
|
+
showModal() {
|
|
237
|
+
this.keyboardService.deactivate('editor-escape');
|
|
238
|
+
this.keyboardService.activate('modal-escape');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
hideModal() {
|
|
242
|
+
this.keyboardService.deactivate('modal-escape');
|
|
243
|
+
this.keyboardService.activate('editor-escape');
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Alternative Shortcuts
|
|
248
|
+
|
|
249
|
+
Provide multiple ways to trigger the same functionality:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// Primary shortcut
|
|
253
|
+
this.keyboardService.register({
|
|
254
|
+
id: 'help-f1',
|
|
255
|
+
keys: ['f1'],
|
|
256
|
+
action: () => this.showHelp(),
|
|
257
|
+
description: 'Show help (F1)'
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Alternative shortcut - different keys, same action
|
|
261
|
+
this.keyboardService.register({
|
|
262
|
+
id: 'help-ctrl-h',
|
|
263
|
+
keys: ['ctrl', 'h'],
|
|
264
|
+
action: () => this.showHelp(), // Same action
|
|
265
|
+
description: 'Show help (Ctrl+H)'
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Both are active simultaneously since they don't conflict
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Feature Toggles
|
|
272
|
+
|
|
273
|
+
Switch between different modes that use the same keys:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// Design mode
|
|
277
|
+
this.keyboardService.register({
|
|
278
|
+
id: 'design-mode-space',
|
|
279
|
+
keys: ['space'],
|
|
280
|
+
action: () => this.toggleDesignElement(),
|
|
281
|
+
description: 'Toggle design element'
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Play mode (same key, different action)
|
|
285
|
+
this.keyboardService.register({
|
|
286
|
+
id: 'play-mode-space',
|
|
287
|
+
keys: ['space'],
|
|
288
|
+
action: () => this.pausePlayback(),
|
|
289
|
+
description: 'Pause/resume playback'
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Initially deactivate play mode
|
|
293
|
+
this.keyboardService.deactivate('play-mode-space');
|
|
294
|
+
|
|
295
|
+
// Switch modes
|
|
296
|
+
switchToPlayMode() {
|
|
297
|
+
this.keyboardService.deactivate('design-mode-space');
|
|
298
|
+
this.keyboardService.activate('play-mode-space');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
switchToDesignMode() {
|
|
302
|
+
this.keyboardService.deactivate('play-mode-space');
|
|
303
|
+
this.keyboardService.activate('design-mode-space');
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Advanced Group Patterns
|
|
308
|
+
|
|
309
|
+
Use groups for complex activation/deactivation scenarios:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// Create context-specific groups
|
|
313
|
+
const modalShortcuts = [
|
|
314
|
+
{ id: 'modal-close', keys: ['escape'], action: () => this.closeModal(), description: 'Close modal' },
|
|
315
|
+
{ id: 'modal-confirm', keys: ['enter'], action: () => this.confirmModal(), description: 'Confirm' }
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
const editorShortcuts = [
|
|
319
|
+
{ id: 'editor-save', keys: ['ctrl', 's'], action: () => this.save(), description: 'Save' },
|
|
320
|
+
{ id: 'editor-undo', keys: ['ctrl', 'z'], action: () => this.undo(), description: 'Undo' }
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
// Register both groups
|
|
324
|
+
this.keyboardService.registerGroup('modal', modalShortcuts);
|
|
325
|
+
this.keyboardService.registerGroup('editor', editorShortcuts);
|
|
326
|
+
|
|
327
|
+
// Initially only editor is active
|
|
328
|
+
this.keyboardService.deactivateGroup('modal');
|
|
329
|
+
|
|
330
|
+
// Switch contexts
|
|
331
|
+
showModal() {
|
|
332
|
+
this.keyboardService.deactivateGroup('editor');
|
|
333
|
+
this.keyboardService.activateGroup('modal');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
hideModal() {
|
|
337
|
+
this.keyboardService.deactivateGroup('modal');
|
|
338
|
+
this.keyboardService.activateGroup('editor');
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Conflict Detection Rules
|
|
343
|
+
|
|
344
|
+
- **Registration**: Only checks conflicts with currently **active** shortcuts
|
|
345
|
+
- **Activation**: Throws error if activating would conflict with other active shortcuts
|
|
346
|
+
- **Groups**: Same rules apply - groups can contain conflicting shortcuts as long as they're not simultaneously active
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// ✅ This works - shortcuts with same keys but only one active at a time
|
|
350
|
+
this.keyboardService.register(shortcut1); // Active by default
|
|
351
|
+
this.keyboardService.deactivate('shortcut1');
|
|
352
|
+
this.keyboardService.register(shortcut2); // Same keys, but shortcut1 is inactive
|
|
353
|
+
|
|
354
|
+
// ❌ This fails - trying to activate would create conflict
|
|
355
|
+
this.keyboardService.activate('shortcut1'); // Throws error - conflicts with active shortcut2
|
|
356
|
+
```
|
|
357
|
+
|
|
135
358
|
## API Reference
|
|
136
359
|
|
|
137
360
|
### KeyboardShortcuts Service
|
|
@@ -139,15 +362,18 @@ this.keyboardService.activate('save');
|
|
|
139
362
|
#### Methods
|
|
140
363
|
|
|
141
364
|
**Registration Methods:**
|
|
142
|
-
|
|
143
|
-
|
|
365
|
+
> [!TIP]
|
|
366
|
+
Conflicts are only checked among **active** shortcuts, not all registered shortcuts.
|
|
367
|
+
|
|
368
|
+
- `register(shortcut: KeyboardShortcut)` - Register and automatically activate a single shortcut *Throws error on conflicts with active shortcuts only*
|
|
369
|
+
- `registerGroup(groupId: string, shortcuts: KeyboardShortcut[])` - Register and automatically activate a group of shortcuts *Throws error on conflicts with active shortcuts only*
|
|
144
370
|
|
|
145
371
|
**Management Methods:**
|
|
146
372
|
- `unregister(shortcutId: string)` - Remove a shortcut *Throws error if not found*
|
|
147
373
|
- `unregisterGroup(groupId: string)` - Remove a group *Throws error if not found*
|
|
148
|
-
- `activate(shortcutId: string)` - Activate a shortcut *Throws error if not registered*
|
|
374
|
+
- `activate(shortcutId: string)` - Activate a shortcut *Throws error if not registered or would create conflicts*
|
|
149
375
|
- `deactivate(shortcutId: string)` - Deactivate a shortcut *Throws error if not registered*
|
|
150
|
-
- `activateGroup(groupId: string)` - Activate all shortcuts in a group *Throws error if not found*
|
|
376
|
+
- `activateGroup(groupId: string)` - Activate all shortcuts in a group *Throws error if not found or would create conflicts*
|
|
151
377
|
- `deactivateGroup(groupId: string)` - Deactivate all shortcuts in a group *Throws error if not found*
|
|
152
378
|
|
|
153
379
|
**Query Methods:**
|
|
@@ -201,8 +427,14 @@ interface KeyboardShortcutUI {
|
|
|
201
427
|
```typescript
|
|
202
428
|
interface KeyboardShortcut {
|
|
203
429
|
id: string; // Unique identifier
|
|
204
|
-
|
|
205
|
-
|
|
430
|
+
// Single-step combinations (existing API)
|
|
431
|
+
keys?: string[]; // Key combination for PC/Linux (e.g., ['ctrl', 's'])
|
|
432
|
+
macKeys?: string[]; // Key combination for Mac (e.g., ['meta', 's'])
|
|
433
|
+
|
|
434
|
+
// Multi-step sequences (new)
|
|
435
|
+
// Each step is an array of keys pressed together. Example: steps: [['ctrl','k'], ['s']]
|
|
436
|
+
steps?: string[][];
|
|
437
|
+
macSteps?: string[][];
|
|
206
438
|
action: () => void; // Function to execute
|
|
207
439
|
description: string; // Human-readable description
|
|
208
440
|
}
|
|
@@ -356,6 +588,62 @@ export class FeatureComponent {
|
|
|
356
588
|
}
|
|
357
589
|
```
|
|
358
590
|
|
|
591
|
+
### Automatic unregistering
|
|
592
|
+
|
|
593
|
+
`register` and `registerGroup` have the optional parameter: `activeUntil`.
|
|
594
|
+
The `activeUntil` parameter allows you to connect the shortcut to the wrappers lifecycle or logic in general.
|
|
595
|
+
|
|
596
|
+
`activeUntil` supports three types:
|
|
597
|
+
- `'destruct'`: the shortcut injects the parents `DestroyRef` and unregisters once the component destructs
|
|
598
|
+
- `DestroyRef`: DestroyRef which should trigger the destruction of the shortcut
|
|
599
|
+
- `Observable<unknown>`: an Observable which will unregister the shortcut when triggered
|
|
600
|
+
|
|
601
|
+
#### Example: `'destruct'`
|
|
602
|
+
Shortcuts defined by this component will only be listening during the lifecycle of the component.
|
|
603
|
+
Shortcuts are registered on construction and are automatically unregistered on destruction.
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
export class Component {
|
|
607
|
+
constructor() {
|
|
608
|
+
const keyboardService = inject(KeyboardShortcuts)
|
|
609
|
+
|
|
610
|
+
keyboardService.register({
|
|
611
|
+
// ...
|
|
612
|
+
activeUntil: 'destruct', // alternatively: inject(DestroyRef)
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
keyboardService.registerGroup(
|
|
616
|
+
'shortcuts',
|
|
617
|
+
[/* ... */],
|
|
618
|
+
'destruct', // alternatively: inject(DestroyRef)
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
#### Example: `Observable`
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
const shortcutTTL = new Subject<void>();
|
|
628
|
+
|
|
629
|
+
keyboardService.register({
|
|
630
|
+
// ...
|
|
631
|
+
activeUntil: shortcutTTL,
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
keyboardService.registerGroup(
|
|
635
|
+
'shortcuts',
|
|
636
|
+
[/* ... */],
|
|
637
|
+
shortcutTTL,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
// Shortcuts are listening...
|
|
641
|
+
|
|
642
|
+
shortcutTTL.next();
|
|
643
|
+
|
|
644
|
+
// Shortcuts are unregistered
|
|
645
|
+
```
|
|
646
|
+
|
|
359
647
|
### Batch Operations
|
|
360
648
|
|
|
361
649
|
For better performance when making multiple changes, use the `batchUpdate` method.
|
|
@@ -423,6 +711,176 @@ export class MyComponent {
|
|
|
423
711
|
}
|
|
424
712
|
```
|
|
425
713
|
|
|
714
|
+
### Chords (multiple non-modifier keys)
|
|
715
|
+
|
|
716
|
+
- ngx-keys supports chords composed of multiple non-modifier keys pressed simultaneously (for example `C + A`).
|
|
717
|
+
- When multiple non-modifier keys are physically held down at the same time, the service uses the set of currently pressed keys plus any modifier flags to match registered shortcuts.
|
|
718
|
+
- Example: register a chord with `keys: ['c','a']` and pressing and holding `c` then pressing `a` will trigger the shortcut.
|
|
719
|
+
- Note: Browsers deliver separate keydown events for each physical key; the library maintains a Set of currently-down keys via `keydown`/`keyup` listeners to enable chords. This approach attempts to be robust but can be affected by browser focus changes — ensure tests in your target browsers.
|
|
720
|
+
|
|
721
|
+
Example registration:
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
this.keyboardService.register({
|
|
725
|
+
id: 'chord-ca',
|
|
726
|
+
keys: ['c', 'a'],
|
|
727
|
+
macKeys: ['c', 'a'],
|
|
728
|
+
action: () => console.log('Chord C+A executed'),
|
|
729
|
+
description: 'Demo chord'
|
|
730
|
+
});
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Event Filtering
|
|
734
|
+
|
|
735
|
+
You can configure which keyboard events should be processed by setting a filter function. This is useful for ignoring shortcuts when users are typing in input fields, text areas, or other form elements.
|
|
736
|
+
|
|
737
|
+
> [!NOTE]
|
|
738
|
+
> **No Default Filtering**: ngx-keys processes ALL keyboard events by default. This gives you maximum flexibility - some apps want shortcuts to work everywhere, others want to exclude form inputs. You decide!
|
|
739
|
+
|
|
740
|
+
#### Named filters (recommended)
|
|
741
|
+
|
|
742
|
+
For efficiency and control, prefer named global filters. You can toggle them on/off without replacing others, and ngx-keys evaluates them only once per keydown event (fast path), short‑circuiting further work when blocked.
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
// Add named filters
|
|
746
|
+
keyboardService.addFilter('forms', (event) => {
|
|
747
|
+
const t = event.target as HTMLElement | null;
|
|
748
|
+
const tag = t?.tagName?.toLowerCase();
|
|
749
|
+
return !(['input', 'textarea', 'select'].includes(tag ?? '')) && !t?.isContentEditable;
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
keyboardService.addFilter('modal-scope', (event) => {
|
|
753
|
+
const t = event.target as HTMLElement | null;
|
|
754
|
+
return !!t?.closest('.modal');
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Remove/toggle when context changes
|
|
758
|
+
keyboardService.removeFilter('modal-scope');
|
|
759
|
+
|
|
760
|
+
// Inspect and manage
|
|
761
|
+
keyboardService.getFilterNames(); // ['forms']
|
|
762
|
+
keyboardService.clearFilters(); // remove all
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
import { Component, inject } from '@angular/core';
|
|
767
|
+
import { KeyboardShortcuts, KeyboardShortcutFilter } from 'ngx-keys';
|
|
768
|
+
|
|
769
|
+
export class FilterExampleComponent {
|
|
770
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
771
|
+
|
|
772
|
+
constructor() {
|
|
773
|
+
// Set up shortcuts
|
|
774
|
+
this.keyboardService.register({
|
|
775
|
+
id: 'save',
|
|
776
|
+
keys: ['ctrl', 's'],
|
|
777
|
+
macKeys: ['meta', 's'],
|
|
778
|
+
action: () => this.save(),
|
|
779
|
+
description: 'Save document'
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// Configure filtering to ignore form elements
|
|
783
|
+
this.setupInputFiltering();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private setupInputFiltering() {
|
|
787
|
+
const inputFilter: KeyboardShortcutFilter = (event) => {
|
|
788
|
+
const target = event.target as HTMLElement;
|
|
789
|
+
const tagName = target?.tagName?.toLowerCase();
|
|
790
|
+
return !['input', 'textarea', 'select'].includes(tagName) && !target?.isContentEditable;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// Use named filter for toggling
|
|
794
|
+
this.keyboardService.addFilter('forms', inputFilter);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private save() {
|
|
798
|
+
console.log('Document saved!');
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
#### Common Filter Patterns
|
|
804
|
+
|
|
805
|
+
**Ignore form elements:**
|
|
806
|
+
```typescript
|
|
807
|
+
const formFilter: KeyboardShortcutFilter = (event) => {
|
|
808
|
+
const target = event.target as HTMLElement;
|
|
809
|
+
const tagName = target?.tagName?.toLowerCase();
|
|
810
|
+
return !['input', 'textarea', 'select'].includes(tagName) && !target?.isContentEditable;
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
keyboardService.addFilter('forms', formFilter);
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**Ignore elements with specific attributes:**
|
|
817
|
+
```typescript
|
|
818
|
+
const attributeFilter: KeyboardShortcutFilter = (event) => {
|
|
819
|
+
const target = event.target as HTMLElement;
|
|
820
|
+
return !target?.hasAttribute('data-no-shortcuts');
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
keyboardService.addFilter('no-shortcuts-attr', attributeFilter);
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
**Complex conditional filtering:**
|
|
827
|
+
```typescript
|
|
828
|
+
const conditionalFilter: KeyboardShortcutFilter = (event) => {
|
|
829
|
+
const target = event.target as HTMLElement;
|
|
830
|
+
|
|
831
|
+
// Allow shortcuts in code editors (even though they're contentEditable)
|
|
832
|
+
if (target?.classList?.contains('code-editor')) {
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Block shortcuts in form elements
|
|
837
|
+
if (target?.tagName?.match(/INPUT|TEXTAREA|SELECT/i) || target?.isContentEditable) {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return true;
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
keyboardService.addFilter('conditional', conditionalFilter);
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
**Remove filtering:**
|
|
848
|
+
```typescript
|
|
849
|
+
// Remove a specific named filter
|
|
850
|
+
keyboardService.removeFilter('forms');
|
|
851
|
+
// Or remove all
|
|
852
|
+
keyboardService.clearFilters();
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
#### Example: Modal Context Filtering
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
export class ModalComponent {
|
|
859
|
+
constructor() {
|
|
860
|
+
// When modal opens, only allow modal-specific shortcuts
|
|
861
|
+
this.keyboardService.addFilter('modal-scope', (event) => {
|
|
862
|
+
const target = event.target as HTMLElement;
|
|
863
|
+
|
|
864
|
+
// Only process events within the modal
|
|
865
|
+
return target?.closest('.modal') !== null;
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
onClose() {
|
|
870
|
+
// Restore normal filtering when modal closes
|
|
871
|
+
this.keyboardService.removeFilter('modal-scope');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
#### Performance tips
|
|
877
|
+
|
|
878
|
+
- Filters are evaluated once per keydown before scanning shortcuts. If any global filter returns false, ngx-keys exits early and clears pending sequences.
|
|
879
|
+
- Group-level filters are precomputed once per event; shortcuts in blocked groups are skipped without key matching.
|
|
880
|
+
- Keep filters cheap and synchronous. Prefer reading event.target properties (tagName, isContentEditable, classList) over layout-triggering queries.
|
|
881
|
+
- Use named filters to toggle contexts (modals, editors) without allocating new closures per interaction.
|
|
882
|
+
- Avoid complex DOM traversals inside filters; if needed, memoize simple queries or use attributes (e.g., data-no-shortcuts).
|
|
883
|
+
|
|
426
884
|
## Building
|
|
427
885
|
|
|
428
886
|
To build the library:
|
|
@@ -451,4 +909,4 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
451
909
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
452
910
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
453
911
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
454
|
-
5. Open a Pull Request
|
|
912
|
+
5. Open a Pull Request
|