ngx-keys 1.0.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 +382 -0
- package/fesm2022/ngx-keys.mjs +495 -0
- package/fesm2022/ngx-keys.mjs.map +1 -0
- package/index.d.ts +199 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# ngx-keys
|
|
2
|
+
|
|
3
|
+
A lightweight, reactive Angular service for managing keyboard shortcuts with signals-based UI integration.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **🎯 Service-Focused**: Clean, focused API without unnecessary UI components
|
|
8
|
+
- **⚡ Reactive Signals**: Track active/inactive shortcuts with Angular signals
|
|
9
|
+
- **🔧 UI-Agnostic**: Build your own UI using the provided reactive signals
|
|
10
|
+
- **🌍 Cross-Platform**: Automatic Mac/PC key display formatting
|
|
11
|
+
- **🔄 Dynamic Management**: Add, remove, activate/deactivate shortcuts at runtime
|
|
12
|
+
- **📁 Group Management**: Organize shortcuts into logical groups
|
|
13
|
+
- **🪶 Lightweight**: Zero dependencies, minimal bundle impact
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install ngx-keys
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
### Displaying Shortcuts
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Component, inject } from '@angular/core';
|
|
26
|
+
import { KeyboardShortcuts } from 'ngx-keys';
|
|
27
|
+
|
|
28
|
+
@Component({
|
|
29
|
+
template: `
|
|
30
|
+
@for (shortcut of activeShortcuts(); track shortcut.id) {
|
|
31
|
+
<div>
|
|
32
|
+
<kbd>{{ shortcut.keys }}</kbd> - {{ shortcut.description }}
|
|
33
|
+
</div>
|
|
34
|
+
}
|
|
35
|
+
`
|
|
36
|
+
})
|
|
37
|
+
export class ShortcutsComponent {
|
|
38
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
39
|
+
protected readonly activeShortcuts = () => this.keyboardService.shortcutsUI$().active;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
## Key Concepts
|
|
43
|
+
|
|
44
|
+
### Automatic Activation
|
|
45
|
+
|
|
46
|
+
> [!IMPORTANT]
|
|
47
|
+
> When you register shortcuts using `register()` or `registerGroup()`, they are **automatically activated** and ready to use immediately. You don't need to call `activate()` unless you've previously deactivated them.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// This shortcut is immediately active after registration
|
|
51
|
+
this.keyboardService.register({
|
|
52
|
+
id: 'save',
|
|
53
|
+
keys: ['ctrl', 's'],
|
|
54
|
+
macKeys: ['meta', 's'],
|
|
55
|
+
action: () => this.save(),
|
|
56
|
+
description: 'Save document'
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use the `activate()` and `deactivate()` methods for dynamic control after registration:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Temporarily disable a shortcut
|
|
64
|
+
this.keyboardService.deactivate('save');
|
|
65
|
+
|
|
66
|
+
// Re-enable it later
|
|
67
|
+
this.keyboardService.activate('save');
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API Reference
|
|
71
|
+
|
|
72
|
+
### KeyboardShortcuts Service
|
|
73
|
+
|
|
74
|
+
#### Methods
|
|
75
|
+
|
|
76
|
+
**Registration Methods:**
|
|
77
|
+
- `register(shortcut: KeyboardShortcut)` - Register and automatically activate a single shortcut *Throws error on conflicts*
|
|
78
|
+
- `registerGroup(groupId: string, shortcuts: KeyboardShortcut[])` - Register and automatically activate a group of shortcuts *Throws error on conflicts*
|
|
79
|
+
|
|
80
|
+
**Management Methods:**
|
|
81
|
+
- `unregister(shortcutId: string)` - Remove a shortcut *Throws error if not found*
|
|
82
|
+
- `unregisterGroup(groupId: string)` - Remove a group *Throws error if not found*
|
|
83
|
+
- `activate(shortcutId: string)` - Activate a shortcut *Throws error if not registered*
|
|
84
|
+
- `deactivate(shortcutId: string)` - Deactivate a shortcut *Throws error if not registered*
|
|
85
|
+
- `activateGroup(groupId: string)` - Activate all shortcuts in a group *Throws error if not found*
|
|
86
|
+
- `deactivateGroup(groupId: string)` - Deactivate all shortcuts in a group *Throws error if not found*
|
|
87
|
+
|
|
88
|
+
**Query Methods:**
|
|
89
|
+
- `isActive(shortcutId: string): boolean` - Check if a shortcut is active
|
|
90
|
+
- `isRegistered(shortcutId: string): boolean` - Check if a shortcut is registered
|
|
91
|
+
- `isGroupActive(groupId: string): boolean` - Check if a group is active
|
|
92
|
+
- `isGroupRegistered(groupId: string): boolean` - Check if a group is registered
|
|
93
|
+
- `getShortcuts(): ReadonlyMap<string, KeyboardShortcut>` - Get all registered shortcuts
|
|
94
|
+
- `getGroups(): ReadonlyMap<string, KeyboardShortcutGroup>` - Get all registered groups
|
|
95
|
+
|
|
96
|
+
**Utility Methods:**
|
|
97
|
+
- `formatShortcutForUI(shortcut: KeyboardShortcut): KeyboardShortcutUI` - Format a shortcut for display
|
|
98
|
+
- `batchUpdate(operations: () => void): void` - Batch multiple operations to reduce signal updates
|
|
99
|
+
|
|
100
|
+
#### Reactive Signals
|
|
101
|
+
|
|
102
|
+
The service provides reactive signals for UI integration:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Primary signal containing all shortcut state
|
|
106
|
+
shortcuts$: Signal<{
|
|
107
|
+
active: KeyboardShortcut[];
|
|
108
|
+
inactive: KeyboardShortcut[];
|
|
109
|
+
all: KeyboardShortcut[];
|
|
110
|
+
groups: {
|
|
111
|
+
active: string[];
|
|
112
|
+
inactive: string[];
|
|
113
|
+
};
|
|
114
|
+
}>
|
|
115
|
+
|
|
116
|
+
// Pre-formatted UI signal for easy display
|
|
117
|
+
shortcutsUI$: Signal<{
|
|
118
|
+
active: ShortcutUI[];
|
|
119
|
+
inactive: ShortcutUI[];
|
|
120
|
+
all: ShortcutUI[];
|
|
121
|
+
}>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**ShortcutUI Interface:**
|
|
125
|
+
```typescript
|
|
126
|
+
interface KeyboardShortcutUI {
|
|
127
|
+
id: string; // Shortcut identifier
|
|
128
|
+
keys: string; // Formatted PC/Linux keys (e.g., "Ctrl+S")
|
|
129
|
+
macKeys: string; // Formatted Mac keys (e.g., "⌘+S")
|
|
130
|
+
description: string; // Human-readable description
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### KeyboardShortcut Interface
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
interface KeyboardShortcut {
|
|
138
|
+
id: string; // Unique identifier
|
|
139
|
+
keys: string[]; // Key combination for PC/Linux (e.g., ['ctrl', 's'])
|
|
140
|
+
macKeys: string[]; // Key combination for Mac (e.g., ['meta', 's'])
|
|
141
|
+
action: () => void; // Function to execute
|
|
142
|
+
description: string; // Human-readable description
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### KeyboardShortcutGroup Interface
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
interface KeyboardShortcutGroup {
|
|
150
|
+
id: string; // Unique group identifier
|
|
151
|
+
shortcuts: KeyboardShortcut[]; // Array of shortcuts in this group
|
|
152
|
+
active: boolean; // Whether the group is currently active
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Key Mapping Reference
|
|
157
|
+
|
|
158
|
+
### Modifier Keys
|
|
159
|
+
|
|
160
|
+
| PC/Linux | Mac | Description |
|
|
161
|
+
|----------|-----|-------------|
|
|
162
|
+
| `ctrl` | `meta` | Control/Command key |
|
|
163
|
+
| `alt` | `alt` | Alt/Option key |
|
|
164
|
+
| `shift` | `shift` | Shift key |
|
|
165
|
+
|
|
166
|
+
### Special Keys
|
|
167
|
+
|
|
168
|
+
| Key | Value |
|
|
169
|
+
|-----|-------|
|
|
170
|
+
| Function keys | `f1`, `f2`, `f3`, ... `f12` |
|
|
171
|
+
| Arrow keys | `arrowup`, `arrowdown`, `arrowleft`, `arrowright` |
|
|
172
|
+
| Navigation | `home`, `end`, `pageup`, `pagedown` |
|
|
173
|
+
| Editing | `insert`, `delete`, `backspace` |
|
|
174
|
+
| Other | `escape`, `tab`, `enter`, `space` |
|
|
175
|
+
|
|
176
|
+
## Browser Conflicts Warning
|
|
177
|
+
|
|
178
|
+
> [!WARNING]
|
|
179
|
+
> Some key combinations conflict with browser defaults. Use these with caution:
|
|
180
|
+
|
|
181
|
+
### High-Risk Combinations (avoid these)
|
|
182
|
+
- `Ctrl+N` / `⌘+N` - New tab/window
|
|
183
|
+
- `Ctrl+T` / `⌘+T` - New tab
|
|
184
|
+
- `Ctrl+W` / `⌘+W` - Close tab
|
|
185
|
+
- `Ctrl+R` / `⌘+R` - Reload page
|
|
186
|
+
- `Ctrl+L` / `⌘+L` - Focus address bar
|
|
187
|
+
- `Ctrl+D` / `⌘+D` - Bookmark page
|
|
188
|
+
|
|
189
|
+
### Safer Alternatives
|
|
190
|
+
- Function keys: `F1`, `F2`, `F3`, etc.
|
|
191
|
+
- Custom combinations: `Ctrl+Shift+S`, `Alt+Enter`
|
|
192
|
+
- Arrow keys with modifiers: `Ctrl+ArrowUp`
|
|
193
|
+
- Application-specific: `Ctrl+K`, `Ctrl+P` (if not conflicting)
|
|
194
|
+
|
|
195
|
+
### Testing Browser Conflicts
|
|
196
|
+
|
|
197
|
+
> [!TIP]
|
|
198
|
+
> Always test your shortcuts across different browsers and operating systems. Consider providing alternative key combinations or allow users to customize shortcuts.
|
|
199
|
+
|
|
200
|
+
## Advanced Usage
|
|
201
|
+
|
|
202
|
+
### Reactive UI Integration
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { Component, inject } from '@angular/core';
|
|
206
|
+
import { KeyboardShortcuts } from 'ngx-keys';
|
|
207
|
+
|
|
208
|
+
@Component({
|
|
209
|
+
template: `
|
|
210
|
+
<section>
|
|
211
|
+
<h3>Active Shortcuts ({{ activeShortcuts().length }})</h3>
|
|
212
|
+
@for (shortcut of activeShortcuts(); track shortcut.id) {
|
|
213
|
+
<div>
|
|
214
|
+
<kbd>{{ shortcut.keys }}</kbd> - {{ shortcut.description }}
|
|
215
|
+
</div>
|
|
216
|
+
}
|
|
217
|
+
</section>
|
|
218
|
+
|
|
219
|
+
<section>
|
|
220
|
+
<h3>Active Groups</h3>
|
|
221
|
+
@for (groupId of activeGroups(); track groupId) {
|
|
222
|
+
<div>{{ groupId }}</div>
|
|
223
|
+
}
|
|
224
|
+
</section>
|
|
225
|
+
`
|
|
226
|
+
})
|
|
227
|
+
export class ShortcutsDisplayComponent {
|
|
228
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
229
|
+
|
|
230
|
+
// Access formatted shortcuts for display
|
|
231
|
+
protected readonly activeShortcuts = () => this.keyboardService.shortcutsUI$().active;
|
|
232
|
+
protected readonly inactiveShortcuts = () => this.keyboardService.shortcutsUI$().inactive;
|
|
233
|
+
protected readonly allShortcuts = () => this.keyboardService.shortcutsUI$().all;
|
|
234
|
+
|
|
235
|
+
// Access group information
|
|
236
|
+
protected readonly activeGroups = () => this.keyboardService.shortcuts$().groups.active;
|
|
237
|
+
protected readonly inactiveGroups = () => this.keyboardService.shortcuts$().groups.inactive;
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
### Group Management
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { Component, DestroyRef, inject } from '@angular/core';
|
|
244
|
+
import { KeyboardShortcuts, KeyboardShortcut } from 'ngx-keys';
|
|
245
|
+
|
|
246
|
+
export class FeatureComponent {
|
|
247
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
248
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
249
|
+
|
|
250
|
+
constructor() {
|
|
251
|
+
const shortcuts: KeyboardShortcut[] = [
|
|
252
|
+
{
|
|
253
|
+
id: 'cut',
|
|
254
|
+
keys: ['ctrl', 'x'],
|
|
255
|
+
macKeys: ['meta', 'x'],
|
|
256
|
+
action: () => this.cut(),
|
|
257
|
+
description: 'Cut selection'
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: 'copy',
|
|
261
|
+
keys: ['ctrl', 'c'],
|
|
262
|
+
macKeys: ['meta', 'c'],
|
|
263
|
+
action: () => this.copy(),
|
|
264
|
+
description: 'Copy selection'
|
|
265
|
+
}
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
// Group is automatically activated when registered
|
|
269
|
+
this.keyboardService.registerGroup('edit-shortcuts', shortcuts);
|
|
270
|
+
|
|
271
|
+
// Setup cleanup on destroy
|
|
272
|
+
this.destroyRef.onDestroy(() => {
|
|
273
|
+
this.keyboardService.unregisterGroup('edit-shortcuts');
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
toggleEditMode(enabled: boolean) {
|
|
278
|
+
if (enabled) {
|
|
279
|
+
this.keyboardService.activateGroup('edit-shortcuts');
|
|
280
|
+
} else {
|
|
281
|
+
this.keyboardService.deactivateGroup('edit-shortcuts');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private cut() { /* implementation */ }
|
|
286
|
+
private copy() { /* implementation */ }
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Batch Operations
|
|
291
|
+
|
|
292
|
+
For better performance when making multiple changes, use the `batchUpdate` method:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { Component, inject } from '@angular/core';
|
|
296
|
+
import { KeyboardShortcuts } from 'ngx-keys';
|
|
297
|
+
|
|
298
|
+
export class BatchUpdateComponent {
|
|
299
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
300
|
+
|
|
301
|
+
constructor() {
|
|
302
|
+
this.setupMultipleShortcuts();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private setupMultipleShortcuts() {
|
|
306
|
+
// Batch multiple operations to reduce signal updates
|
|
307
|
+
// Note: Shortcuts are automatically activated when registered
|
|
308
|
+
this.keyboardService.batchUpdate(() => {
|
|
309
|
+
this.keyboardService.register({
|
|
310
|
+
id: 'action1',
|
|
311
|
+
keys: ['ctrl', '1'],
|
|
312
|
+
macKeys: ['meta', '1'],
|
|
313
|
+
action: () => this.action1(),
|
|
314
|
+
description: 'Action 1'
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
this.keyboardService.register({
|
|
318
|
+
id: 'action2',
|
|
319
|
+
keys: ['ctrl', '2'],
|
|
320
|
+
macKeys: ['meta', '2'],
|
|
321
|
+
action: () => this.action2(),
|
|
322
|
+
description: 'Action 2'
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private action1() { /* implementation */ }
|
|
328
|
+
private action2() { /* implementation */ }
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Checking Status
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { Component, inject } from '@angular/core';
|
|
336
|
+
import { KeyboardShortcuts } from 'ngx-keys';
|
|
337
|
+
|
|
338
|
+
export class MyComponent {
|
|
339
|
+
private readonly keyboardService = inject(KeyboardShortcuts);
|
|
340
|
+
|
|
341
|
+
checkAndActivate() {
|
|
342
|
+
// Check before performing operations
|
|
343
|
+
if (this.keyboardService.isRegistered('my-shortcut')) {
|
|
344
|
+
this.keyboardService.activate('my-shortcut');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (this.keyboardService.isGroupRegistered('my-group')) {
|
|
348
|
+
this.keyboardService.activateGroup('my-group');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Building
|
|
355
|
+
|
|
356
|
+
To build the library:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
ng build ngx-keys
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Testing
|
|
363
|
+
|
|
364
|
+
To run tests:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
ng test ngx-keys
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## License
|
|
371
|
+
|
|
372
|
+
0BSD © [ngx-keys Contributors](LICENSE)
|
|
373
|
+
|
|
374
|
+
## Contributing
|
|
375
|
+
|
|
376
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
377
|
+
|
|
378
|
+
1. Fork the repository
|
|
379
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
380
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
381
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
382
|
+
5. Open a Pull Request
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, computed, inject, PLATFORM_ID, Injectable } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Centralized error messages for keyboard shortcuts service
|
|
7
|
+
* This ensures consistency across the application and makes testing easier
|
|
8
|
+
*/
|
|
9
|
+
const KeyboardShortcutsErrors = {
|
|
10
|
+
// Registration errors
|
|
11
|
+
SHORTCUT_ALREADY_REGISTERED: (id) => `Shortcut "${id}" already registered`,
|
|
12
|
+
GROUP_ALREADY_REGISTERED: (id) => `Group "${id}" already registered`,
|
|
13
|
+
KEY_CONFLICT: (conflictId) => `Key conflict with "${conflictId}"`,
|
|
14
|
+
SHORTCUT_IDS_ALREADY_REGISTERED: (ids) => `Shortcut IDs already registered: ${ids.join(', ')}`,
|
|
15
|
+
DUPLICATE_SHORTCUTS_IN_GROUP: (ids) => `Duplicate shortcuts in group: ${ids.join(', ')}`,
|
|
16
|
+
KEY_CONFLICTS_IN_GROUP: (conflicts) => `Key conflicts: ${conflicts.join(', ')}`,
|
|
17
|
+
// Operation errors
|
|
18
|
+
SHORTCUT_NOT_REGISTERED: (id) => `Shortcut "${id}" not registered`,
|
|
19
|
+
GROUP_NOT_REGISTERED: (id) => `Group "${id}" not registered`,
|
|
20
|
+
// Activation/Deactivation errors
|
|
21
|
+
CANNOT_ACTIVATE_SHORTCUT: (id) => `Cannot activate shortcut "${id}": not registered`,
|
|
22
|
+
CANNOT_DEACTIVATE_SHORTCUT: (id) => `Cannot deactivate shortcut "${id}": not registered`,
|
|
23
|
+
CANNOT_ACTIVATE_GROUP: (id) => `Cannot activate group "${id}": not registered`,
|
|
24
|
+
CANNOT_DEACTIVATE_GROUP: (id) => `Cannot deactivate group "${id}": not registered`,
|
|
25
|
+
// Unregistration errors
|
|
26
|
+
CANNOT_UNREGISTER_SHORTCUT: (id) => `Cannot unregister shortcut "${id}": not registered`,
|
|
27
|
+
CANNOT_UNREGISTER_GROUP: (id) => `Cannot unregister group "${id}": not registered`,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Custom error class for keyboard shortcuts
|
|
31
|
+
*/
|
|
32
|
+
class KeyboardShortcutError extends Error {
|
|
33
|
+
errorType;
|
|
34
|
+
context;
|
|
35
|
+
constructor(errorType, message, context) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.errorType = errorType;
|
|
38
|
+
this.context = context;
|
|
39
|
+
this.name = 'KeyboardShortcutError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Error factory for creating consistent errors
|
|
44
|
+
*/
|
|
45
|
+
class KeyboardShortcutsErrorFactory {
|
|
46
|
+
static shortcutAlreadyRegistered(id) {
|
|
47
|
+
return new KeyboardShortcutError('SHORTCUT_ALREADY_REGISTERED', KeyboardShortcutsErrors.SHORTCUT_ALREADY_REGISTERED(id), { shortcutId: id });
|
|
48
|
+
}
|
|
49
|
+
static groupAlreadyRegistered(id) {
|
|
50
|
+
return new KeyboardShortcutError('GROUP_ALREADY_REGISTERED', KeyboardShortcutsErrors.GROUP_ALREADY_REGISTERED(id), { groupId: id });
|
|
51
|
+
}
|
|
52
|
+
static keyConflict(conflictId) {
|
|
53
|
+
return new KeyboardShortcutError('KEY_CONFLICT', KeyboardShortcutsErrors.KEY_CONFLICT(conflictId), { conflictId });
|
|
54
|
+
}
|
|
55
|
+
static shortcutIdsAlreadyRegistered(ids) {
|
|
56
|
+
return new KeyboardShortcutError('SHORTCUT_IDS_ALREADY_REGISTERED', KeyboardShortcutsErrors.SHORTCUT_IDS_ALREADY_REGISTERED(ids), { duplicateIds: ids });
|
|
57
|
+
}
|
|
58
|
+
static duplicateShortcutsInGroup(ids) {
|
|
59
|
+
return new KeyboardShortcutError('DUPLICATE_SHORTCUTS_IN_GROUP', KeyboardShortcutsErrors.DUPLICATE_SHORTCUTS_IN_GROUP(ids), { duplicateIds: ids });
|
|
60
|
+
}
|
|
61
|
+
static keyConflictsInGroup(conflicts) {
|
|
62
|
+
return new KeyboardShortcutError('KEY_CONFLICTS_IN_GROUP', KeyboardShortcutsErrors.KEY_CONFLICTS_IN_GROUP(conflicts), { conflicts });
|
|
63
|
+
}
|
|
64
|
+
static shortcutNotRegistered(id) {
|
|
65
|
+
return new KeyboardShortcutError('SHORTCUT_NOT_REGISTERED', KeyboardShortcutsErrors.SHORTCUT_NOT_REGISTERED(id), { shortcutId: id });
|
|
66
|
+
}
|
|
67
|
+
static groupNotRegistered(id) {
|
|
68
|
+
return new KeyboardShortcutError('GROUP_NOT_REGISTERED', KeyboardShortcutsErrors.GROUP_NOT_REGISTERED(id), { groupId: id });
|
|
69
|
+
}
|
|
70
|
+
static cannotActivateShortcut(id) {
|
|
71
|
+
return new KeyboardShortcutError('CANNOT_ACTIVATE_SHORTCUT', KeyboardShortcutsErrors.CANNOT_ACTIVATE_SHORTCUT(id), { shortcutId: id });
|
|
72
|
+
}
|
|
73
|
+
static cannotDeactivateShortcut(id) {
|
|
74
|
+
return new KeyboardShortcutError('CANNOT_DEACTIVATE_SHORTCUT', KeyboardShortcutsErrors.CANNOT_DEACTIVATE_SHORTCUT(id), { shortcutId: id });
|
|
75
|
+
}
|
|
76
|
+
static cannotActivateGroup(id) {
|
|
77
|
+
return new KeyboardShortcutError('CANNOT_ACTIVATE_GROUP', KeyboardShortcutsErrors.CANNOT_ACTIVATE_GROUP(id), { groupId: id });
|
|
78
|
+
}
|
|
79
|
+
static cannotDeactivateGroup(id) {
|
|
80
|
+
return new KeyboardShortcutError('CANNOT_DEACTIVATE_GROUP', KeyboardShortcutsErrors.CANNOT_DEACTIVATE_GROUP(id), { groupId: id });
|
|
81
|
+
}
|
|
82
|
+
static cannotUnregisterShortcut(id) {
|
|
83
|
+
return new KeyboardShortcutError('CANNOT_UNREGISTER_SHORTCUT', KeyboardShortcutsErrors.CANNOT_UNREGISTER_SHORTCUT(id), { shortcutId: id });
|
|
84
|
+
}
|
|
85
|
+
static cannotUnregisterGroup(id) {
|
|
86
|
+
return new KeyboardShortcutError('CANNOT_UNREGISTER_GROUP', KeyboardShortcutsErrors.CANNOT_UNREGISTER_GROUP(id), { groupId: id });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class KeyboardShortcuts {
|
|
91
|
+
shortcuts = new Map();
|
|
92
|
+
groups = new Map();
|
|
93
|
+
activeShortcuts = new Set();
|
|
94
|
+
activeGroups = new Set();
|
|
95
|
+
// Single consolidated state signal - reduces memory overhead
|
|
96
|
+
state = signal({
|
|
97
|
+
shortcuts: new Map(),
|
|
98
|
+
groups: new Map(),
|
|
99
|
+
activeShortcuts: new Set(),
|
|
100
|
+
activeGroups: new Set(),
|
|
101
|
+
version: 0 // for change detection optimization
|
|
102
|
+
}, ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
103
|
+
// Primary computed signal - consumers derive what they need from this
|
|
104
|
+
shortcuts$ = computed(() => {
|
|
105
|
+
const state = this.state();
|
|
106
|
+
const activeShortcuts = Array.from(state.activeShortcuts)
|
|
107
|
+
.map(id => state.shortcuts.get(id))
|
|
108
|
+
.filter((s) => s !== undefined);
|
|
109
|
+
const inactiveShortcuts = Array.from(state.shortcuts.values())
|
|
110
|
+
.filter(s => !state.activeShortcuts.has(s.id));
|
|
111
|
+
return {
|
|
112
|
+
active: activeShortcuts,
|
|
113
|
+
inactive: inactiveShortcuts,
|
|
114
|
+
all: Array.from(state.shortcuts.values()),
|
|
115
|
+
groups: {
|
|
116
|
+
active: Array.from(state.activeGroups),
|
|
117
|
+
inactive: Array.from(state.groups.keys())
|
|
118
|
+
.filter(id => !state.activeGroups.has(id))
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}, ...(ngDevMode ? [{ debugName: "shortcuts$" }] : []));
|
|
122
|
+
// Optional: Pre-formatted UI signal for components that need it
|
|
123
|
+
shortcutsUI$ = computed(() => {
|
|
124
|
+
const shortcuts = this.shortcuts$();
|
|
125
|
+
return {
|
|
126
|
+
active: shortcuts.active.map(s => this.formatShortcutForUI(s)),
|
|
127
|
+
inactive: shortcuts.inactive.map(s => this.formatShortcutForUI(s)),
|
|
128
|
+
all: shortcuts.all.map(s => this.formatShortcutForUI(s))
|
|
129
|
+
};
|
|
130
|
+
}, ...(ngDevMode ? [{ debugName: "shortcutsUI$" }] : []));
|
|
131
|
+
keydownListener = this.handleKeydown.bind(this);
|
|
132
|
+
isListening = false;
|
|
133
|
+
isBrowser;
|
|
134
|
+
constructor() {
|
|
135
|
+
// Use try-catch to handle injection context for better testability
|
|
136
|
+
try {
|
|
137
|
+
const platformId = inject(PLATFORM_ID);
|
|
138
|
+
this.isBrowser = isPlatformBrowser(platformId);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Fallback for testing - assume browser environment
|
|
142
|
+
this.isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
143
|
+
}
|
|
144
|
+
if (this.isBrowser) {
|
|
145
|
+
this.startListening();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
ngOnDestroy() {
|
|
149
|
+
this.stopListening();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Batch updates and increment version for change detection
|
|
153
|
+
*/
|
|
154
|
+
updateState() {
|
|
155
|
+
this.state.update(current => ({
|
|
156
|
+
shortcuts: new Map(this.shortcuts),
|
|
157
|
+
groups: new Map(this.groups),
|
|
158
|
+
activeShortcuts: new Set(this.activeShortcuts),
|
|
159
|
+
activeGroups: new Set(this.activeGroups),
|
|
160
|
+
version: current.version + 1
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Utility method for UI formatting
|
|
165
|
+
*/
|
|
166
|
+
formatShortcutForUI(shortcut) {
|
|
167
|
+
return {
|
|
168
|
+
id: shortcut.id,
|
|
169
|
+
keys: this.formatKeysForDisplay(shortcut.keys, false),
|
|
170
|
+
macKeys: this.formatKeysForDisplay(shortcut.macKeys, true),
|
|
171
|
+
description: shortcut.description
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Utility method for batch operations - reduces signal updates
|
|
176
|
+
*/
|
|
177
|
+
batchUpdate(operations) {
|
|
178
|
+
operations();
|
|
179
|
+
this.updateState();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Format keys for display with proper Unicode symbols
|
|
183
|
+
*/
|
|
184
|
+
formatKeysForDisplay(keys, isMac = false) {
|
|
185
|
+
const keyMap = isMac ? {
|
|
186
|
+
'ctrl': '⌃',
|
|
187
|
+
'alt': '⌥',
|
|
188
|
+
'shift': '⇧',
|
|
189
|
+
'meta': '⌘',
|
|
190
|
+
'cmd': '⌘',
|
|
191
|
+
'command': '⌘'
|
|
192
|
+
} : {
|
|
193
|
+
'ctrl': 'Ctrl',
|
|
194
|
+
'alt': 'Alt',
|
|
195
|
+
'shift': 'Shift',
|
|
196
|
+
'meta': 'Win'
|
|
197
|
+
};
|
|
198
|
+
return keys
|
|
199
|
+
.map(key => keyMap[key.toLowerCase()] || key.toUpperCase())
|
|
200
|
+
.join('+');
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Check if a key combination is already registered
|
|
204
|
+
* @returns The ID of the conflicting shortcut, or null if no conflict
|
|
205
|
+
*/
|
|
206
|
+
findConflict(newShortcut) {
|
|
207
|
+
for (const existing of this.shortcuts.values()) {
|
|
208
|
+
if (this.keysMatch(newShortcut.keys, existing.keys)) {
|
|
209
|
+
return existing.id;
|
|
210
|
+
}
|
|
211
|
+
if (this.keysMatch(newShortcut.macKeys, existing.macKeys)) {
|
|
212
|
+
return existing.id;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Register a single keyboard shortcut
|
|
219
|
+
* @throws KeyboardShortcutError if shortcut ID is already registered or key combination is in use
|
|
220
|
+
*/
|
|
221
|
+
register(shortcut) {
|
|
222
|
+
if (this.shortcuts.has(shortcut.id)) {
|
|
223
|
+
throw KeyboardShortcutsErrorFactory.shortcutAlreadyRegistered(shortcut.id);
|
|
224
|
+
}
|
|
225
|
+
const conflictId = this.findConflict(shortcut);
|
|
226
|
+
if (conflictId) {
|
|
227
|
+
throw KeyboardShortcutsErrorFactory.keyConflict(conflictId);
|
|
228
|
+
}
|
|
229
|
+
this.shortcuts.set(shortcut.id, shortcut);
|
|
230
|
+
this.activeShortcuts.add(shortcut.id);
|
|
231
|
+
this.updateState();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Register multiple keyboard shortcuts as a group
|
|
235
|
+
* @throws KeyboardShortcutError if group ID is already registered or if any shortcut ID or key combination conflicts
|
|
236
|
+
*/
|
|
237
|
+
registerGroup(groupId, shortcuts) {
|
|
238
|
+
// Check if group ID already exists
|
|
239
|
+
if (this.groups.has(groupId)) {
|
|
240
|
+
throw KeyboardShortcutsErrorFactory.groupAlreadyRegistered(groupId);
|
|
241
|
+
}
|
|
242
|
+
// Check for duplicate shortcut IDs and key combination conflicts
|
|
243
|
+
const duplicateIds = [];
|
|
244
|
+
const keyConflicts = [];
|
|
245
|
+
shortcuts.forEach(shortcut => {
|
|
246
|
+
if (this.shortcuts.has(shortcut.id)) {
|
|
247
|
+
duplicateIds.push(shortcut.id);
|
|
248
|
+
}
|
|
249
|
+
const conflictId = this.findConflict(shortcut);
|
|
250
|
+
if (conflictId) {
|
|
251
|
+
keyConflicts.push(`"${shortcut.id}" conflicts with "${conflictId}"`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
if (duplicateIds.length > 0) {
|
|
255
|
+
throw KeyboardShortcutsErrorFactory.shortcutIdsAlreadyRegistered(duplicateIds);
|
|
256
|
+
}
|
|
257
|
+
if (keyConflicts.length > 0) {
|
|
258
|
+
throw KeyboardShortcutsErrorFactory.keyConflictsInGroup(keyConflicts);
|
|
259
|
+
}
|
|
260
|
+
// Validate that all shortcuts have unique IDs within the group
|
|
261
|
+
const groupIds = new Set();
|
|
262
|
+
const duplicatesInGroup = [];
|
|
263
|
+
shortcuts.forEach(shortcut => {
|
|
264
|
+
if (groupIds.has(shortcut.id)) {
|
|
265
|
+
duplicatesInGroup.push(shortcut.id);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
groupIds.add(shortcut.id);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
if (duplicatesInGroup.length > 0) {
|
|
272
|
+
throw KeyboardShortcutsErrorFactory.duplicateShortcutsInGroup(duplicatesInGroup);
|
|
273
|
+
}
|
|
274
|
+
// Use batch update to reduce signal updates
|
|
275
|
+
this.batchUpdate(() => {
|
|
276
|
+
const group = {
|
|
277
|
+
id: groupId,
|
|
278
|
+
shortcuts,
|
|
279
|
+
active: true
|
|
280
|
+
};
|
|
281
|
+
this.groups.set(groupId, group);
|
|
282
|
+
this.activeGroups.add(groupId);
|
|
283
|
+
// Register individual shortcuts
|
|
284
|
+
shortcuts.forEach(shortcut => {
|
|
285
|
+
this.shortcuts.set(shortcut.id, shortcut);
|
|
286
|
+
this.activeShortcuts.add(shortcut.id);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Unregister a single keyboard shortcut
|
|
292
|
+
* @throws KeyboardShortcutError if shortcut ID doesn't exist
|
|
293
|
+
*/
|
|
294
|
+
unregister(shortcutId) {
|
|
295
|
+
if (!this.shortcuts.has(shortcutId)) {
|
|
296
|
+
throw KeyboardShortcutsErrorFactory.cannotUnregisterShortcut(shortcutId);
|
|
297
|
+
}
|
|
298
|
+
this.shortcuts.delete(shortcutId);
|
|
299
|
+
this.activeShortcuts.delete(shortcutId);
|
|
300
|
+
this.updateState();
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Unregister a group of keyboard shortcuts
|
|
304
|
+
* @throws KeyboardShortcutError if group ID doesn't exist
|
|
305
|
+
*/
|
|
306
|
+
unregisterGroup(groupId) {
|
|
307
|
+
const group = this.groups.get(groupId);
|
|
308
|
+
if (!group) {
|
|
309
|
+
throw KeyboardShortcutsErrorFactory.cannotUnregisterGroup(groupId);
|
|
310
|
+
}
|
|
311
|
+
this.batchUpdate(() => {
|
|
312
|
+
group.shortcuts.forEach(shortcut => {
|
|
313
|
+
this.shortcuts.delete(shortcut.id);
|
|
314
|
+
this.activeShortcuts.delete(shortcut.id);
|
|
315
|
+
});
|
|
316
|
+
this.groups.delete(groupId);
|
|
317
|
+
this.activeGroups.delete(groupId);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Activate a single keyboard shortcut
|
|
322
|
+
* @throws KeyboardShortcutError if shortcut ID doesn't exist
|
|
323
|
+
*/
|
|
324
|
+
activate(shortcutId) {
|
|
325
|
+
if (!this.shortcuts.has(shortcutId)) {
|
|
326
|
+
throw KeyboardShortcutsErrorFactory.cannotActivateShortcut(shortcutId);
|
|
327
|
+
}
|
|
328
|
+
this.activeShortcuts.add(shortcutId);
|
|
329
|
+
this.updateState();
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Deactivate a single keyboard shortcut
|
|
333
|
+
* @throws KeyboardShortcutError if shortcut ID doesn't exist
|
|
334
|
+
*/
|
|
335
|
+
deactivate(shortcutId) {
|
|
336
|
+
if (!this.shortcuts.has(shortcutId)) {
|
|
337
|
+
throw KeyboardShortcutsErrorFactory.cannotDeactivateShortcut(shortcutId);
|
|
338
|
+
}
|
|
339
|
+
this.activeShortcuts.delete(shortcutId);
|
|
340
|
+
this.updateState();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Activate a group of keyboard shortcuts
|
|
344
|
+
* @throws KeyboardShortcutError if group ID doesn't exist
|
|
345
|
+
*/
|
|
346
|
+
activateGroup(groupId) {
|
|
347
|
+
const group = this.groups.get(groupId);
|
|
348
|
+
if (!group) {
|
|
349
|
+
throw KeyboardShortcutsErrorFactory.cannotActivateGroup(groupId);
|
|
350
|
+
}
|
|
351
|
+
this.batchUpdate(() => {
|
|
352
|
+
group.active = true;
|
|
353
|
+
this.activeGroups.add(groupId);
|
|
354
|
+
group.shortcuts.forEach(shortcut => {
|
|
355
|
+
this.activeShortcuts.add(shortcut.id);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Deactivate a group of keyboard shortcuts
|
|
361
|
+
* @throws KeyboardShortcutError if group ID doesn't exist
|
|
362
|
+
*/
|
|
363
|
+
deactivateGroup(groupId) {
|
|
364
|
+
const group = this.groups.get(groupId);
|
|
365
|
+
if (!group) {
|
|
366
|
+
throw KeyboardShortcutsErrorFactory.cannotDeactivateGroup(groupId);
|
|
367
|
+
}
|
|
368
|
+
this.batchUpdate(() => {
|
|
369
|
+
group.active = false;
|
|
370
|
+
this.activeGroups.delete(groupId);
|
|
371
|
+
group.shortcuts.forEach(shortcut => {
|
|
372
|
+
this.activeShortcuts.delete(shortcut.id);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Check if a shortcut is active
|
|
378
|
+
*/
|
|
379
|
+
isActive(shortcutId) {
|
|
380
|
+
return this.activeShortcuts.has(shortcutId);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Check if a shortcut is registered
|
|
384
|
+
*/
|
|
385
|
+
isRegistered(shortcutId) {
|
|
386
|
+
return this.shortcuts.has(shortcutId);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Check if a group is active
|
|
390
|
+
*/
|
|
391
|
+
isGroupActive(groupId) {
|
|
392
|
+
return this.activeGroups.has(groupId);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Check if a group is registered
|
|
396
|
+
*/
|
|
397
|
+
isGroupRegistered(groupId) {
|
|
398
|
+
return this.groups.has(groupId);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Get all registered shortcuts
|
|
402
|
+
*/
|
|
403
|
+
getShortcuts() {
|
|
404
|
+
return this.shortcuts;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get all registered groups
|
|
408
|
+
*/
|
|
409
|
+
getGroups() {
|
|
410
|
+
return this.groups;
|
|
411
|
+
}
|
|
412
|
+
startListening() {
|
|
413
|
+
if (!this.isBrowser || this.isListening) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
document.addEventListener('keydown', this.keydownListener, { passive: false });
|
|
417
|
+
this.isListening = true;
|
|
418
|
+
}
|
|
419
|
+
stopListening() {
|
|
420
|
+
if (!this.isBrowser || !this.isListening) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
document.removeEventListener('keydown', this.keydownListener);
|
|
424
|
+
this.isListening = false;
|
|
425
|
+
}
|
|
426
|
+
handleKeydown(event) {
|
|
427
|
+
const pressedKeys = this.getPressedKeys(event);
|
|
428
|
+
const isMac = this.isMacPlatform();
|
|
429
|
+
for (const shortcutId of this.activeShortcuts) {
|
|
430
|
+
const shortcut = this.shortcuts.get(shortcutId);
|
|
431
|
+
if (!shortcut)
|
|
432
|
+
continue;
|
|
433
|
+
const targetKeys = isMac ? shortcut.macKeys : shortcut.keys;
|
|
434
|
+
if (this.keysMatch(pressedKeys, targetKeys)) {
|
|
435
|
+
event.preventDefault();
|
|
436
|
+
event.stopPropagation();
|
|
437
|
+
try {
|
|
438
|
+
shortcut.action();
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
console.error(`Error executing keyboard shortcut "${shortcut.id}":`, error);
|
|
442
|
+
}
|
|
443
|
+
break; // Only execute the first matching shortcut
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
getPressedKeys(event) {
|
|
448
|
+
const keys = [];
|
|
449
|
+
if (event.ctrlKey)
|
|
450
|
+
keys.push('ctrl');
|
|
451
|
+
if (event.altKey)
|
|
452
|
+
keys.push('alt');
|
|
453
|
+
if (event.shiftKey)
|
|
454
|
+
keys.push('shift');
|
|
455
|
+
if (event.metaKey)
|
|
456
|
+
keys.push('meta');
|
|
457
|
+
// Add the main key (normalize to lowercase)
|
|
458
|
+
const key = event.key.toLowerCase();
|
|
459
|
+
if (!['control', 'alt', 'shift', 'meta'].includes(key)) {
|
|
460
|
+
keys.push(key);
|
|
461
|
+
}
|
|
462
|
+
return keys;
|
|
463
|
+
}
|
|
464
|
+
keysMatch(pressedKeys, targetKeys) {
|
|
465
|
+
if (pressedKeys.length !== targetKeys.length) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
// Normalize and sort both arrays for comparison
|
|
469
|
+
const normalizedPressed = pressedKeys.map(key => key.toLowerCase()).sort();
|
|
470
|
+
const normalizedTarget = targetKeys.map(key => key.toLowerCase()).sort();
|
|
471
|
+
return normalizedPressed.every((key, index) => key === normalizedTarget[index]);
|
|
472
|
+
}
|
|
473
|
+
isMacPlatform() {
|
|
474
|
+
return this.isBrowser && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
475
|
+
}
|
|
476
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: KeyboardShortcuts, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
477
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: KeyboardShortcuts, providedIn: 'root' });
|
|
478
|
+
}
|
|
479
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.4", ngImport: i0, type: KeyboardShortcuts, decorators: [{
|
|
480
|
+
type: Injectable,
|
|
481
|
+
args: [{
|
|
482
|
+
providedIn: 'root'
|
|
483
|
+
}]
|
|
484
|
+
}], ctorParameters: () => [] });
|
|
485
|
+
|
|
486
|
+
/*
|
|
487
|
+
* Public API Surface of ngx-keys
|
|
488
|
+
*/
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Generated bundle index. Do not edit.
|
|
492
|
+
*/
|
|
493
|
+
|
|
494
|
+
export { KeyboardShortcutError, KeyboardShortcuts, KeyboardShortcutsErrorFactory, KeyboardShortcutsErrors };
|
|
495
|
+
//# sourceMappingURL=ngx-keys.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngx-keys.mjs","sources":["../../../projects/ngx-keys/src/lib/keyboard-shortcuts.errors.ts","../../../projects/ngx-keys/src/lib/keyboard-shortcuts.ts","../../../projects/ngx-keys/src/public-api.ts","../../../projects/ngx-keys/src/ngx-keys.ts"],"sourcesContent":["/**\n * Centralized error messages for keyboard shortcuts service\n * This ensures consistency across the application and makes testing easier\n */\nexport const KeyboardShortcutsErrors = {\n // Registration errors\n SHORTCUT_ALREADY_REGISTERED: (id: string) => `Shortcut \"${id}\" already registered`,\n GROUP_ALREADY_REGISTERED: (id: string) => `Group \"${id}\" already registered`,\n KEY_CONFLICT: (conflictId: string) => `Key conflict with \"${conflictId}\"`,\n SHORTCUT_IDS_ALREADY_REGISTERED: (ids: string[]) => `Shortcut IDs already registered: ${ids.join(', ')}`,\n DUPLICATE_SHORTCUTS_IN_GROUP: (ids: string[]) => `Duplicate shortcuts in group: ${ids.join(', ')}`,\n KEY_CONFLICTS_IN_GROUP: (conflicts: string[]) => `Key conflicts: ${conflicts.join(', ')}`,\n \n // Operation errors\n SHORTCUT_NOT_REGISTERED: (id: string) => `Shortcut \"${id}\" not registered`,\n GROUP_NOT_REGISTERED: (id: string) => `Group \"${id}\" not registered`,\n \n // Activation/Deactivation errors\n CANNOT_ACTIVATE_SHORTCUT: (id: string) => `Cannot activate shortcut \"${id}\": not registered`,\n CANNOT_DEACTIVATE_SHORTCUT: (id: string) => `Cannot deactivate shortcut \"${id}\": not registered`,\n CANNOT_ACTIVATE_GROUP: (id: string) => `Cannot activate group \"${id}\": not registered`,\n CANNOT_DEACTIVATE_GROUP: (id: string) => `Cannot deactivate group \"${id}\": not registered`,\n \n // Unregistration errors\n CANNOT_UNREGISTER_SHORTCUT: (id: string) => `Cannot unregister shortcut \"${id}\": not registered`,\n CANNOT_UNREGISTER_GROUP: (id: string) => `Cannot unregister group \"${id}\": not registered`,\n} as const;\n\n/**\n * Error types for type safety\n */\nexport type KeyboardShortcutsErrorType = keyof typeof KeyboardShortcutsErrors;\n\n/**\n * Custom error class for keyboard shortcuts\n */\nexport class KeyboardShortcutError extends Error {\n constructor(\n public readonly errorType: KeyboardShortcutsErrorType,\n message: string,\n public readonly context?: Record<string, any>\n ) {\n super(message);\n this.name = 'KeyboardShortcutError';\n }\n}\n\n/**\n * Error factory for creating consistent errors\n */\nexport class KeyboardShortcutsErrorFactory {\n static shortcutAlreadyRegistered(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'SHORTCUT_ALREADY_REGISTERED',\n KeyboardShortcutsErrors.SHORTCUT_ALREADY_REGISTERED(id),\n { shortcutId: id }\n );\n }\n\n static groupAlreadyRegistered(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'GROUP_ALREADY_REGISTERED',\n KeyboardShortcutsErrors.GROUP_ALREADY_REGISTERED(id),\n { groupId: id }\n );\n }\n\n static keyConflict(conflictId: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'KEY_CONFLICT',\n KeyboardShortcutsErrors.KEY_CONFLICT(conflictId),\n { conflictId }\n );\n }\n\n static shortcutIdsAlreadyRegistered(ids: string[]): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'SHORTCUT_IDS_ALREADY_REGISTERED',\n KeyboardShortcutsErrors.SHORTCUT_IDS_ALREADY_REGISTERED(ids),\n { duplicateIds: ids }\n );\n }\n\n static duplicateShortcutsInGroup(ids: string[]): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'DUPLICATE_SHORTCUTS_IN_GROUP',\n KeyboardShortcutsErrors.DUPLICATE_SHORTCUTS_IN_GROUP(ids),\n { duplicateIds: ids }\n );\n }\n\n static keyConflictsInGroup(conflicts: string[]): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'KEY_CONFLICTS_IN_GROUP',\n KeyboardShortcutsErrors.KEY_CONFLICTS_IN_GROUP(conflicts),\n { conflicts }\n );\n }\n\n static shortcutNotRegistered(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'SHORTCUT_NOT_REGISTERED',\n KeyboardShortcutsErrors.SHORTCUT_NOT_REGISTERED(id),\n { shortcutId: id }\n );\n }\n\n static groupNotRegistered(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'GROUP_NOT_REGISTERED',\n KeyboardShortcutsErrors.GROUP_NOT_REGISTERED(id),\n { groupId: id }\n );\n }\n\n static cannotActivateShortcut(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'CANNOT_ACTIVATE_SHORTCUT',\n KeyboardShortcutsErrors.CANNOT_ACTIVATE_SHORTCUT(id),\n { shortcutId: id }\n );\n }\n\n static cannotDeactivateShortcut(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'CANNOT_DEACTIVATE_SHORTCUT',\n KeyboardShortcutsErrors.CANNOT_DEACTIVATE_SHORTCUT(id),\n { shortcutId: id }\n );\n }\n\n static cannotActivateGroup(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'CANNOT_ACTIVATE_GROUP',\n KeyboardShortcutsErrors.CANNOT_ACTIVATE_GROUP(id),\n { groupId: id }\n );\n }\n\n static cannotDeactivateGroup(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'CANNOT_DEACTIVATE_GROUP',\n KeyboardShortcutsErrors.CANNOT_DEACTIVATE_GROUP(id),\n { groupId: id }\n );\n }\n\n static cannotUnregisterShortcut(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'CANNOT_UNREGISTER_SHORTCUT',\n KeyboardShortcutsErrors.CANNOT_UNREGISTER_SHORTCUT(id),\n { shortcutId: id }\n );\n }\n\n static cannotUnregisterGroup(id: string): KeyboardShortcutError {\n return new KeyboardShortcutError(\n 'CANNOT_UNREGISTER_GROUP',\n KeyboardShortcutsErrors.CANNOT_UNREGISTER_GROUP(id),\n { groupId: id }\n );\n }\n}\n","import { Injectable, OnDestroy, PLATFORM_ID, inject, signal, computed } from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { KeyboardShortcut, KeyboardShortcutGroup, KeyboardShortcutUI } from './keyboard-shortcut.interface';\nimport { KeyboardShortcutsErrorFactory } from './keyboard-shortcuts.errors';\n\n@Injectable({\n providedIn: 'root'\n})\nexport class KeyboardShortcuts implements OnDestroy {\n private readonly shortcuts = new Map<string, KeyboardShortcut>();\n private readonly groups = new Map<string, KeyboardShortcutGroup>();\n private readonly activeShortcuts = new Set<string>();\n private readonly activeGroups = new Set<string>();\n \n // Single consolidated state signal - reduces memory overhead\n private readonly state = signal({\n shortcuts: new Map<string, KeyboardShortcut>(),\n groups: new Map<string, KeyboardShortcutGroup>(),\n activeShortcuts: new Set<string>(),\n activeGroups: new Set<string>(),\n version: 0 // for change detection optimization\n });\n \n // Primary computed signal - consumers derive what they need from this\n readonly shortcuts$ = computed(() => {\n const state = this.state();\n const activeShortcuts = Array.from(state.activeShortcuts)\n .map(id => state.shortcuts.get(id))\n .filter((s): s is KeyboardShortcut => s !== undefined);\n \n const inactiveShortcuts = Array.from(state.shortcuts.values())\n .filter(s => !state.activeShortcuts.has(s.id));\n \n return {\n active: activeShortcuts,\n inactive: inactiveShortcuts,\n all: Array.from(state.shortcuts.values()),\n groups: {\n active: Array.from(state.activeGroups),\n inactive: Array.from(state.groups.keys())\n .filter(id => !state.activeGroups.has(id))\n }\n };\n });\n\n // Optional: Pre-formatted UI signal for components that need it\n readonly shortcutsUI$ = computed(() => {\n const shortcuts = this.shortcuts$();\n return {\n active: shortcuts.active.map(s => this.formatShortcutForUI(s)),\n inactive: shortcuts.inactive.map(s => this.formatShortcutForUI(s)),\n all: shortcuts.all.map(s => this.formatShortcutForUI(s))\n };\n });\n \n private readonly keydownListener = this.handleKeydown.bind(this);\n private isListening = false;\n protected isBrowser: boolean;\n\n constructor() {\n // Use try-catch to handle injection context for better testability\n try {\n const platformId = inject(PLATFORM_ID);\n this.isBrowser = isPlatformBrowser(platformId);\n } catch {\n // Fallback for testing - assume browser environment\n this.isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';\n }\n\n if (this.isBrowser) {\n this.startListening();\n }\n }\n\n ngOnDestroy(): void {\n this.stopListening();\n }\n\n /**\n * Batch updates and increment version for change detection\n */\n private updateState(): void {\n this.state.update(current => ({\n shortcuts: new Map(this.shortcuts),\n groups: new Map(this.groups),\n activeShortcuts: new Set(this.activeShortcuts),\n activeGroups: new Set(this.activeGroups),\n version: current.version + 1\n }));\n }\n\n /**\n * Utility method for UI formatting\n */\n formatShortcutForUI(shortcut: KeyboardShortcut): KeyboardShortcutUI {\n return {\n id: shortcut.id,\n keys: this.formatKeysForDisplay(shortcut.keys, false),\n macKeys: this.formatKeysForDisplay(shortcut.macKeys, true),\n description: shortcut.description\n };\n }\n\n /**\n * Utility method for batch operations - reduces signal updates\n */\n batchUpdate(operations: () => void): void {\n operations();\n this.updateState();\n }\n\n /**\n * Format keys for display with proper Unicode symbols\n */\n private formatKeysForDisplay(keys: string[], isMac = false): string {\n const keyMap: Record<string, string> = isMac ? {\n 'ctrl': '⌃',\n 'alt': '⌥', \n 'shift': '⇧',\n 'meta': '⌘',\n 'cmd': '⌘',\n 'command': '⌘'\n } : {\n 'ctrl': 'Ctrl',\n 'alt': 'Alt',\n 'shift': 'Shift', \n 'meta': 'Win'\n };\n\n return keys\n .map(key => keyMap[key.toLowerCase()] || key.toUpperCase())\n .join('+');\n }\n\n /**\n * Check if a key combination is already registered\n * @returns The ID of the conflicting shortcut, or null if no conflict\n */\n private findConflict(newShortcut: KeyboardShortcut): string | null {\n for (const existing of this.shortcuts.values()) {\n if (this.keysMatch(newShortcut.keys, existing.keys)) {\n return existing.id;\n }\n if (this.keysMatch(newShortcut.macKeys, existing.macKeys)) {\n return existing.id;\n }\n }\n return null;\n }\n\n /**\n * Register a single keyboard shortcut\n * @throws KeyboardShortcutError if shortcut ID is already registered or key combination is in use\n */\n register(shortcut: KeyboardShortcut): void {\n if (this.shortcuts.has(shortcut.id)) {\n throw KeyboardShortcutsErrorFactory.shortcutAlreadyRegistered(shortcut.id);\n }\n\n const conflictId = this.findConflict(shortcut);\n if (conflictId) {\n throw KeyboardShortcutsErrorFactory.keyConflict(conflictId);\n }\n \n this.shortcuts.set(shortcut.id, shortcut);\n this.activeShortcuts.add(shortcut.id);\n this.updateState();\n }\n\n /**\n * Register multiple keyboard shortcuts as a group\n * @throws KeyboardShortcutError if group ID is already registered or if any shortcut ID or key combination conflicts\n */\n registerGroup(groupId: string, shortcuts: KeyboardShortcut[]): void {\n // Check if group ID already exists\n if (this.groups.has(groupId)) {\n throw KeyboardShortcutsErrorFactory.groupAlreadyRegistered(groupId);\n }\n \n // Check for duplicate shortcut IDs and key combination conflicts\n const duplicateIds: string[] = [];\n const keyConflicts: string[] = [];\n shortcuts.forEach(shortcut => {\n if (this.shortcuts.has(shortcut.id)) {\n duplicateIds.push(shortcut.id);\n }\n const conflictId = this.findConflict(shortcut);\n if (conflictId) {\n keyConflicts.push(`\"${shortcut.id}\" conflicts with \"${conflictId}\"`);\n }\n });\n \n if (duplicateIds.length > 0) {\n throw KeyboardShortcutsErrorFactory.shortcutIdsAlreadyRegistered(duplicateIds);\n }\n\n if (keyConflicts.length > 0) {\n throw KeyboardShortcutsErrorFactory.keyConflictsInGroup(keyConflicts);\n }\n \n // Validate that all shortcuts have unique IDs within the group\n const groupIds = new Set<string>();\n const duplicatesInGroup: string[] = [];\n shortcuts.forEach(shortcut => {\n if (groupIds.has(shortcut.id)) {\n duplicatesInGroup.push(shortcut.id);\n } else {\n groupIds.add(shortcut.id);\n }\n });\n \n if (duplicatesInGroup.length > 0) {\n throw KeyboardShortcutsErrorFactory.duplicateShortcutsInGroup(duplicatesInGroup);\n }\n \n // Use batch update to reduce signal updates\n this.batchUpdate(() => {\n const group: KeyboardShortcutGroup = {\n id: groupId,\n shortcuts,\n active: true\n };\n \n this.groups.set(groupId, group);\n this.activeGroups.add(groupId);\n \n // Register individual shortcuts\n shortcuts.forEach(shortcut => {\n this.shortcuts.set(shortcut.id, shortcut);\n this.activeShortcuts.add(shortcut.id);\n });\n });\n }\n\n /**\n * Unregister a single keyboard shortcut\n * @throws KeyboardShortcutError if shortcut ID doesn't exist\n */\n unregister(shortcutId: string): void {\n if (!this.shortcuts.has(shortcutId)) {\n throw KeyboardShortcutsErrorFactory.cannotUnregisterShortcut(shortcutId);\n }\n \n this.shortcuts.delete(shortcutId);\n this.activeShortcuts.delete(shortcutId);\n this.updateState();\n }\n\n /**\n * Unregister a group of keyboard shortcuts\n * @throws KeyboardShortcutError if group ID doesn't exist\n */\n unregisterGroup(groupId: string): void {\n const group = this.groups.get(groupId);\n if (!group) {\n throw KeyboardShortcutsErrorFactory.cannotUnregisterGroup(groupId);\n }\n \n this.batchUpdate(() => {\n group.shortcuts.forEach(shortcut => {\n this.shortcuts.delete(shortcut.id);\n this.activeShortcuts.delete(shortcut.id);\n });\n this.groups.delete(groupId);\n this.activeGroups.delete(groupId);\n });\n }\n\n /**\n * Activate a single keyboard shortcut\n * @throws KeyboardShortcutError if shortcut ID doesn't exist\n */\n activate(shortcutId: string): void {\n if (!this.shortcuts.has(shortcutId)) {\n throw KeyboardShortcutsErrorFactory.cannotActivateShortcut(shortcutId);\n }\n \n this.activeShortcuts.add(shortcutId);\n this.updateState();\n }\n\n /**\n * Deactivate a single keyboard shortcut\n * @throws KeyboardShortcutError if shortcut ID doesn't exist\n */\n deactivate(shortcutId: string): void {\n if (!this.shortcuts.has(shortcutId)) {\n throw KeyboardShortcutsErrorFactory.cannotDeactivateShortcut(shortcutId);\n }\n \n this.activeShortcuts.delete(shortcutId);\n this.updateState();\n }\n\n /**\n * Activate a group of keyboard shortcuts\n * @throws KeyboardShortcutError if group ID doesn't exist\n */\n activateGroup(groupId: string): void {\n const group = this.groups.get(groupId);\n if (!group) {\n throw KeyboardShortcutsErrorFactory.cannotActivateGroup(groupId);\n }\n \n this.batchUpdate(() => {\n group.active = true;\n this.activeGroups.add(groupId);\n group.shortcuts.forEach(shortcut => {\n this.activeShortcuts.add(shortcut.id);\n });\n });\n }\n\n /**\n * Deactivate a group of keyboard shortcuts\n * @throws KeyboardShortcutError if group ID doesn't exist\n */\n deactivateGroup(groupId: string): void {\n const group = this.groups.get(groupId);\n if (!group) {\n throw KeyboardShortcutsErrorFactory.cannotDeactivateGroup(groupId);\n }\n \n this.batchUpdate(() => {\n group.active = false;\n this.activeGroups.delete(groupId);\n group.shortcuts.forEach(shortcut => {\n this.activeShortcuts.delete(shortcut.id);\n });\n });\n }\n\n /**\n * Check if a shortcut is active\n */\n isActive(shortcutId: string): boolean {\n return this.activeShortcuts.has(shortcutId);\n }\n\n /**\n * Check if a shortcut is registered\n */\n isRegistered(shortcutId: string): boolean {\n return this.shortcuts.has(shortcutId);\n }\n\n /**\n * Check if a group is active\n */\n isGroupActive(groupId: string): boolean {\n return this.activeGroups.has(groupId);\n }\n\n /**\n * Check if a group is registered\n */\n isGroupRegistered(groupId: string): boolean {\n return this.groups.has(groupId);\n }\n\n /**\n * Get all registered shortcuts\n */\n getShortcuts(): ReadonlyMap<string, KeyboardShortcut> {\n return this.shortcuts;\n }\n\n /**\n * Get all registered groups\n */\n getGroups(): ReadonlyMap<string, KeyboardShortcutGroup> {\n return this.groups;\n }\n\n private startListening(): void {\n if (!this.isBrowser || this.isListening) {\n return;\n }\n \n document.addEventListener('keydown', this.keydownListener, { passive: false });\n this.isListening = true;\n }\n\n private stopListening(): void {\n if (!this.isBrowser || !this.isListening) {\n return;\n }\n \n document.removeEventListener('keydown', this.keydownListener);\n this.isListening = false;\n }\n\n protected handleKeydown(event: KeyboardEvent): void {\n const pressedKeys = this.getPressedKeys(event);\n const isMac = this.isMacPlatform();\n \n for (const shortcutId of this.activeShortcuts) {\n const shortcut = this.shortcuts.get(shortcutId);\n if (!shortcut) continue;\n \n const targetKeys = isMac ? shortcut.macKeys : shortcut.keys;\n \n if (this.keysMatch(pressedKeys, targetKeys)) {\n event.preventDefault();\n event.stopPropagation();\n \n try {\n shortcut.action();\n } catch (error) {\n console.error(`Error executing keyboard shortcut \"${shortcut.id}\":`, error);\n }\n \n break; // Only execute the first matching shortcut\n }\n }\n }\n\n protected getPressedKeys(event: KeyboardEvent): string[] {\n const keys: string[] = [];\n \n if (event.ctrlKey) keys.push('ctrl');\n if (event.altKey) keys.push('alt');\n if (event.shiftKey) keys.push('shift');\n if (event.metaKey) keys.push('meta');\n \n // Add the main key (normalize to lowercase)\n const key = event.key.toLowerCase();\n if (!['control', 'alt', 'shift', 'meta'].includes(key)) {\n keys.push(key);\n }\n \n return keys;\n }\n\n protected keysMatch(pressedKeys: string[], targetKeys: string[]): boolean {\n if (pressedKeys.length !== targetKeys.length) {\n return false;\n }\n \n // Normalize and sort both arrays for comparison\n const normalizedPressed = pressedKeys.map(key => key.toLowerCase()).sort();\n const normalizedTarget = targetKeys.map(key => key.toLowerCase()).sort();\n \n return normalizedPressed.every((key, index) => key === normalizedTarget[index]);\n }\n\n protected isMacPlatform(): boolean {\n return this.isBrowser && /Mac|iPod|iPhone|iPad/.test(navigator.platform);\n }\n}\n","/*\n * Public API Surface of ngx-keys\n */\n\nexport * from './lib/keyboard-shortcuts';\nexport * from './lib/keyboard-shortcut.interface';\nexport * from './lib/keyboard-shortcuts.errors';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
import { OnDestroy } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
interface KeyboardShortcut {
|
|
5
|
+
id: string;
|
|
6
|
+
keys: string[];
|
|
7
|
+
macKeys: string[];
|
|
8
|
+
action: () => void;
|
|
9
|
+
description: string;
|
|
10
|
+
}
|
|
11
|
+
interface KeyboardShortcutGroup {
|
|
12
|
+
id: string;
|
|
13
|
+
shortcuts: KeyboardShortcut[];
|
|
14
|
+
active: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Interface for keyboard shortcut data optimized for UI display
|
|
18
|
+
*/
|
|
19
|
+
interface KeyboardShortcutUI {
|
|
20
|
+
id: string;
|
|
21
|
+
keys: string;
|
|
22
|
+
macKeys: string;
|
|
23
|
+
description: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare class KeyboardShortcuts implements OnDestroy {
|
|
27
|
+
private readonly shortcuts;
|
|
28
|
+
private readonly groups;
|
|
29
|
+
private readonly activeShortcuts;
|
|
30
|
+
private readonly activeGroups;
|
|
31
|
+
private readonly state;
|
|
32
|
+
readonly shortcuts$: _angular_core.Signal<{
|
|
33
|
+
active: KeyboardShortcut[];
|
|
34
|
+
inactive: KeyboardShortcut[];
|
|
35
|
+
all: KeyboardShortcut[];
|
|
36
|
+
groups: {
|
|
37
|
+
active: string[];
|
|
38
|
+
inactive: string[];
|
|
39
|
+
};
|
|
40
|
+
}>;
|
|
41
|
+
readonly shortcutsUI$: _angular_core.Signal<{
|
|
42
|
+
active: KeyboardShortcutUI[];
|
|
43
|
+
inactive: KeyboardShortcutUI[];
|
|
44
|
+
all: KeyboardShortcutUI[];
|
|
45
|
+
}>;
|
|
46
|
+
private readonly keydownListener;
|
|
47
|
+
private isListening;
|
|
48
|
+
protected isBrowser: boolean;
|
|
49
|
+
constructor();
|
|
50
|
+
ngOnDestroy(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Batch updates and increment version for change detection
|
|
53
|
+
*/
|
|
54
|
+
private updateState;
|
|
55
|
+
/**
|
|
56
|
+
* Utility method for UI formatting
|
|
57
|
+
*/
|
|
58
|
+
formatShortcutForUI(shortcut: KeyboardShortcut): KeyboardShortcutUI;
|
|
59
|
+
/**
|
|
60
|
+
* Utility method for batch operations - reduces signal updates
|
|
61
|
+
*/
|
|
62
|
+
batchUpdate(operations: () => void): void;
|
|
63
|
+
/**
|
|
64
|
+
* Format keys for display with proper Unicode symbols
|
|
65
|
+
*/
|
|
66
|
+
private formatKeysForDisplay;
|
|
67
|
+
/**
|
|
68
|
+
* Check if a key combination is already registered
|
|
69
|
+
* @returns The ID of the conflicting shortcut, or null if no conflict
|
|
70
|
+
*/
|
|
71
|
+
private findConflict;
|
|
72
|
+
/**
|
|
73
|
+
* Register a single keyboard shortcut
|
|
74
|
+
* @throws KeyboardShortcutError if shortcut ID is already registered or key combination is in use
|
|
75
|
+
*/
|
|
76
|
+
register(shortcut: KeyboardShortcut): void;
|
|
77
|
+
/**
|
|
78
|
+
* Register multiple keyboard shortcuts as a group
|
|
79
|
+
* @throws KeyboardShortcutError if group ID is already registered or if any shortcut ID or key combination conflicts
|
|
80
|
+
*/
|
|
81
|
+
registerGroup(groupId: string, shortcuts: KeyboardShortcut[]): void;
|
|
82
|
+
/**
|
|
83
|
+
* Unregister a single keyboard shortcut
|
|
84
|
+
* @throws KeyboardShortcutError if shortcut ID doesn't exist
|
|
85
|
+
*/
|
|
86
|
+
unregister(shortcutId: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* Unregister a group of keyboard shortcuts
|
|
89
|
+
* @throws KeyboardShortcutError if group ID doesn't exist
|
|
90
|
+
*/
|
|
91
|
+
unregisterGroup(groupId: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* Activate a single keyboard shortcut
|
|
94
|
+
* @throws KeyboardShortcutError if shortcut ID doesn't exist
|
|
95
|
+
*/
|
|
96
|
+
activate(shortcutId: string): void;
|
|
97
|
+
/**
|
|
98
|
+
* Deactivate a single keyboard shortcut
|
|
99
|
+
* @throws KeyboardShortcutError if shortcut ID doesn't exist
|
|
100
|
+
*/
|
|
101
|
+
deactivate(shortcutId: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* Activate a group of keyboard shortcuts
|
|
104
|
+
* @throws KeyboardShortcutError if group ID doesn't exist
|
|
105
|
+
*/
|
|
106
|
+
activateGroup(groupId: string): void;
|
|
107
|
+
/**
|
|
108
|
+
* Deactivate a group of keyboard shortcuts
|
|
109
|
+
* @throws KeyboardShortcutError if group ID doesn't exist
|
|
110
|
+
*/
|
|
111
|
+
deactivateGroup(groupId: string): void;
|
|
112
|
+
/**
|
|
113
|
+
* Check if a shortcut is active
|
|
114
|
+
*/
|
|
115
|
+
isActive(shortcutId: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Check if a shortcut is registered
|
|
118
|
+
*/
|
|
119
|
+
isRegistered(shortcutId: string): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Check if a group is active
|
|
122
|
+
*/
|
|
123
|
+
isGroupActive(groupId: string): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Check if a group is registered
|
|
126
|
+
*/
|
|
127
|
+
isGroupRegistered(groupId: string): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Get all registered shortcuts
|
|
130
|
+
*/
|
|
131
|
+
getShortcuts(): ReadonlyMap<string, KeyboardShortcut>;
|
|
132
|
+
/**
|
|
133
|
+
* Get all registered groups
|
|
134
|
+
*/
|
|
135
|
+
getGroups(): ReadonlyMap<string, KeyboardShortcutGroup>;
|
|
136
|
+
private startListening;
|
|
137
|
+
private stopListening;
|
|
138
|
+
protected handleKeydown(event: KeyboardEvent): void;
|
|
139
|
+
protected getPressedKeys(event: KeyboardEvent): string[];
|
|
140
|
+
protected keysMatch(pressedKeys: string[], targetKeys: string[]): boolean;
|
|
141
|
+
protected isMacPlatform(): boolean;
|
|
142
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<KeyboardShortcuts, never>;
|
|
143
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<KeyboardShortcuts>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Centralized error messages for keyboard shortcuts service
|
|
148
|
+
* This ensures consistency across the application and makes testing easier
|
|
149
|
+
*/
|
|
150
|
+
declare const KeyboardShortcutsErrors: {
|
|
151
|
+
readonly SHORTCUT_ALREADY_REGISTERED: (id: string) => string;
|
|
152
|
+
readonly GROUP_ALREADY_REGISTERED: (id: string) => string;
|
|
153
|
+
readonly KEY_CONFLICT: (conflictId: string) => string;
|
|
154
|
+
readonly SHORTCUT_IDS_ALREADY_REGISTERED: (ids: string[]) => string;
|
|
155
|
+
readonly DUPLICATE_SHORTCUTS_IN_GROUP: (ids: string[]) => string;
|
|
156
|
+
readonly KEY_CONFLICTS_IN_GROUP: (conflicts: string[]) => string;
|
|
157
|
+
readonly SHORTCUT_NOT_REGISTERED: (id: string) => string;
|
|
158
|
+
readonly GROUP_NOT_REGISTERED: (id: string) => string;
|
|
159
|
+
readonly CANNOT_ACTIVATE_SHORTCUT: (id: string) => string;
|
|
160
|
+
readonly CANNOT_DEACTIVATE_SHORTCUT: (id: string) => string;
|
|
161
|
+
readonly CANNOT_ACTIVATE_GROUP: (id: string) => string;
|
|
162
|
+
readonly CANNOT_DEACTIVATE_GROUP: (id: string) => string;
|
|
163
|
+
readonly CANNOT_UNREGISTER_SHORTCUT: (id: string) => string;
|
|
164
|
+
readonly CANNOT_UNREGISTER_GROUP: (id: string) => string;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Error types for type safety
|
|
168
|
+
*/
|
|
169
|
+
type KeyboardShortcutsErrorType = keyof typeof KeyboardShortcutsErrors;
|
|
170
|
+
/**
|
|
171
|
+
* Custom error class for keyboard shortcuts
|
|
172
|
+
*/
|
|
173
|
+
declare class KeyboardShortcutError extends Error {
|
|
174
|
+
readonly errorType: KeyboardShortcutsErrorType;
|
|
175
|
+
readonly context?: Record<string, any> | undefined;
|
|
176
|
+
constructor(errorType: KeyboardShortcutsErrorType, message: string, context?: Record<string, any> | undefined);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Error factory for creating consistent errors
|
|
180
|
+
*/
|
|
181
|
+
declare class KeyboardShortcutsErrorFactory {
|
|
182
|
+
static shortcutAlreadyRegistered(id: string): KeyboardShortcutError;
|
|
183
|
+
static groupAlreadyRegistered(id: string): KeyboardShortcutError;
|
|
184
|
+
static keyConflict(conflictId: string): KeyboardShortcutError;
|
|
185
|
+
static shortcutIdsAlreadyRegistered(ids: string[]): KeyboardShortcutError;
|
|
186
|
+
static duplicateShortcutsInGroup(ids: string[]): KeyboardShortcutError;
|
|
187
|
+
static keyConflictsInGroup(conflicts: string[]): KeyboardShortcutError;
|
|
188
|
+
static shortcutNotRegistered(id: string): KeyboardShortcutError;
|
|
189
|
+
static groupNotRegistered(id: string): KeyboardShortcutError;
|
|
190
|
+
static cannotActivateShortcut(id: string): KeyboardShortcutError;
|
|
191
|
+
static cannotDeactivateShortcut(id: string): KeyboardShortcutError;
|
|
192
|
+
static cannotActivateGroup(id: string): KeyboardShortcutError;
|
|
193
|
+
static cannotDeactivateGroup(id: string): KeyboardShortcutError;
|
|
194
|
+
static cannotUnregisterShortcut(id: string): KeyboardShortcutError;
|
|
195
|
+
static cannotUnregisterGroup(id: string): KeyboardShortcutError;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { KeyboardShortcutError, KeyboardShortcuts, KeyboardShortcutsErrorFactory, KeyboardShortcutsErrors };
|
|
199
|
+
export type { KeyboardShortcut, KeyboardShortcutGroup, KeyboardShortcutUI, KeyboardShortcutsErrorType };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ngx-keys",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A reactive Angular library for managing keyboard shortcuts with signals-based UI integration",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"angular",
|
|
7
|
+
"typescript",
|
|
8
|
+
"keyboard-shortcuts",
|
|
9
|
+
"hotkeys",
|
|
10
|
+
"angular-library",
|
|
11
|
+
"reactive",
|
|
12
|
+
"signals",
|
|
13
|
+
"ui-components",
|
|
14
|
+
"cross-platform",
|
|
15
|
+
"angular-signals"
|
|
16
|
+
],
|
|
17
|
+
"license": "0BSD",
|
|
18
|
+
"author": "NgxKeys Contributors",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/mrivasperez/ngx-keys.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/mrivasperez/ngx-keys/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/mrivasperez/ngx-keys#readme",
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@angular/common": "^20.2.0",
|
|
29
|
+
"@angular/core": "^20.2.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"tslib": "^2.3.0"
|
|
33
|
+
},
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"module": "fesm2022/ngx-keys.mjs",
|
|
36
|
+
"typings": "index.d.ts",
|
|
37
|
+
"exports": {
|
|
38
|
+
"./package.json": {
|
|
39
|
+
"default": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./index.d.ts",
|
|
43
|
+
"default": "./fesm2022/ngx-keys.mjs"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|