Skip to content

Commit

Permalink
feat: support for sequence hotkeys added
Browse files Browse the repository at this point in the history
  • Loading branch information
JH committed Mar 7, 2022
1 parent 6efe837 commit fcdcdbd
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 9 deletions.
8 changes: 6 additions & 2 deletions projects/ngneat/hotkeys/src/lib/hotkeys-shortcut.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';

import { hostPlatform } from './utils/platform';

const symbols = {
Expand Down Expand Up @@ -36,15 +37,18 @@ export class HotkeysShortcutPipe implements PipeTransform {
this.symbols = this.getPlatformSymbols(platform);
}

transform(value: string, separator = ' + '): any {
transform(value: string, dotSeparator = ' + ', thenSeparator = ' then '): any {
if (!value) {
return '';
}
return value
.split('>')
.join(thenSeparator)
.split('.')
.map(c => c.toLowerCase())
.map(c => this.symbols[c] || c)
.join(separator);
.join(dotSeparator)
.split('>');
}

private getPlatformSymbols(platform): any {
Expand Down
11 changes: 7 additions & 4 deletions projects/ngneat/hotkeys/src/lib/hotkeys.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ export class HotkeysDirective implements OnChanges, OnDestroy {
constructor(private hotkeysService: HotkeysService, private elementRef: ElementRef) {}

@Input() hotkeys: string;
@Input() isSequence: boolean = false;
@Input() hotkeysGroup: string;
@Input() hotkeysOptions: Partial<Options> = {};
@Input() hotkeysDescription: string;

@Output()
hotkey = new EventEmitter<KeyboardEvent>();
hotkey = new EventEmitter<KeyboardEvent | Hotkey>();

ngOnChanges(changes: SimpleChanges): void {
this.deleteHotkeys();
Expand Down Expand Up @@ -58,9 +59,11 @@ export class HotkeysDirective implements OnChanges, OnDestroy {
private setHotkeys(hotkeys: Hotkey | Hotkey[]) {
const coercedHotkeys = coerceArray(hotkeys);
this.subscription = merge(
coercedHotkeys.map(hotkey =>
this.hotkeysService.addShortcut({ ...hotkey, element: this.elementRef.nativeElement })
)
coercedHotkeys.map(hotkey => {
return this.isSequence
? this.hotkeysService.addSequenceShortcut({ ...hotkey, element: this.elementRef.nativeElement })
: this.hotkeysService.addShortcut({ ...hotkey, element: this.elementRef.nativeElement });
})
)
.pipe(mergeAll())
.subscribe(e => this.hotkey.next(e));
Expand Down
104 changes: 101 additions & 3 deletions projects/ngneat/hotkeys/src/lib/hotkeys.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable } from '@angular/core';
import { EventManager } from '@angular/platform-browser';
import { Observable, of, Subject } from 'rxjs';
import { EMPTY, fromEvent, Observable, of, Subject, Subscription } from 'rxjs';
import { debounceTime, filter, mergeMap, takeUntil, tap } from 'rxjs/operators';

import { hostPlatform, normalizeKeys } from './utils/platform';
import { coerceArray } from './utils/array';
import { filter, takeUntil } from 'rxjs/operators';
import { hostPlatform, normalizeKeys } from './utils/platform';

export type AllowInElement = 'INPUT' | 'TEXTAREA' | 'SELECT';
interface Options {
Expand All @@ -26,6 +26,17 @@ export interface HotkeyGroup {
export type Hotkey = Partial<Options> & { keys: string };
export type HotkeyCallback = (event: KeyboardEvent, keys: string, target: HTMLElement) => void;

interface HotkeySummary {
hotkey: Hotkey;
subject: Subject<Hotkey>;
}

interface SequenceSummary {
subscription: Subscription;
observer: Observable<Hotkey>;
hotkeyMap: Map<string, HotkeySummary>;
}

@Injectable({ providedIn: 'root' })
export class HotkeysService {
private readonly hotkeys = new Map<string, Hotkey>();
Expand All @@ -40,6 +51,8 @@ export class HotkeysService {
preventDefault: true
};
private callbacks: HotkeyCallback[] = [];
private sequenceMaps = new Map<HTMLElement, SequenceSummary>();
private sequenceDebounce: number = 250;

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

Expand All @@ -50,6 +63,10 @@ export class HotkeysService {
getShortcuts(): HotkeyGroup[] {
const hotkeys = Array.from(this.hotkeys.values());
const groups: HotkeyGroup[] = [];
const sequenceKeys = Array.from(this.sequenceMaps.values())
.map(s => [s.hotkeyMap].reduce((_acc, val) => [...val.values()], []))
.reduce((_x, y) => y, [])
.map(h => h.hotkey);

for (const hotkey of hotkeys) {
if (!hotkey.showInHelpMenu) {
Expand All @@ -66,9 +83,79 @@ export class HotkeysService {
group.hotkeys.push({ keys: normalizedKeys, description: hotkey.description });
}

for (const hotkey of sequenceKeys) {
if (!hotkey.showInHelpMenu) {
continue;
}

let group = groups.find(g => g.group === hotkey.group);
if (!group) {
group = { group: hotkey.group, hotkeys: [] };
groups.push(group);
}

const normalizedKeys = normalizeKeys(hotkey.keys, hostPlatform());
group.hotkeys.push({ keys: normalizedKeys, description: hotkey.description });
}

return groups;
}

addSequenceShortcut(options: Hotkey): Observable<Hotkey> {
const getObserver = (element: HTMLElement, eventName: string) => {
let sequence = '';
return fromEvent<KeyboardEvent>(element, eventName).pipe(
tap(e => (sequence = `${sequence}${sequence ? '>' : ''}${e.key}`)),
debounceTime(this.sequenceDebounce),
mergeMap(() => {
const resultSequence = sequence;
sequence = '';
const summary = this.sequenceMaps.get(element);
if (summary.hotkeyMap.has(resultSequence)) {
const hotkeySummary = summary.hotkeyMap.get(resultSequence);
hotkeySummary.subject.next(hotkeySummary.hotkey);
return of(hotkeySummary.hotkey);
} else {
return EMPTY;
}
})
);
};

const mergedOptions = { ...this.defaults, ...options };
let normalizedKeys = normalizeKeys(mergedOptions.keys, hostPlatform());

if (this.sequenceMaps.has(mergedOptions.element)) {
const sequenceSummary = this.sequenceMaps.get(mergedOptions.element);

if (sequenceSummary.hotkeyMap.has(normalizedKeys)) {
console.error('Duplicated shortcut');
return of(null);
}

const hotkeySummary = {
subject: new Subject<Hotkey>(),
hotkey: mergedOptions
};

sequenceSummary.hotkeyMap.set(normalizedKeys, hotkeySummary);
return hotkeySummary.subject.asObservable();
} else {
const observer = getObserver(mergedOptions.element, mergedOptions.trigger);
const subscription = observer.subscribe();

const hotkeySummary = {
subject: new Subject<Hotkey>(),
hotkey: mergedOptions
};
const hotkeyMap = new Map<string, HotkeySummary>([[normalizedKeys, hotkeySummary]]);
const sequenceSummary = { subscription, observer, hotkeyMap };
this.sequenceMaps.set(mergedOptions.element, sequenceSummary);

return hotkeySummary.subject.asObservable();
}
}

addShortcut(options: Hotkey): Observable<KeyboardEvent> {
const mergedOptions = { ...this.defaults, ...options };
const normalizedKeys = normalizeKeys(mergedOptions.keys, hostPlatform());
Expand Down Expand Up @@ -112,9 +199,20 @@ export class HotkeysService {
coercedHotkeys.forEach(hotkey => {
this.hotkeys.delete(hotkey);
this.dispose.next(hotkey);

this.sequenceMaps.forEach(v => {
v.hotkeyMap.delete(hotkey);
if (v.hotkeyMap.size === 0) {
v.subscription.unsubscribe();
}
});
});
}

setSequenceDebounce(debounce: number): void {
this.sequenceDebounce = debounce;
}

onShortcut(callback: HotkeyCallback): () => void {
this.callbacks.push(callback);

Expand Down
11 changes: 11 additions & 0 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,15 @@
hotkeysGroup="Source code browsing"
[hotkeysOptions]="{ allowIn: ['input'] }"
/>

<input
#input3
placeholder="m>t"
hotkeys="m>t"
(hotkey)="handleHotkey($event)"
hotkeysDescription="Directive Test"
hotkeysGroup="Sequencing"
[hotkeysOptions]="{ allowIn: ['input'] }"
[isSequence]="true"
/>
</div>
13 changes: 13 additions & 0 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export class AppComponent implements AfterViewInit {

this.hotkeys.registerHelpModal(helpFcn);

this.hotkeys
.addSequenceShortcut({
keys: 'g>t',
description: 'In Code Test',
preventDefault: false,
group: 'Sequencing'
})
.subscribe(e => {
console.log('Test Sequence:', e);

this.hotkeys.removeShortcuts('g>t');
});

this.hotkeys
.addShortcut({
keys: 'meta.g',
Expand Down

0 comments on commit fcdcdbd

Please sign in to comment.