Skip to content

Commit f70e412

Browse files
author
Ghislain Thau
committed
fix: prevents the shortcut's callback from being executing on registering of duplicated shortcuts
When registering a duplicated shortcut, the `addShortcut` and `addSequenceShortcut` methods return `of(null)` which immediately emits on subscribe. In case of duplicated shortcuts, we instead return EMPTY which doesn't emit and completes immediately on subscribe.
1 parent c61076b commit f70e412

File tree

3 files changed

+43
-41
lines changed

3 files changed

+43
-41
lines changed

README.md

+31-39
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Web apps are getting closer and closer to be desktop-class applications. With th
3636
## Compatibility with Angular Versions
3737

3838
| @ngneat/hotkeys | Angular |
39-
|-----------------|---------|
39+
| --------------- | ------- |
4040
| 3.x.x | >=17.2 |
4141
| 2.x.x | >=16 |
4242
| 1.3.x | >=14 |
@@ -49,18 +49,20 @@ Web apps are getting closer and closer to be desktop-class applications. With th
4949
## Usage
5050

5151
### Module
52+
5253
Add `HotkeysModule` in your `AppModule`:
5354

5455
```ts
5556
import { HotkeysModule } from '@ngneat/hotkeys';
5657

5758
@NgModule({
58-
imports: [HotkeysModule]
59+
imports: [HotkeysModule],
5960
})
6061
export class AppModule {}
6162
```
6263

6364
### Standalone
65+
6466
Add `HotkeysService` in the standalone components :
6567

6668
```ts
@@ -93,9 +95,9 @@ For example:
9395

9496
<!-- prettier-ignore -->
9597
```html
96-
<input hotkeys="meta.n"
97-
hotkeysGroup="File"
98-
hotkeysDescription="New Document"
98+
<input hotkeys="meta.n"
99+
hotkeysGroup="File"
100+
hotkeysDescription="New Document"
99101
(hotkey)="handleHotkey($event)"/>
100102

101103
<button hotkeys="shift.f" isGlobal (hotkey)="handleHotkey($event)">Click me</button>
@@ -117,14 +119,14 @@ import { HotkeysService } from '@ngneat/hotkeys';
117119
@Component({
118120
selector: 'app-root',
119121
templateUrl: './app.component.html',
120-
styleUrls: ['./app.component.css']
122+
styleUrls: ['./app.component.css'],
121123
})
122124
export class AppComponent {
123125
constructor(private hotkeys: HotkeysService) {}
124126

125127
ngOnInit() {
126-
this.hotkeys.addShortcut({ keys: 'meta.a' }).subscribe(e => console.log('Hotkey', e));
127-
this.hotkeys.addSequenceShortcut({ keys: 'g>i' }).subscribe(e => console.log('Hotkey', e));
128+
this.hotkeys.addShortcut({ keys: 'meta.a' }).subscribe((e) => console.log('Hotkey', e));
129+
this.hotkeys.addSequenceShortcut({ keys: 'g>i' }).subscribe((e) => console.log('Hotkey', e));
128130
}
129131
}
130132
```
@@ -175,10 +177,13 @@ import { HotkeysHelpComponent, HotkeysService } from '@ngneat/hotkeys';
175177
@Component({
176178
selector: 'app-root',
177179
templateUrl: './app.component.html',
178-
styleUrls: ['./app.component.css']
180+
styleUrls: ['./app.component.css'],
179181
})
180182
export class AppComponent implements AfterViewInit {
181-
constructor(private hotkeys: HotkeysService, private dialog: NgbModal) {}
183+
constructor(
184+
private hotkeys: HotkeysService,
185+
private dialog: NgbModal,
186+
) {}
182187

183188
ngAfterViewInit() {
184189
this.hotkeys.registerHelpModal(() => {
@@ -191,6 +196,7 @@ export class AppComponent implements AfterViewInit {
191196
```
192197

193198
It accepts a second input that allows defining the hotkey that should open the dialog. The default shortcut is `Shift + ?`. Here's how `HotkeysHelpComponent` looks like:
199+
194200
```
195201
<p align="center">
196202
<img width="50%" height="50%" src="./help_screenshot.png">
@@ -251,44 +257,28 @@ The pipe accepts and additional parameter the way key combinations are separated
251257

252258
It is also possible to alias keys to custom strings. For example, the macos key for enter is ``. To display it as `Enter`, you can use the following:
253259

254-
```html
260+
````html
255261
<div class="help-dialog-shortcut-key">
256262
<kbd [innerHTML]="hotkey.keys | hotkeysShortcut: '-' : ' then ': {enter: 'Enter'}"></kbd>
257263
</div>
258264

259-
```html
260-
261-
## Allowing hotkeys in form elements
262-
263-
By default, the library prevents hotkey callbacks from firing when their event originates from an `input`, `select`, or `textarea` element or any elements that are contenteditable. To enable hotkeys in these elements, specify them in the `allowIn` parameter:
264-
265-
```ts
266-
import { HotkeysService } from '@ngneat/hotkeys';
267-
268-
@Component({
269-
selector: 'app-root',
270-
templateUrl: './app.component.html',
271-
styleUrls: ['./app.component.css']
272-
})
273-
export class AppComponent {
274-
constructor(private hotkeys: HotkeysService) {}
275-
276-
ngOnInit() {
277-
this.hotkeys
278-
.addShortcut({ keys: 'meta.a', allowIn: ['INPUT', 'SELECT', 'TEXTAREA', 'CONTENTEDITABLE'] })
279-
.subscribe(e => console.log('Hotkey', e));
280-
}
281-
}
282-
```
265+
```html ## Allowing hotkeys in form elements By default, the library prevents hotkey callbacks from firing when their
266+
event originates from an `input`, `select`, or `textarea` element or any elements that are contenteditable. To enable
267+
hotkeys in these elements, specify them in the `allowIn` parameter: ```ts import { HotkeysService } from
268+
'@ngneat/hotkeys'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls:
269+
['./app.component.css'] }) export class AppComponent { constructor(private hotkeys: HotkeysService) {} ngOnInit() {
270+
this.hotkeys .addShortcut({ keys: 'meta.a', allowIn: ['INPUT', 'SELECT', 'TEXTAREA', 'CONTENTEDITABLE'] }) .subscribe(e
271+
=> console.log('Hotkey', e)); } }
272+
````
283273

284274
It's possible to enable them in the template as well:
285275

286276
<!-- prettier-ignore -->
287277
```html
288-
<input hotkeys="meta.n"
289-
hotkeysGroup="File"
290-
hotkeysDescription="New Document"
291-
hotkeysOptions="{allowIn: ['INPUT','SELECT', 'TEXTAREA', 'CONTENTEDITABLE']}"
278+
<input hotkeys="meta.n"
279+
hotkeysGroup="File"
280+
hotkeysDescription="New Document"
281+
hotkeysOptions="{allowIn: ['INPUT','SELECT', 'TEXTAREA', 'CONTENTEDITABLE']}"
292282
(hotkey)="handleHotkey($event)"
293283
```
294284

@@ -300,6 +290,8 @@ That's all for now! Make sure to check out the `playground` inside the `src` [fo
300290
301291
No. It's not possible to define a hotkey multiple times. Each hotkey has a description and a group, so it doesn't make sense assigning a hotkey to different actions.
302292
293+
In case of registering duplicated hotkeys, the `addShortcut` and `addSequenceShortcut` methods return an `EMPTY` observable, allowing to detect duplicated hotkeys registration and execute custom logic.
294+
303295
**Why am I not receiving any event?**
304296
305297
If you've added a hotkey to a particular element of your DOM, make sure it's focusable. Otherwise, hotkeys cannot capture any keyboard event.

projects/ngneat/hotkeys/src/lib/hotkeys.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export class HotkeysService {
137137

138138
if (sequenceSummary.hotkeyMap.has(normalizedKeys)) {
139139
console.error('Duplicated shortcut');
140-
return of(null);
140+
return EMPTY;
141141
}
142142

143143
sequenceSummary.hotkeyMap.set(normalizedKeys, hotkeySummary);
@@ -170,7 +170,7 @@ export class HotkeysService {
170170

171171
if (this.hotkeys.has(normalizedKeys)) {
172172
console.error('Duplicated shortcut');
173-
return of(null);
173+
return EMPTY;
174174
}
175175

176176
this.hotkeys.set(normalizedKeys, mergedOptions);

projects/ngneat/hotkeys/src/lib/tests/hotkeys.service.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ describe('Service: Hotkeys', () => {
2727
expect(spectator.service.getHotkeys().length).toBe(0);
2828
});
2929

30+
it('should not emit after registering duplicated shortcuts', () => {
31+
const spyFcn = createSpy('subscribe', (...args) => {});
32+
spectator.service.addShortcut({ keys: 'a' }).subscribe(spyFcn);
33+
spectator.service.addShortcut({ keys: 'a' }).subscribe(spyFcn);
34+
spectator.service.addSequenceShortcut({ keys: 'g>a' }).subscribe(spyFcn);
35+
spectator.service.addSequenceShortcut({ keys: 'g>a' }).subscribe(spyFcn);
36+
37+
expect(spyFcn).not.toHaveBeenCalled();
38+
});
39+
3040
it('should unsubscribe shortcuts when removed', () => {
3141
const subscription = spectator.service.addShortcut({ keys: 'meta.a' }).subscribe();
3242
spectator.service.removeShortcuts('meta.a');

0 commit comments

Comments
 (0)