Skip to content

Commit

Permalink
fix(release): update to angular 17 and bump all dependencies to latest
Browse files Browse the repository at this point in the history
Changed effects to ngOnChanges for input changes in the directive.
Used Omit<> to reuse Options type from the service instead of creating a new one.
Removed unnecessary insert of the service into providers array.
  • Loading branch information
Pilpin committed Mar 14, 2024
1 parent 234edd9 commit 5323d0c
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 71 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ export class AppModule {}
```

### Standalone
Add `HotkeysService` in your `ApplicationConfig` providers:
Add `HotkeysService` in the standalone components :

```ts
import {HotkeysService} from "@ngneat/hotkeys";

bootstrapApplication(AppComponent, {
providers: [HotkeysService]
@Component({
standalone: true,
imports: [HotkeysDirective],
})
export class AppComponent {}
```

Now you have two ways to start adding shortcuts to your application:
Expand Down
2 changes: 1 addition & 1 deletion projects/ngneat/hotkeys/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngneat/hotkeys",
"version": "2.0.0",
"version": "3.0.0",
"description": "A declarative library for handling hotkeys in Angular applications",
"keywords": [
"angular",
Expand Down
79 changes: 46 additions & 33 deletions projects/ngneat/hotkeys/src/lib/hotkeys.directive.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import { DestroyRef, Directive, effect, ElementRef, EventEmitter, inject, input, Output } from '@angular/core';
import {
computed,
DestroyRef,
Directive,
ElementRef,
EventEmitter,
inject,
input,
OnChanges,
Output,
SimpleChanges,
} from '@angular/core';
import { merge, Subscription } from 'rxjs';
import { mergeAll } from 'rxjs/operators';

import { AllowInElement, Hotkey, HotkeysService } from './hotkeys.service';
import { Hotkey, HotkeysService, Options as ServiceOptions } from './hotkeys.service';
import { coerceArray } from './utils/array';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

interface Options {
trigger: 'keydown' | 'keyup';
allowIn: AllowInElement[];
showInHelpMenu: boolean;
preventDefault: boolean;
}
type Options = Omit<ServiceOptions, 'group' | 'element' | 'description'>;

@Directive({
standalone: true,
selector: '[hotkeys]',
})
export class HotkeysDirective {
export class HotkeysDirective implements OnChanges {
private destroyRef = inject(DestroyRef);
private hotkeysService = inject(HotkeysService);
private elementRef = inject(ElementRef);
Expand All @@ -30,29 +36,36 @@ export class HotkeysDirective {
hotkeysDescription = input<string>();
@Output() hotkey = new EventEmitter<KeyboardEvent | Hotkey>();

constructor() {
effect(() => {
if (this.subscription) this.subscription.unsubscribe();
this.subscription = null;
if (!this.hotkeys()) return;

const hotkey: Hotkey = {
keys: this.hotkeys(),
group: this.hotkeysGroup(),
description: this.hotkeysDescription(),
...this.hotkeysOptions(),
};

const coercedHotkeys = coerceArray(hotkey);
this.subscription = merge(
coercedHotkeys.map((hotkey) => {
return this.isSequence()
? this.hotkeysService.addSequenceShortcut({ ...hotkey, element: this.elementRef.nativeElement })
: this.hotkeysService.addShortcut({ ...hotkey, element: this.elementRef.nativeElement });
}),
)
.pipe(mergeAll(), takeUntilDestroyed(this.destroyRef))
.subscribe((e) => this.hotkey.next(e));
});
private _hotkey = computed(() => ({
keys: this.hotkeys(),
group: this.hotkeysGroup(),
description: this.hotkeysDescription(),
...this.hotkeysOptions(),
}));

ngOnChanges(changes: SimpleChanges): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
this.subscription = null;

if (!this.hotkeys) {
return;
}

this.setHotkeys(this._hotkey());
}

private setHotkeys(hotkeys: Hotkey | Hotkey[]) {
const coercedHotkeys = coerceArray(hotkeys);
this.subscription = merge(
coercedHotkeys.map((hotkey) => {
return this.isSequence()
? this.hotkeysService.addSequenceShortcut({ ...hotkey, element: this.elementRef.nativeElement })
: this.hotkeysService.addShortcut({ ...hotkey, element: this.elementRef.nativeElement });
}),
)
.pipe(takeUntilDestroyed(this.destroyRef), mergeAll())
.subscribe((e) => this.hotkey.next(e));
}
}
65 changes: 35 additions & 30 deletions projects/ngneat/hotkeys/src/lib/hotkeys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import { coerceArray } from './utils/array';
import { hostPlatform, normalizeKeys } from './utils/platform';

export type AllowInElement = 'INPUT' | 'TEXTAREA' | 'SELECT' | 'CONTENTEDITABLE';
interface Options {
export type Options = {
group: string;
element: HTMLElement;
trigger: 'keydown' | 'keyup';
allowIn: AllowInElement[];
description: string;
showInHelpMenu: boolean;
preventDefault: boolean;
}
};

export interface HotkeyGroup {
group: string;
Expand Down Expand Up @@ -48,19 +48,22 @@ export class HotkeysService {
group: undefined,
description: undefined,
showInHelpMenu: true,
preventDefault: true
preventDefault: true,
};
private callbacks: HotkeyCallback[] = [];
private sequenceMaps = new Map<HTMLElement, SequenceSummary>();
private sequenceDebounce: number = 250;

constructor(private eventManager: EventManager, @Inject(DOCUMENT) private document: Document) {}
constructor(
private eventManager: EventManager,
@Inject(DOCUMENT) private document: Document,
) {}

getHotkeys(): Hotkey[] {
const sequenceKeys = Array.from(this.sequenceMaps.values())
.map(s => [s.hotkeyMap].reduce((_acc, val) => [...val.values()], []))
.map((s) => [s.hotkeyMap].reduce((_acc, val) => [...val.values()], []))
.reduce((_x, y) => y, [])
.map(h => h.hotkey);
.map((h) => h.hotkey);

return Array.from(this.hotkeys.values()).concat(sequenceKeys);
}
Expand All @@ -74,7 +77,7 @@ export class HotkeysService {
continue;
}

let group = groups.find(g => g.group === hotkey.group);
let group = groups.find((g) => g.group === hotkey.group);
if (!group) {
group = { group: hotkey.group, hotkeys: [] };
groups.push(group);
Expand All @@ -92,10 +95,10 @@ export class HotkeysService {
let sequence = '';
return fromEvent<KeyboardEvent>(element, eventName).pipe(
tap(
e =>
(e) =>
(sequence = `${sequence}${sequence ? '>' : ''}${e.ctrlKey ? 'control.' : ''}${e.altKey ? 'alt.' : ''}${
e.shiftKey ? 'shift.' : ''
}${e.key}`)
}${e.key}`),
),
debounceTime(this.sequenceDebounce),
mergeMap(() => {
Expand All @@ -109,7 +112,7 @@ export class HotkeysService {
} else {
return EMPTY;
}
})
}),
);
};

Expand All @@ -119,7 +122,7 @@ export class HotkeysService {
const getSequenceCompleteObserver = (): Observable<Hotkey> => {
const hotkeySummary = {
subject: new Subject<Hotkey>(),
hotkey: mergedOptions
hotkey: mergedOptions,
};

if (this.sequenceMaps.has(mergedOptions.element)) {
Expand All @@ -144,10 +147,10 @@ export class HotkeysService {
};

return getSequenceCompleteObserver().pipe(
takeUntil<Hotkey>(this.dispose.pipe(filter(v => v === normalizedKeys))),
filter(hotkey => !this.targetIsExcluded(hotkey.allowIn)),
tap(hotkey => this.callbacks.forEach(cb => cb(hotkey, normalizedKeys, hotkey.element))),
finalize(() => this.removeShortcuts(normalizedKeys))
takeUntil<Hotkey>(this.dispose.pipe(filter((v) => v === normalizedKeys))),
filter((hotkey) => !this.targetIsExcluded(hotkey.allowIn)),
tap((hotkey) => this.callbacks.forEach((cb) => cb(hotkey, normalizedKeys, hotkey.element))),
finalize(() => this.removeShortcuts(normalizedKeys)),
);
}

Expand All @@ -163,7 +166,7 @@ export class HotkeysService {
this.hotkeys.set(normalizedKeys, mergedOptions);
const event = `${mergedOptions.trigger}.${normalizedKeys}`;

return new Observable(observer => {
return new Observable((observer) => {
const handler = (e: KeyboardEvent) => {
const hotkey = this.hotkeys.get(normalizedKeys);
const skipShortcutTrigger = this.targetIsExcluded(hotkey.allowIn);
Expand All @@ -176,7 +179,7 @@ export class HotkeysService {
e.preventDefault();
}

this.callbacks.forEach(cb => cb(e, normalizedKeys, hotkey.element));
this.callbacks.forEach((cb) => cb(e, normalizedKeys, hotkey.element));
observer.next(e);
};
const dispose = this.eventManager.addEventListener(mergedOptions.element, event, handler);
Expand All @@ -185,12 +188,12 @@ export class HotkeysService {
this.hotkeys.delete(normalizedKeys);
dispose();
};
}).pipe(takeUntil<KeyboardEvent>(this.dispose.pipe(filter(v => v === normalizedKeys))));
}).pipe(takeUntil<KeyboardEvent>(this.dispose.pipe(filter((v) => v === normalizedKeys))));
}

removeShortcuts(hotkeys: string | string[]): void {
const coercedHotkeys = coerceArray(hotkeys).map(hotkey => normalizeKeys(hotkey, hostPlatform()));
coercedHotkeys.forEach(hotkey => {
const coercedHotkeys = coerceArray(hotkeys).map((hotkey) => normalizeKeys(hotkey, hostPlatform()));
coercedHotkeys.forEach((hotkey) => {
this.hotkeys.delete(hotkey);
this.dispose.next(hotkey);

Expand Down Expand Up @@ -218,19 +221,21 @@ export class HotkeysService {
onShortcut(callback: HotkeyCallback): () => void {
this.callbacks.push(callback);

return () => (this.callbacks = this.callbacks.filter(cb => cb !== callback));
return () => (this.callbacks = this.callbacks.filter((cb) => cb !== callback));
}

registerHelpModal(openHelpModalFn: () => void, helpShortcut: string = '') {
this.addShortcut({ keys: helpShortcut || 'shift.?', showInHelpMenu: false, preventDefault: false }).subscribe(e => {
const skipMenu =
/^(input|textarea|select)$/i.test(document.activeElement.nodeName) ||
(e.target as HTMLElement).isContentEditable;

if (!skipMenu && this.hotkeys.size) {
openHelpModalFn();
}
});
this.addShortcut({ keys: helpShortcut || 'shift.?', showInHelpMenu: false, preventDefault: false }).subscribe(
(e) => {
const skipMenu =
/^(input|textarea|select)$/i.test(document.activeElement.nodeName) ||
(e.target as HTMLElement).isContentEditable;

if (!skipMenu && this.hotkeys.size) {
openHelpModalFn();
}
},
);
}

private targetIsExcluded(allowIn?: AllowInElement[]) {
Expand Down
3 changes: 1 addition & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import { bootstrapApplication, BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
import { HotkeysService } from '@ngneat/hotkeys';

if (environment.production) {
enableProdMode();
}

bootstrapApplication(AppComponent, {
providers: [importProvidersFrom(BrowserModule, BrowserAnimationsModule, NgbModalModule), HotkeysService],
providers: [importProvidersFrom(BrowserModule, BrowserAnimationsModule, NgbModalModule)],
}).catch((err) => console.error(err));

0 comments on commit 5323d0c

Please sign in to comment.