diff --git a/src/ui/signinBanner/signinBanner.components.ts b/src/ui/signinBanner/signinBanner.components.ts index 86d4ea70180a3ebfe0c76b2e4ddd093da0ce9aa3..524c174d1cdfe97166337266eef2359783c9786f 100644 --- a/src/ui/signinBanner/signinBanner.components.ts +++ b/src/ui/signinBanner/signinBanner.components.ts @@ -103,15 +103,16 @@ export class SigninBanner { private keyListenerConfigBase = { type: 'keydown', stop: true, - prevent: true, target: 'document', } public keyListenerConfig = [{ key: 'h', + capture: true, ...this.keyListenerConfigBase, }, { key: 'H', + capture: true, ...this.keyListenerConfigBase, }, { key: '?', diff --git a/src/util/directives/keyDownListener.directive.ts b/src/util/directives/keyDownListener.directive.ts index 9cae5845bcbb408312d9bed42b70cfd0dcf1f571..f3fc055d1ffbb171c5ae2b9a72fa6af3ef12bb80 100644 --- a/src/util/directives/keyDownListener.directive.ts +++ b/src/util/directives/keyDownListener.directive.ts @@ -1,12 +1,12 @@ -import { Directive, EventEmitter, HostListener, Input, Output } from "@angular/core"; - -const getFilterFn = (ev: KeyboardEvent, isDocument: boolean) => ({ type, key, target }: KeyListenerConfig): boolean => type === ev.type && ev.key === key && (target === 'document') === isDocument +import { Directive, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, Output } from "@angular/core"; +import { DOCUMENT } from '@angular/common' +import { fromEvent, merge, Subscription } from "rxjs"; @Directive({ selector: '[iav-key-listener]', }) -export class KeyListner { +export class KeyListner implements OnChanges, OnDestroy{ @Input('iav-key-listener') public keydownConfig: KeyListenerConfig[] = [] @@ -19,64 +19,72 @@ export class KeyListner { return (tagName === 'SELECT' || tagName === 'INPUT' || tagName === 'TEXTAREA') } - @HostListener('keydown', ['$event']) - public keydown(ev: KeyboardEvent) { - this.handleSelfListener(ev) - } - - @HostListener('document:keydown', ['$event']) - public documentKeydown(ev: KeyboardEvent) { - this.handleDocumentListener(ev) + private subs: Subscription[] = [] + + ngOnChanges(){ + if (this.keydownConfig) { + this.ngOnDestroy() + + const documentEv = this.keydownConfig.filter(c => c.target === 'document') + const documentCaptureEv = documentEv.filter(c => c.capture) + const documentBubbleEv = documentEv.filter(c => !c.capture) + + const selfEv = this.keydownConfig.filter(c => c.target !== 'document') + const selfCaptureEv = selfEv.filter(c => c.capture) + const selfBubbleEv = selfEv.filter(c => !c.capture) + + this.subs.push( + merge( + fromEvent(this.document, 'keydown', { capture: true }), + fromEvent(this.document, 'keyup', { capture: true }) + ).subscribe(this.getEvHandler(documentCaptureEv).bind(this)), + + merge( + fromEvent(this.document, 'keydown'), + fromEvent(this.document, 'keyup') + ).subscribe(this.getEvHandler(documentBubbleEv).bind(this)), + + merge( + fromEvent(this.el.nativeElement, 'keydown', { capture: true }), + fromEvent(this.el.nativeElement, 'keyup', { capture: true }) + ).subscribe(this.getEvHandler(selfCaptureEv).bind(this)), + + merge( + fromEvent(this.el.nativeElement, 'keydown'), + fromEvent(this.el.nativeElement, 'keyup') + ).subscribe(this.getEvHandler(selfBubbleEv).bind(this)), + ) + } } - @HostListener('keyup', ['$event']) - public keyup(ev: KeyboardEvent) { - this.handleSelfListener(ev) + ngOnDestroy(){ + while(this.subs.length > 0) this.subs.pop().unsubscribe() } - @HostListener('document:keyup', ['$event']) - public documentKeyup(ev: KeyboardEvent) { - this.handleDocumentListener(ev) - } + constructor( + private el: ElementRef, + @Inject(DOCUMENT) private document + ){ - private handleSelfListener(ev: KeyboardEvent) { - if (!this.keydownConfig) { return } - if (this.isTextField(ev)) { return } - - const filteredConfig = this.keydownConfig - .filter(getFilterFn(ev, false)) - .map(config => { - return { - config, - ev, - } - }) - this.emitEv(filteredConfig) } - private handleDocumentListener(ev: KeyboardEvent) { - if (!this.keydownConfig) { return } - if (this.isTextField(ev)) { return } - - const filteredConfig = this.keydownConfig - .filter(getFilterFn(ev, true)) - .map(config => { - return { - config, - ev, - } - }) - this.emitEv(filteredConfig) + private getEvHandler(cfgs: KeyListenerConfig[]){ + return (ev: KeyboardEvent) => { + if (this.isTextField(ev)) return + const filteredCfgs = cfgs.filter(c => c.key === ev.key && c.type === ev.type) + for (const cfg of filteredCfgs) { + if (cfg.stop) ev.stopPropagation() + } + this.emitEv(filteredCfgs.map(cfg => ({ + config: cfg, + ev + }))) + } } private emitEv(items: Array<{config: KeyListenerConfig, ev: KeyboardEvent}>) { for (const item of items) { const { config, ev } = item as {config: KeyListenerConfig, ev: KeyboardEvent} - - const { stop, prevent } = config - if (stop) { ev.stopPropagation() } - if (prevent) { ev.preventDefault() } - this.keyEvent.emit({ config, ev, }) @@ -91,6 +99,8 @@ export interface KeyListenerConfig { type: 'keydown' | 'keyup' key: string target?: 'document' + capture?: boolean stop: boolean - prevent: boolean + // fromEvent seems to be a passive listener, wheather or not { passive: false } flag is set or not + // so preventDefault cannot be called anyway }