diff --git a/projects/ngneat/hotkeys/src/lib/hotkeys.service.ts b/projects/ngneat/hotkeys/src/lib/hotkeys.service.ts index 6c2e890..677b674 100644 --- a/projects/ngneat/hotkeys/src/lib/hotkeys.service.ts +++ b/projects/ngneat/hotkeys/src/lib/hotkeys.service.ts @@ -7,7 +7,7 @@ import { debounceTime, filter, finalize, mergeMap, takeUntil, tap } from 'rxjs/o import { coerceArray } from './utils/array'; import { hostPlatform, normalizeKeys } from './utils/platform'; -export type AllowInElement = 'INPUT' | 'TEXTAREA' | 'SELECT'; +export type AllowInElement = 'INPUT' | 'TEXTAREA' | 'SELECT' | 'CONTENTEDITABLE'; interface Options { group: string; element: HTMLElement; @@ -54,7 +54,7 @@ export class HotkeysService { private sequenceMaps = new Map(); private sequenceDebounce: number = 250; - constructor(private eventManager: EventManager, @Inject(DOCUMENT) private document) {} + constructor(private eventManager: EventManager, @Inject(DOCUMENT) private document: Document) {} getHotkeys(): Hotkey[] { const sequenceKeys = Array.from(this.sequenceMaps.values()) @@ -145,10 +145,7 @@ export class HotkeysService { return getSequenceCompleteObserver().pipe( takeUntil(this.dispose.pipe(filter(v => v === normalizedKeys))), - filter(hotkey => { - const excludedTargets = this.getExcludedTargets(hotkey.allowIn || []); - return !excludedTargets?.includes(document.activeElement.nodeName); - }), + filter(hotkey => !this.targetIsExcluded(hotkey.allowIn)), tap(hotkey => this.callbacks.forEach(cb => cb(hotkey, normalizedKeys, hotkey.element))), finalize(() => this.removeShortcuts(normalizedKeys)) ); @@ -169,9 +166,8 @@ export class HotkeysService { return new Observable(observer => { const handler = (e: KeyboardEvent) => { const hotkey = this.hotkeys.get(normalizedKeys); - const excludedTargets = this.getExcludedTargets(hotkey.allowIn || []); + const skipShortcutTrigger = this.targetIsExcluded(hotkey.allowIn); - const skipShortcutTrigger = excludedTargets && excludedTargets.includes(document.activeElement.nodeName); if (skipShortcutTrigger) { return; } @@ -237,7 +233,21 @@ export class HotkeysService { }); } - private getExcludedTargets(allowIn: AllowInElement[]) { - return ['INPUT', 'SELECT', 'TEXTAREA'].filter(t => !allowIn.includes(t as AllowInElement)); + private targetIsExcluded(allowIn?: AllowInElement[]) { + const activeElement = this.document.activeElement; + const elementName = activeElement.nodeName; + const elementIsContentEditable = (activeElement as HTMLElement).isContentEditable; + let isExcluded = ['INPUT', 'SELECT', 'TEXTAREA'].includes(elementName) || elementIsContentEditable; + + if (isExcluded && allowIn?.length) { + for (let t of allowIn) { + if (activeElement.nodeName === t || (t === 'CONTENTEDITABLE' && elementIsContentEditable)) { + isExcluded = false; + break; + } + } + } + + return isExcluded; } } diff --git a/projects/ngneat/hotkeys/src/lib/tests/hotkeys.directive.spec.ts b/projects/ngneat/hotkeys/src/lib/tests/hotkeys.directive.spec.ts index 076b170..3fc0ce4 100644 --- a/projects/ngneat/hotkeys/src/lib/tests/hotkeys.directive.spec.ts +++ b/projects/ngneat/hotkeys/src/lib/tests/hotkeys.directive.spec.ts @@ -36,6 +36,30 @@ describe('Directive: Hotkeys', () => { expect(spyFcn).toHaveBeenCalled(); }); + it('should ignore hotkey when typing in a contentEditable element', () => { + const spyFcn = createSpy('subscribe', (...args) => {}); + spectator = createDirective(`
`); + spectator.output('hotkey').subscribe(spyFcn); + spyOnProperty(document.activeElement, 'nodeName', 'get').and.returnValue('DIV'); + spyOnProperty(document.activeElement as HTMLElement, 'isContentEditable', 'get').and.returnValue(true); + spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'a'); + spectator.fixture.detectChanges(); + expect(spyFcn).not.toHaveBeenCalled(); + }); + + it('should trigger hotkey when typing in a contentEditable element', () => { + const spyFcn = createSpy('subscribe', (...args) => {}); + spectator = createDirective( + `
` + ); + spectator.output('hotkey').subscribe(spyFcn); + spyOnProperty(document.activeElement, 'nodeName', 'get').and.returnValue('DIV'); + spyOnProperty(document.activeElement as HTMLElement, 'isContentEditable', 'get').and.returnValue(true); + spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'a'); + spectator.fixture.detectChanges(); + expect(spyFcn).toHaveBeenCalled(); + }); + it('should register hotkey', () => { spectator = createDirective(`
`); spectator.fixture.detectChanges(); @@ -148,6 +172,46 @@ describe('Directive: Sequence Hotkeys', () => { return run(); }); + it('should ignore hotkey when typing in a contentEditable element', () => { + const run = async () => { + // * Need to space out time to prevent other test keystrokes from interfering with sequence + await sleep(250); + const spyFcn = createSpy('subscribe', (...args) => {}); + spectator = createDirective(`
`); + spectator.output('hotkey').subscribe(spyFcn); + spyOnProperty(document.activeElement, 'nodeName', 'get').and.returnValue('DIV'); + spyOnProperty(document.activeElement as HTMLElement, 'isContentEditable', 'get').and.returnValue(true); + spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'g', spectator.element); + spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'n', spectator.element); + await sleep(250); + spectator.fixture.detectChanges(); + expect(spyFcn).not.toHaveBeenCalled(); + }; + + return run(); + }); + + it('should trigger hotkey when typing in a contentEditable element', () => { + const run = async () => { + // * Need to space out time to prevent other test keystrokes from interfering with sequence + await sleep(250); + const spyFcn = createSpy('subscribe', (...args) => {}); + spectator = createDirective( + `
` + ); + spectator.output('hotkey').subscribe(spyFcn); + spyOnProperty(document.activeElement, 'nodeName', 'get').and.returnValue('DIV'); + spyOnProperty(document.activeElement as HTMLElement, 'isContentEditable', 'get').and.returnValue(true); + spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'g', spectator.element); + spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'n', spectator.element); + await sleep(250); + spectator.fixture.detectChanges(); + expect(spyFcn).toHaveBeenCalled(); + }; + + return run(); + }); + it('should register hotkey', () => { spectator = createDirective(`
`); spectator.fixture.detectChanges();