ngx-keys 1.1.0 โ 1.3.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 +808 -19
- package/fesm2022/ngx-keys.mjs +935 -119
- package/fesm2022/ngx-keys.mjs.map +1 -1
- package/index.d.ts +484 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,11 +5,14 @@ A lightweight, reactive Angular service for managing keyboard shortcuts with sig
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **๐ฏ Service-Focused**: Clean, focused API without unnecessary UI components
|
|
8
|
+
- **๐ Declarative Directive**: Optional directive for template-based shortcut registration
|
|
8
9
|
- **โก Reactive Signals**: Track active/inactive shortcuts with Angular signals
|
|
9
10
|
- **๐ง UI-Agnostic**: Build your own UI using the provided reactive signals
|
|
10
11
|
- **๐ Cross-Platform**: Automatic Mac/PC key display formatting
|
|
11
12
|
- **๐ Dynamic Management**: Add, remove, activate/deactivate shortcuts at runtime
|
|
12
13
|
- **๐ Group Management**: Organize shortcuts into logical groups
|
|
14
|
+
- **โ๏ธ Configurable**: Customize sequence timeout and other behavior via dependency injection
|
|
15
|
+
- **๐ Smart Conflict Detection**: Register multiple shortcuts with same keys when not simultaneously active
|
|
13
16
|
- **๐ชถ Lightweight**: Zero dependencies, minimal bundle impact
|
|
14
17
|
|
|
15
18
|
## Installation
|
|
@@ -83,7 +86,110 @@ export class MyComponent {
|
|
|
83
86
|
}
|
|
84
87
|
```
|
|
85
88
|
|
|
89
|
+
### Using the Directive (Declarative Approach)
|
|
86
90
|
|
|
91
|
+
For a more declarative approach, use the `ngxKeys` directive directly on your elements:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { Component } from '@angular/core';
|
|
95
|
+
import { KeyboardShortcutDirective } from 'ngx-keys';
|
|
96
|
+
|
|
97
|
+
@Component({
|
|
98
|
+
standalone: true,
|
|
99
|
+
imports: [KeyboardShortcutDirective],
|
|
100
|
+
template: `
|
|
101
|
+
<h3>My App</h3>
|
|
102
|
+
<p>Last action: {{ lastAction }}</p>
|
|
103
|
+
|
|
104
|
+
<!-- Directive automatically registers and unregisters shortcuts -->
|
|
105
|
+
<button
|
|
106
|
+
ngxKeys
|
|
107
|
+
keys="ctrl,s"
|
|
108
|
+
macKeys="cmd,s"
|
|
109
|
+
description="Save document"
|
|
110
|
+
(click)="save()">
|
|
111
|
+
Save
|
|
112
|
+
</button>
|
|
113
|
+
|
|
114
|
+
<button
|
|
115
|
+
ngxKeys
|
|
116
|
+
keys="ctrl,h"
|
|
117
|
+
macKeys="cmd,h"
|
|
118
|
+
description="Show help"
|
|
119
|
+
(click)="showHelp()">
|
|
120
|
+
Help
|
|
121
|
+
</button>
|
|
122
|
+
|
|
123
|
+
<!-- Multi-step shortcuts work too -->
|
|
124
|
+
<button
|
|
125
|
+
ngxKeys
|
|
126
|
+
[steps]="[['ctrl', 'k'], ['ctrl', 'p']]"
|
|
127
|
+
[macSteps]="[['cmd', 'k'], ['cmd', 'p']]"
|
|
128
|
+
description="Open command palette (Ctrl+K then Ctrl+P)"
|
|
129
|
+
(click)="openCommandPalette()">
|
|
130
|
+
Command Palette
|
|
131
|
+
</button>
|
|
132
|
+
|
|
133
|
+
<!-- Use custom action instead of click -->
|
|
134
|
+
<div
|
|
135
|
+
ngxKeys
|
|
136
|
+
keys="?"
|
|
137
|
+
description="Show help overlay"
|
|
138
|
+
[action]="showHelpOverlay">
|
|
139
|
+
Press ? for help
|
|
140
|
+
</div>
|
|
141
|
+
`
|
|
142
|
+
})
|
|
143
|
+
export class MyComponent {
|
|
144
|
+
protected lastAction = 'Try pressing Ctrl+S or Ctrl+H';
|
|
145
|
+
|
|
146
|
+
protected readonly showHelpOverlay = () => {
|
|
147
|
+
this.lastAction = 'Help overlay displayed!';
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
protected save() {
|
|
151
|
+
this.lastAction = 'Document saved!';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected showHelp() {
|
|
155
|
+
this.lastAction = 'Help displayed!';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
protected openCommandPalette() {
|
|
159
|
+
this.lastAction = 'Command palette opened!';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> [!TIP]
|
|
165
|
+
> The directive automatically:
|
|
166
|
+
> - Registers shortcuts when initialized
|
|
167
|
+
> - Triggers click events on the host element (or executes a custom action)
|
|
168
|
+
> - Unregisters shortcuts when destroyed
|
|
169
|
+
> - Adds a `data-keyboard-shortcut` attribute for styling/testing
|
|
170
|
+
|
|
171
|
+
**Directive Inputs:**
|
|
172
|
+
|
|
173
|
+
| Input | Type | Description |
|
|
174
|
+
|-------|------|-------------|
|
|
175
|
+
| `keys` | `string` | Comma-separated keys for single-step shortcut (e.g., `"ctrl,s"`) |
|
|
176
|
+
| `macKeys` | `string` | Comma-separated keys for Mac (e.g., `"cmd,s"`) |
|
|
177
|
+
| `steps` | `string[][]` | Multi-step sequence (e.g., `[['ctrl', 'k'], ['ctrl', 'd']]`) |
|
|
178
|
+
| `macSteps` | `string[][]` | Multi-step sequence for Mac |
|
|
179
|
+
| `description` | `string` | Required. Description of what the shortcut does |
|
|
180
|
+
| `action` | `() => void` | Optional custom action. If not provided, triggers click on host element |
|
|
181
|
+
| `shortcutId` | `string` | Optional custom ID. If not provided, generates unique ID |
|
|
182
|
+
|
|
183
|
+
**Directive Outputs:**
|
|
184
|
+
|
|
185
|
+
| Output | Type | Description |
|
|
186
|
+
|--------|------|-------------|
|
|
187
|
+
| `triggered` | `void` | Emitted when the keyboard shortcut is triggered |
|
|
188
|
+
|
|
189
|
+
**When to use the directive vs. the service:**
|
|
190
|
+
|
|
191
|
+
- **Use the directive** when shortcuts are tied to specific UI elements and their lifecycle
|
|
192
|
+
- **Use the service** when you need programmatic control, dynamic shortcuts, or shortcuts not tied to elements
|
|
87
193
|
|
|
88
194
|
## Explore the Demo
|
|
89
195
|
|
|
@@ -94,6 +200,7 @@ Want to see ngx-keys in action? Check out our comprehensive [demo application](/
|
|
|
94
200
|
| [**App Component**](/projects/demo/src/app/app.ts) | Global shortcuts | Single & group registration, cleanup patterns |
|
|
95
201
|
| [**Home Component**](/projects/demo/src/app/home/home.component.ts) | Reactive UI | Real-time status display, toggle controls |
|
|
96
202
|
| [**Feature Component**](/projects/demo/src/app/feature/feature.component.ts) | Route-specific shortcuts | Scoped shortcuts, lifecycle management |
|
|
203
|
+
| [**Directive Demo**](/projects/demo/src/app/directive-demo/directive-demo.component.ts) | Declarative shortcuts | Directive usage, automatic lifecycle management |
|
|
97
204
|
| [**Customize Component**](/projects/demo/src/app/customize/customize.component.ts) | Dynamic shortcut recording | Real-time key capture, shortcut customization |
|
|
98
205
|
|
|
99
206
|
**Run the demo:**
|
|
@@ -122,27 +229,107 @@ this.keyboardService.register({
|
|
|
122
229
|
});
|
|
123
230
|
```
|
|
124
231
|
|
|
232
|
+
### Smart Conflict Detection
|
|
233
|
+
> [!IMPORTANT]
|
|
234
|
+
Conflicts are only checked among **active** shortcuts, not all registered shortcuts.
|
|
235
|
+
|
|
236
|
+
ngx-keys allows registering multiple shortcuts with the same key combination, as long as they're not simultaneously active. This enables powerful patterns:
|
|
237
|
+
|
|
238
|
+
- **Context-specific shortcuts**: Same keys for different UI contexts
|
|
239
|
+
- **Alternative shortcuts**: Multiple ways to trigger the same action
|
|
240
|
+
- **Feature toggles**: Same keys for different modes
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// Basic conflict handling
|
|
244
|
+
this.keyboardService.register(shortcut1); // Active by default
|
|
245
|
+
this.keyboardService.deactivate('shortcut1');
|
|
246
|
+
this.keyboardService.register(shortcut2); // Same keys, but shortcut1 is inactive โ
|
|
247
|
+
|
|
248
|
+
// This would fail - conflicts with active shortcut2
|
|
249
|
+
// this.keyboardService.activate('shortcut1'); // โ Throws error
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Group Management
|
|
253
|
+
|
|
254
|
+
Organize related shortcuts into groups for easier management:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
const editorShortcuts = [
|
|
258
|
+
{
|
|
259
|
+
id: 'bold',
|
|
260
|
+
keys: ['ctrl', 'b'],
|
|
261
|
+
macKeys: ['meta', 'b'],
|
|
262
|
+
action: () => this.makeBold(),
|
|
263
|
+
description: 'Make text bold'
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: 'italic',
|
|
267
|
+
keys: ['ctrl', 'i'],
|
|
268
|
+
macKeys: ['meta', 'i'],
|
|
269
|
+
action: () => this.makeItalic(),
|
|
270
|
+
description: 'Make text italic'
|
|
271
|
+
}
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
// Register all shortcuts in the group
|
|
275
|
+
this.keyboardService.registerGroup('editor', editorShortcuts);
|
|
276
|
+
|
|
277
|
+
// Control the entire group
|
|
278
|
+
this.keyboardService.deactivateGroup('editor'); // Disable all editor shortcuts
|
|
279
|
+
this.keyboardService.activateGroup('editor'); // Re-enable all editor shortcuts
|
|
280
|
+
```
|
|
281
|
+
|
|
125
282
|
### Multi-step (sequence) shortcuts
|
|
126
283
|
|
|
127
284
|
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.
|
|
128
285
|
|
|
129
|
-
Example: register a sequence that requires `Ctrl+K` followed by `
|
|
286
|
+
Example: register a sequence that requires `Ctrl+K` followed by `Ctrl+D`:
|
|
130
287
|
|
|
131
288
|
```typescript
|
|
132
289
|
this.keyboardService.register({
|
|
133
|
-
id: '
|
|
134
|
-
steps: [['ctrl', 'k'], ['
|
|
135
|
-
macSteps: [['meta', 'k'], ['
|
|
136
|
-
action: () => this.
|
|
137
|
-
description: '
|
|
290
|
+
id: 'format-document-seq',
|
|
291
|
+
steps: [['ctrl', 'k'], ['ctrl', 'd']],
|
|
292
|
+
macSteps: [['meta', 'k'], ['meta', 'd']],
|
|
293
|
+
action: () => this.formatDocument(),
|
|
294
|
+
description: 'Format document (Ctrl+K then Ctrl+D)'
|
|
138
295
|
});
|
|
139
296
|
```
|
|
140
297
|
|
|
141
|
-
|
|
298
|
+
**Configuring Sequence Timeout**
|
|
299
|
+
|
|
300
|
+
By default, multi-step shortcuts have **no timeout** - users can take as long as they need between steps. You can configure a timeout globally using a provider function:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { ApplicationConfig } from '@angular/core';
|
|
304
|
+
import { provideKeyboardShortcutsConfig } from 'ngx-keys';
|
|
305
|
+
|
|
306
|
+
export const appConfig: ApplicationConfig = {
|
|
307
|
+
providers: [
|
|
308
|
+
provideKeyboardShortcutsConfig({ sequenceTimeoutMs: 2000 }) // 2 seconds
|
|
309
|
+
]
|
|
310
|
+
};
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Alternatively, you can use the injection token directly:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { ApplicationConfig } from '@angular/core';
|
|
317
|
+
import { KEYBOARD_SHORTCUTS_CONFIG } from 'ngx-keys';
|
|
318
|
+
|
|
319
|
+
export const appConfig: ApplicationConfig = {
|
|
320
|
+
providers: [
|
|
321
|
+
{
|
|
322
|
+
provide: KEYBOARD_SHORTCUTS_CONFIG,
|
|
323
|
+
useValue: { sequenceTimeoutMs: 2000 } // 2 seconds
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Important behavior notes**
|
|
142
330
|
|
|
143
|
-
-
|
|
144
|
-
- Steps are order-sensitive. `steps: [['ctrl','k'], ['s']]` is different from `steps: [['s'], ['ctrl','k']]`.
|
|
145
|
-
- Existing single-step `keys` / `macKeys` remain supported and continue to work as before.
|
|
331
|
+
- **Sequence timeout**: Steps must be entered within the configured timeout (default 2000ms) or the sequence is cleared.
|
|
332
|
+
- **Order-sensitive**: Steps are order-sensitive. `steps: [['ctrl','k'], ['s']]` is different from `steps: [['s'], ['ctrl','k']]`.
|
|
146
333
|
|
|
147
334
|
|
|
148
335
|
Use the `activate()` and `deactivate()` methods for dynamic control after registration:
|
|
@@ -155,6 +342,201 @@ this.keyboardService.deactivate('save');
|
|
|
155
342
|
this.keyboardService.activate('save');
|
|
156
343
|
```
|
|
157
344
|
|
|
345
|
+
## Advanced Usage
|
|
346
|
+
|
|
347
|
+
### Type-Safe Action Functions
|
|
348
|
+
|
|
349
|
+
For better type safety and code reusability, use the exported `Action` type when defining action functions:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { Component, inject } from '@angular/core';
|
|
353
|
+
import { KeyboardShortcuts, Action } from 'ngx-keys';
|
|
354
|
+
|
|
355
|
+
export class MyComponent {
|
|
356
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
357
|
+
|
|
358
|
+
// Define reusable, type-safe action functions
|
|
359
|
+
private readonly saveAction: Action = () => {
|
|
360
|
+
console.log('Saving document...');
|
|
361
|
+
this.performSave();
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
private readonly undoAction: Action = () => {
|
|
365
|
+
console.log('Undoing...');
|
|
366
|
+
this.performUndo();
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
constructor() {
|
|
370
|
+
// Use the typed actions in shortcuts
|
|
371
|
+
this.keyboardService.register({
|
|
372
|
+
id: 'save',
|
|
373
|
+
keys: ['ctrl', 's'],
|
|
374
|
+
macKeys: ['meta', 's'],
|
|
375
|
+
action: this.saveAction,
|
|
376
|
+
description: 'Save document'
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
this.keyboardService.register({
|
|
380
|
+
id: 'undo',
|
|
381
|
+
keys: ['ctrl', 'z'],
|
|
382
|
+
macKeys: ['meta', 'z'],
|
|
383
|
+
action: this.undoAction,
|
|
384
|
+
description: 'Undo last action'
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private performSave() { /* implementation */ }
|
|
389
|
+
private performUndo() { /* implementation */ }
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Context-Specific Shortcuts
|
|
394
|
+
|
|
395
|
+
Register different actions for the same keys in different UI contexts:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
// Modal context
|
|
399
|
+
this.keyboardService.register({
|
|
400
|
+
id: 'modal-escape',
|
|
401
|
+
keys: ['escape'],
|
|
402
|
+
action: () => this.closeModal(),
|
|
403
|
+
description: 'Close modal'
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Initially deactivate since modal isn't shown
|
|
407
|
+
this.keyboardService.deactivate('modal-escape');
|
|
408
|
+
|
|
409
|
+
// Editor context
|
|
410
|
+
this.keyboardService.register({
|
|
411
|
+
id: 'editor-escape',
|
|
412
|
+
keys: ['escape'], // Same key, different context
|
|
413
|
+
action: () => this.exitEditMode(),
|
|
414
|
+
description: 'Exit edit mode'
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Switch contexts dynamically
|
|
418
|
+
showModal() {
|
|
419
|
+
this.keyboardService.deactivate('editor-escape');
|
|
420
|
+
this.keyboardService.activate('modal-escape');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
hideModal() {
|
|
424
|
+
this.keyboardService.deactivate('modal-escape');
|
|
425
|
+
this.keyboardService.activate('editor-escape');
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Alternative Shortcuts
|
|
430
|
+
|
|
431
|
+
Provide multiple ways to trigger the same functionality:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
// Primary shortcut
|
|
435
|
+
this.keyboardService.register({
|
|
436
|
+
id: 'help-f1',
|
|
437
|
+
keys: ['f1'],
|
|
438
|
+
action: () => this.showHelp(),
|
|
439
|
+
description: 'Show help (F1)'
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Alternative shortcut - different keys, same action
|
|
443
|
+
this.keyboardService.register({
|
|
444
|
+
id: 'help-ctrl-h',
|
|
445
|
+
keys: ['ctrl', 'h'],
|
|
446
|
+
action: () => this.showHelp(), // Same action
|
|
447
|
+
description: 'Show help (Ctrl+H)'
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Both are active simultaneously since they don't conflict
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Feature Toggles
|
|
454
|
+
|
|
455
|
+
Switch between different modes that use the same keys:
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// Design mode
|
|
459
|
+
this.keyboardService.register({
|
|
460
|
+
id: 'design-mode-space',
|
|
461
|
+
keys: ['space'],
|
|
462
|
+
action: () => this.toggleDesignElement(),
|
|
463
|
+
description: 'Toggle design element'
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Play mode (same key, different action)
|
|
467
|
+
this.keyboardService.register({
|
|
468
|
+
id: 'play-mode-space',
|
|
469
|
+
keys: ['space'],
|
|
470
|
+
action: () => this.pausePlayback(),
|
|
471
|
+
description: 'Pause/resume playback'
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Initially deactivate play mode
|
|
475
|
+
this.keyboardService.deactivate('play-mode-space');
|
|
476
|
+
|
|
477
|
+
// Switch modes
|
|
478
|
+
switchToPlayMode() {
|
|
479
|
+
this.keyboardService.deactivate('design-mode-space');
|
|
480
|
+
this.keyboardService.activate('play-mode-space');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
switchToDesignMode() {
|
|
484
|
+
this.keyboardService.deactivate('play-mode-space');
|
|
485
|
+
this.keyboardService.activate('design-mode-space');
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Advanced Group Patterns
|
|
490
|
+
|
|
491
|
+
Use groups for complex activation/deactivation scenarios:
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// Create context-specific groups
|
|
495
|
+
const modalShortcuts = [
|
|
496
|
+
{ id: 'modal-close', keys: ['escape'], action: () => this.closeModal(), description: 'Close modal' },
|
|
497
|
+
{ id: 'modal-confirm', keys: ['enter'], action: () => this.confirmModal(), description: 'Confirm' }
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
const editorShortcuts = [
|
|
501
|
+
{ id: 'editor-save', keys: ['ctrl', 's'], action: () => this.save(), description: 'Save' },
|
|
502
|
+
{ id: 'editor-undo', keys: ['ctrl', 'z'], action: () => this.undo(), description: 'Undo' }
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
// Register both groups
|
|
506
|
+
this.keyboardService.registerGroup('modal', modalShortcuts);
|
|
507
|
+
this.keyboardService.registerGroup('editor', editorShortcuts);
|
|
508
|
+
|
|
509
|
+
// Initially only editor is active
|
|
510
|
+
this.keyboardService.deactivateGroup('modal');
|
|
511
|
+
|
|
512
|
+
// Switch contexts
|
|
513
|
+
showModal() {
|
|
514
|
+
this.keyboardService.deactivateGroup('editor');
|
|
515
|
+
this.keyboardService.activateGroup('modal');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
hideModal() {
|
|
519
|
+
this.keyboardService.deactivateGroup('modal');
|
|
520
|
+
this.keyboardService.activateGroup('editor');
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Conflict Detection Rules
|
|
525
|
+
|
|
526
|
+
- **Registration**: Only checks conflicts with currently **active** shortcuts
|
|
527
|
+
- **Activation**: Throws error if activating would conflict with other active shortcuts
|
|
528
|
+
- **Groups**: Same rules apply - groups can contain conflicting shortcuts as long as they're not simultaneously active
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// โ
This works - shortcuts with same keys but only one active at a time
|
|
532
|
+
this.keyboardService.register(shortcut1); // Active by default
|
|
533
|
+
this.keyboardService.deactivate('shortcut1');
|
|
534
|
+
this.keyboardService.register(shortcut2); // Same keys, but shortcut1 is inactive
|
|
535
|
+
|
|
536
|
+
// โ This fails - trying to activate would create conflict
|
|
537
|
+
this.keyboardService.activate('shortcut1'); // Throws error - conflicts with active shortcut2
|
|
538
|
+
```
|
|
539
|
+
|
|
158
540
|
## API Reference
|
|
159
541
|
|
|
160
542
|
### KeyboardShortcuts Service
|
|
@@ -162,17 +544,43 @@ this.keyboardService.activate('save');
|
|
|
162
544
|
#### Methods
|
|
163
545
|
|
|
164
546
|
**Registration Methods:**
|
|
165
|
-
|
|
166
|
-
|
|
547
|
+
> [!TIP]
|
|
548
|
+
Conflicts are only checked among **active** shortcuts, not all registered shortcuts.
|
|
549
|
+
|
|
550
|
+
- `register(shortcut: KeyboardShortcut)` - Register and automatically activate a single shortcut *Throws error on conflicts with active shortcuts only*
|
|
551
|
+
- `registerGroup(groupId: string, shortcuts: KeyboardShortcut[])` - Register and automatically activate a group of shortcuts *Throws error on conflicts with active shortcuts only*
|
|
552
|
+
- `registerMany(shortcuts: KeyboardShortcut[])` - Register multiple shortcuts in a single batch update
|
|
553
|
+
|
|
554
|
+
**Unregistration Methods:**
|
|
555
|
+
> [!NOTE]
|
|
556
|
+
> `unregister()` automatically removes shortcuts from all groups they belong to.
|
|
557
|
+
|
|
558
|
+
- `unregister(shortcutId: string)` - Remove a shortcut and its group associations *Throws error if not found*
|
|
559
|
+
- `unregisterGroup(groupId: string)` - Remove a group and all its shortcuts *Throws error if not found*
|
|
560
|
+
- `unregisterMany(ids: string[])` - Unregister multiple shortcuts in a single batch update
|
|
561
|
+
- `unregisterGroups(ids: string[])` - Unregister multiple groups in a single batch update
|
|
562
|
+
- `clearAll()` - Remove all shortcuts, groups, and filters (nuclear reset)
|
|
167
563
|
|
|
168
|
-
**
|
|
169
|
-
- `
|
|
170
|
-
- `unregisterGroup(groupId: string)` - Remove a group *Throws error if not found*
|
|
171
|
-
- `activate(shortcutId: string)` - Activate a shortcut *Throws error if not registered*
|
|
564
|
+
**Activation Methods:**
|
|
565
|
+
- `activate(shortcutId: string)` - Activate a shortcut *Throws error if not registered or would create conflicts*
|
|
172
566
|
- `deactivate(shortcutId: string)` - Deactivate a shortcut *Throws error if not registered*
|
|
173
|
-
- `activateGroup(groupId: string)` - Activate all shortcuts in a group *Throws error if not found*
|
|
567
|
+
- `activateGroup(groupId: string)` - Activate all shortcuts in a group *Throws error if not found or would create conflicts*
|
|
174
568
|
- `deactivateGroup(groupId: string)` - Deactivate all shortcuts in a group *Throws error if not found*
|
|
175
569
|
|
|
570
|
+
**Filter Methods:**
|
|
571
|
+
- `addFilter(name: string, filter: Function)` - Add a named global filter
|
|
572
|
+
- `removeFilter(name: string)` - Remove a named global filter
|
|
573
|
+
- `clearFilters()` - Remove all global filters
|
|
574
|
+
- `hasFilter(name: string): boolean` - Check if a filter exists
|
|
575
|
+
- `getFilter(name: string)` - Get a filter function by name
|
|
576
|
+
- `getFilterNames(): string[]` - Get all filter names
|
|
577
|
+
- `removeGroupFilter(groupId: string)` - Remove filter from a group
|
|
578
|
+
- `removeShortcutFilter(shortcutId: string)` - Remove filter from a shortcut
|
|
579
|
+
- `clearAllGroupFilters()` - Remove all group filters
|
|
580
|
+
- `clearAllShortcutFilters()` - Remove all shortcut filters
|
|
581
|
+
- `hasGroupFilter(groupId: string): boolean` - Check if group has a filter
|
|
582
|
+
- `hasShortcutFilter(shortcutId: string): boolean` - Check if shortcut has a filter
|
|
583
|
+
|
|
176
584
|
**Query Methods:**
|
|
177
585
|
- `isActive(shortcutId: string): boolean` - Check if a shortcut is active
|
|
178
586
|
- `isRegistered(shortcutId: string): boolean` - Check if a shortcut is registered
|
|
@@ -180,6 +588,7 @@ this.keyboardService.activate('save');
|
|
|
180
588
|
- `isGroupRegistered(groupId: string): boolean` - Check if a group is registered
|
|
181
589
|
- `getShortcuts(): ReadonlyMap<string, KeyboardShortcut>` - Get all registered shortcuts
|
|
182
590
|
- `getGroups(): ReadonlyMap<string, KeyboardShortcutGroup>` - Get all registered groups
|
|
591
|
+
- `getGroupShortcuts(groupId: string): KeyboardShortcut[]` - Get all shortcuts in a specific group
|
|
183
592
|
|
|
184
593
|
**Utility Methods:**
|
|
185
594
|
- `formatShortcutForUI(shortcut: KeyboardShortcut): KeyboardShortcutUI` - Format a shortcut for display
|
|
@@ -222,6 +631,9 @@ interface KeyboardShortcutUI {
|
|
|
222
631
|
### KeyboardShortcut Interface
|
|
223
632
|
|
|
224
633
|
```typescript
|
|
634
|
+
// Type for shortcut action functions
|
|
635
|
+
type Action = () => void;
|
|
636
|
+
|
|
225
637
|
interface KeyboardShortcut {
|
|
226
638
|
id: string; // Unique identifier
|
|
227
639
|
// Single-step combinations (existing API)
|
|
@@ -232,7 +644,7 @@ interface KeyboardShortcut {
|
|
|
232
644
|
// Each step is an array of keys pressed together. Example: steps: [['ctrl','k'], ['s']]
|
|
233
645
|
steps?: string[][];
|
|
234
646
|
macSteps?: string[][];
|
|
235
|
-
action:
|
|
647
|
+
action: Action; // Function to execute
|
|
236
648
|
description: string; // Human-readable description
|
|
237
649
|
}
|
|
238
650
|
```
|
|
@@ -247,6 +659,108 @@ interface KeyboardShortcutGroup {
|
|
|
247
659
|
}
|
|
248
660
|
```
|
|
249
661
|
|
|
662
|
+
### KeyboardShortcutDirective
|
|
663
|
+
|
|
664
|
+
A declarative directive for registering keyboard shortcuts directly in templates.
|
|
665
|
+
|
|
666
|
+
#### Selector
|
|
667
|
+
```typescript
|
|
668
|
+
[ngxKeys]
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
#### Inputs
|
|
672
|
+
|
|
673
|
+
| Input | Type | Required | Description |
|
|
674
|
+
|-------|------|----------|-------------|
|
|
675
|
+
| `keys` | `string` | No* | Comma-separated keys for single-step shortcut (e.g., `"ctrl,s"`) |
|
|
676
|
+
| `macKeys` | `string` | No | Comma-separated keys for Mac (e.g., `"cmd,s"`) |
|
|
677
|
+
| `steps` | `string[][]` | No* | Multi-step sequence. Each inner array is one step (e.g., `[['ctrl', 'k'], ['ctrl', 'd']]`) |
|
|
678
|
+
| `macSteps` | `string[][]` | No | Multi-step sequence for Mac |
|
|
679
|
+
| `description` | `string` | **Yes** | Human-readable description of what the shortcut does |
|
|
680
|
+
| `action` | `() => void` | No | Custom action to execute. If not provided, triggers click on host element |
|
|
681
|
+
| `shortcutId` | `string` | No | Custom ID for the shortcut. If not provided, generates a unique ID |
|
|
682
|
+
|
|
683
|
+
\* Either `keys`/`macKeys` OR `steps`/`macSteps` must be provided (not both)
|
|
684
|
+
|
|
685
|
+
#### Outputs
|
|
686
|
+
|
|
687
|
+
| Output | Type | Description |
|
|
688
|
+
|--------|------|-------------|
|
|
689
|
+
| `triggered` | `void` | Emitted when the keyboard shortcut is triggered (in addition to the action) |
|
|
690
|
+
|
|
691
|
+
#### Host Bindings
|
|
692
|
+
|
|
693
|
+
The directive adds the following to the host element:
|
|
694
|
+
- `data-keyboard-shortcut` attribute with the shortcut ID (useful for styling or testing)
|
|
695
|
+
|
|
696
|
+
#### Behavior
|
|
697
|
+
|
|
698
|
+
- **Registration**: Automatically registers the shortcut when the directive initializes
|
|
699
|
+
- **Default Action**: Triggers a click event on the host element (unless custom `action` is provided)
|
|
700
|
+
- **Cleanup**: Automatically unregisters the shortcut when the directive is destroyed
|
|
701
|
+
- **Error Handling**: Throws errors for invalid input combinations or registration failures
|
|
702
|
+
|
|
703
|
+
#### Usage Examples
|
|
704
|
+
|
|
705
|
+
**Basic button with click trigger:**
|
|
706
|
+
```html
|
|
707
|
+
<button
|
|
708
|
+
ngxKeys
|
|
709
|
+
keys="ctrl,s"
|
|
710
|
+
macKeys="cmd,s"
|
|
711
|
+
description="Save document"
|
|
712
|
+
(click)="save()">
|
|
713
|
+
Save
|
|
714
|
+
</button>
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
**Multi-step shortcut:**
|
|
718
|
+
```html
|
|
719
|
+
<button
|
|
720
|
+
ngxKeys
|
|
721
|
+
[steps]="[['ctrl', 'k'], ['ctrl', 'd']]"
|
|
722
|
+
[macSteps]="[['cmd', 'k'], ['cmd', 'd']]"
|
|
723
|
+
description="Format document (Ctrl+K then Ctrl+D)"
|
|
724
|
+
(click)="format()">
|
|
725
|
+
Format
|
|
726
|
+
</button>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**Custom action on non-interactive element:**
|
|
730
|
+
```html
|
|
731
|
+
<div
|
|
732
|
+
ngxKeys
|
|
733
|
+
keys="?"
|
|
734
|
+
description="Show help"
|
|
735
|
+
[action]="showHelp">
|
|
736
|
+
Press ? for help
|
|
737
|
+
</div>
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
**Listen to triggered event:**
|
|
741
|
+
```html
|
|
742
|
+
<button
|
|
743
|
+
ngxKeys
|
|
744
|
+
keys="ctrl,s"
|
|
745
|
+
description="Save document"
|
|
746
|
+
(triggered)="onShortcutTriggered()"
|
|
747
|
+
(click)="save()">
|
|
748
|
+
Save
|
|
749
|
+
</button>
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Custom shortcut ID:**
|
|
753
|
+
```html
|
|
754
|
+
<button
|
|
755
|
+
ngxKeys
|
|
756
|
+
keys="ctrl,s"
|
|
757
|
+
description="Save document"
|
|
758
|
+
shortcutId="my-custom-save-shortcut"
|
|
759
|
+
(click)="save()">
|
|
760
|
+
Save
|
|
761
|
+
</button>
|
|
762
|
+
```
|
|
763
|
+
|
|
250
764
|
## Key Mapping Reference
|
|
251
765
|
|
|
252
766
|
### Modifier Keys
|
|
@@ -483,6 +997,130 @@ export class BatchUpdateComponent {
|
|
|
483
997
|
}
|
|
484
998
|
```
|
|
485
999
|
|
|
1000
|
+
### Bulk Registration and Unregistration
|
|
1001
|
+
|
|
1002
|
+
Register or unregister multiple shortcuts efficiently:
|
|
1003
|
+
|
|
1004
|
+
```typescript
|
|
1005
|
+
import { Component, inject } from '@angular/core';
|
|
1006
|
+
import { KeyboardShortcuts } from 'ngx-keys';
|
|
1007
|
+
|
|
1008
|
+
export class MyComponent {
|
|
1009
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
1010
|
+
|
|
1011
|
+
setupToolbarShortcuts() {
|
|
1012
|
+
// Register multiple shortcuts at once
|
|
1013
|
+
this.keyboardService.registerMany([
|
|
1014
|
+
{
|
|
1015
|
+
id: 'save',
|
|
1016
|
+
keys: ['ctrl', 's'],
|
|
1017
|
+
macKeys: ['meta', 's'],
|
|
1018
|
+
action: () => this.save(),
|
|
1019
|
+
description: 'Save document'
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
id: 'save-as',
|
|
1023
|
+
keys: ['ctrl', 'shift', 's'],
|
|
1024
|
+
macKeys: ['meta', 'shift', 's'],
|
|
1025
|
+
action: () => this.saveAs(),
|
|
1026
|
+
description: 'Save as...'
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
id: 'print',
|
|
1030
|
+
keys: ['ctrl', 'p'],
|
|
1031
|
+
macKeys: ['meta', 'p'],
|
|
1032
|
+
action: () => this.print(),
|
|
1033
|
+
description: 'Print document'
|
|
1034
|
+
}
|
|
1035
|
+
]);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
cleanup() {
|
|
1039
|
+
// Unregister multiple shortcuts at once
|
|
1040
|
+
this.keyboardService.unregisterMany(['save', 'save-as', 'print']);
|
|
1041
|
+
|
|
1042
|
+
// Or unregister entire groups
|
|
1043
|
+
this.keyboardService.unregisterGroups(['toolbar', 'menu']);
|
|
1044
|
+
|
|
1045
|
+
// Or clear everything
|
|
1046
|
+
this.keyboardService.clearAll();
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private save() { /* implementation */ }
|
|
1050
|
+
private saveAs() { /* implementation */ }
|
|
1051
|
+
private print() { /* implementation */ }
|
|
1052
|
+
}
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
### Managing Group Shortcuts
|
|
1056
|
+
|
|
1057
|
+
Remove individual shortcuts from groups and query group contents:
|
|
1058
|
+
|
|
1059
|
+
```typescript
|
|
1060
|
+
import { Component, inject } from '@angular/core';
|
|
1061
|
+
import { KeyboardShortcuts } from 'ngx-keys';
|
|
1062
|
+
|
|
1063
|
+
export class MyComponent {
|
|
1064
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
1065
|
+
|
|
1066
|
+
manageEditorGroup() {
|
|
1067
|
+
// Check if a shortcut is registered
|
|
1068
|
+
if (this.keyboardService.isRegistered('bold')) {
|
|
1069
|
+
// Remove a shortcut - automatically removes it from all groups
|
|
1070
|
+
this.keyboardService.unregister('bold');
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Remove multiple shortcuts (automatically removes from groups)
|
|
1074
|
+
this.keyboardService.unregisterMany(['bold', 'italic', 'underline']);
|
|
1075
|
+
|
|
1076
|
+
// Get all shortcuts in a group
|
|
1077
|
+
const editorShortcuts = this.keyboardService.getGroupShortcuts('editor');
|
|
1078
|
+
console.log('Remaining editor shortcuts:', editorShortcuts);
|
|
1079
|
+
|
|
1080
|
+
// Or remove the entire group and all its shortcuts
|
|
1081
|
+
this.keyboardService.unregisterGroup('editor');
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
### Filter Management
|
|
1087
|
+
|
|
1088
|
+
Remove filters when no longer needed:
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
import { Component, inject } from '@angular/core';
|
|
1092
|
+
import { KeyboardShortcuts } from 'ngx-keys';
|
|
1093
|
+
|
|
1094
|
+
export class MyComponent {
|
|
1095
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
1096
|
+
|
|
1097
|
+
setupModalFilters() {
|
|
1098
|
+
// Add filters for modal context
|
|
1099
|
+
this.keyboardService.addGroupFilter('navigation', () => this.isModalOpen);
|
|
1100
|
+
this.keyboardService.addShortcutFilter('global-search', () => this.isModalOpen);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
cleanupModalFilters() {
|
|
1104
|
+
// Check if filters exist
|
|
1105
|
+
if (this.keyboardService.hasGroupFilter('navigation')) {
|
|
1106
|
+
// Remove specific group filter
|
|
1107
|
+
this.keyboardService.removeGroupFilter('navigation');
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (this.keyboardService.hasShortcutFilter('global-search')) {
|
|
1111
|
+
// Remove specific shortcut filter
|
|
1112
|
+
this.keyboardService.removeShortcutFilter('global-search');
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Or clear all filters at once
|
|
1116
|
+
this.keyboardService.clearAllGroupFilters();
|
|
1117
|
+
this.keyboardService.clearAllShortcutFilters();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private isModalOpen = false;
|
|
1121
|
+
}
|
|
1122
|
+
```
|
|
1123
|
+
|
|
486
1124
|
### Checking Status
|
|
487
1125
|
|
|
488
1126
|
> [!NOTE]
|
|
@@ -527,6 +1165,157 @@ this.keyboardService.register({
|
|
|
527
1165
|
});
|
|
528
1166
|
```
|
|
529
1167
|
|
|
1168
|
+
### Event Filtering
|
|
1169
|
+
|
|
1170
|
+
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.
|
|
1171
|
+
|
|
1172
|
+
> [!NOTE]
|
|
1173
|
+
> **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!
|
|
1174
|
+
|
|
1175
|
+
#### Named filters (recommended)
|
|
1176
|
+
|
|
1177
|
+
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.
|
|
1178
|
+
|
|
1179
|
+
```typescript
|
|
1180
|
+
// Add named filters
|
|
1181
|
+
keyboardService.addFilter('forms', (event) => {
|
|
1182
|
+
const t = event.target as HTMLElement | null;
|
|
1183
|
+
const tag = t?.tagName?.toLowerCase();
|
|
1184
|
+
return !(['input', 'textarea', 'select'].includes(tag ?? '')) && !t?.isContentEditable;
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
keyboardService.addFilter('modal-scope', (event) => {
|
|
1188
|
+
const t = event.target as HTMLElement | null;
|
|
1189
|
+
return !!t?.closest('.modal');
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// Remove/toggle when context changes
|
|
1193
|
+
keyboardService.removeFilter('modal-scope');
|
|
1194
|
+
|
|
1195
|
+
// Inspect and manage
|
|
1196
|
+
keyboardService.getFilterNames(); // ['forms']
|
|
1197
|
+
keyboardService.clearFilters(); // remove all
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
```typescript
|
|
1201
|
+
import { Component, inject } from '@angular/core';
|
|
1202
|
+
import { KeyboardShortcuts, KeyboardShortcutFilter } from 'ngx-keys';
|
|
1203
|
+
|
|
1204
|
+
export class FilterExampleComponent {
|
|
1205
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
1206
|
+
|
|
1207
|
+
constructor() {
|
|
1208
|
+
// Set up shortcuts
|
|
1209
|
+
this.keyboardService.register({
|
|
1210
|
+
id: 'save',
|
|
1211
|
+
keys: ['ctrl', 's'],
|
|
1212
|
+
macKeys: ['meta', 's'],
|
|
1213
|
+
action: () => this.save(),
|
|
1214
|
+
description: 'Save document'
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// Configure filtering to ignore form elements
|
|
1218
|
+
this.setupInputFiltering();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
private setupInputFiltering() {
|
|
1222
|
+
const inputFilter: KeyboardShortcutFilter = (event) => {
|
|
1223
|
+
const target = event.target as HTMLElement;
|
|
1224
|
+
const tagName = target?.tagName?.toLowerCase();
|
|
1225
|
+
return !['input', 'textarea', 'select'].includes(tagName) && !target?.isContentEditable;
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
// Use named filter for toggling
|
|
1229
|
+
this.keyboardService.addFilter('forms', inputFilter);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
private save() {
|
|
1233
|
+
console.log('Document saved!');
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
#### Common Filter Patterns
|
|
1239
|
+
|
|
1240
|
+
**Ignore form elements:**
|
|
1241
|
+
```typescript
|
|
1242
|
+
const formFilter: KeyboardShortcutFilter = (event) => {
|
|
1243
|
+
const target = event.target as HTMLElement;
|
|
1244
|
+
const tagName = target?.tagName?.toLowerCase();
|
|
1245
|
+
return !['input', 'textarea', 'select'].includes(tagName) && !target?.isContentEditable;
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
keyboardService.addFilter('forms', formFilter);
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
**Ignore elements with specific attributes:**
|
|
1252
|
+
```typescript
|
|
1253
|
+
const attributeFilter: KeyboardShortcutFilter = (event) => {
|
|
1254
|
+
const target = event.target as HTMLElement;
|
|
1255
|
+
return !target?.hasAttribute('data-no-shortcuts');
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
keyboardService.addFilter('no-shortcuts-attr', attributeFilter);
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
**Complex conditional filtering:**
|
|
1262
|
+
```typescript
|
|
1263
|
+
const conditionalFilter: KeyboardShortcutFilter = (event) => {
|
|
1264
|
+
const target = event.target as HTMLElement;
|
|
1265
|
+
|
|
1266
|
+
// Allow shortcuts in code editors (even though they're contentEditable)
|
|
1267
|
+
if (target?.classList?.contains('code-editor')) {
|
|
1268
|
+
return true;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Block shortcuts in form elements
|
|
1272
|
+
if (target?.tagName?.match(/INPUT|TEXTAREA|SELECT/i) || target?.isContentEditable) {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return true;
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
keyboardService.addFilter('conditional', conditionalFilter);
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
**Remove filtering:**
|
|
1283
|
+
```typescript
|
|
1284
|
+
// Remove a specific named filter
|
|
1285
|
+
keyboardService.removeFilter('forms');
|
|
1286
|
+
// Or remove all
|
|
1287
|
+
keyboardService.clearFilters();
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
#### Example: Modal Context Filtering
|
|
1291
|
+
|
|
1292
|
+
```typescript
|
|
1293
|
+
export class ModalComponent {
|
|
1294
|
+
constructor() {
|
|
1295
|
+
// When modal opens, only allow modal-specific shortcuts
|
|
1296
|
+
this.keyboardService.addFilter('modal-scope', (event) => {
|
|
1297
|
+
const target = event.target as HTMLElement;
|
|
1298
|
+
|
|
1299
|
+
// Only process events within the modal
|
|
1300
|
+
return target?.closest('.modal') !== null;
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
onClose() {
|
|
1305
|
+
// Restore normal filtering when modal closes
|
|
1306
|
+
this.keyboardService.removeFilter('modal-scope');
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
#### Performance tips
|
|
1312
|
+
|
|
1313
|
+
- Filters are evaluated once per keydown before scanning shortcuts. If any global filter returns false, ngx-keys exits early and clears pending sequences.
|
|
1314
|
+
- Group-level filters are precomputed once per event; shortcuts in blocked groups are skipped without key matching.
|
|
1315
|
+
- Keep filters cheap and synchronous. Prefer reading event.target properties (tagName, isContentEditable, classList) over layout-triggering queries.
|
|
1316
|
+
- Use named filters to toggle contexts (modals, editors) without allocating new closures per interaction.
|
|
1317
|
+
- Avoid complex DOM traversals inside filters; if needed, memoize simple queries or use attributes (e.g., data-no-shortcuts).
|
|
1318
|
+
|
|
530
1319
|
## Building
|
|
531
1320
|
|
|
532
1321
|
To build the library:
|
|
@@ -555,4 +1344,4 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
555
1344
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
556
1345
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
557
1346
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
558
|
-
5. Open a Pull Request
|
|
1347
|
+
5. Open a Pull Request
|