import { Injectable, OnDestroy } from '@angular/core'; import { codes, modifiers } from './keys'; import {Subscription, Observable, Subject, fromEvent, timer, of} from 'rxjs'; import { ShortcutEventOutput, ParsedShortcut, ShortcutInput } from './types'; import { map, filter, tap, debounce, catchError } from 'rxjs/operators'; import { allPass, any, difference, identity, isFunction, isNill } from './utils'; const $$ngOnDestroy = Symbol('OnDestroy'); @Injectable({ providedIn: 'root' }) export class ShortcutsService implements OnDestroy { /** * 解析快捷键 * 并为每个KEY 创建一个函数 */ private _shortcuts: ParsedShortcut[] = []; /** * 控制按键时间. */ private throttleTime = 0; private _pressed = new Subject(); /** * Streams of pressed events, can be used instead or with a command. */ public pressed$ = this._pressed.asObservable(); /** * 禁用快捷键 */ private disabled = false; private _ignored = ['INPUT', 'TEXTAREA', 'SELECT']; /** * Subscription for on destroy. */ private readonly subscription: Subscription; private keydown$; private isAllowed = (shortcut: ParsedShortcut) => { const target = shortcut.event.target as HTMLElement; if (target === shortcut.target) { return true; } if (shortcut.allowIn.length) { return !(difference(this._ignored, shortcut.allowIn)).includes(target.nodeName); } return !(this._ignored).includes(target.nodeName); } private mapEvent = event => this._shortcuts .map(shortcut => Object.assign({}, shortcut, { predicates: any( identity, shortcut.predicates.map((predicates: any) => allPass(predicates)(event)) ), event: event }) ) .filter(shortcut => shortcut.predicates) .reduce((acc, shortcut) => (acc.priority > shortcut.priority ? acc : shortcut), { priority: 0 } as ParsedShortcut) private get shortcuts() { return this._shortcuts; } constructor() { // this.keydown$ = fromEvent(document, 'keydown').pipe( // filter(_ => !this.disabled), // map(this.mapEvent), // filter( // (shortcut: ParsedShortcut) => // !shortcut.target || shortcut.event.target === shortcut.target // ), // filter((shortcut: ParsedShortcut) => isFunction(shortcut.command)), // filter(this.isAllowed), // tap((shortcut: any) => !shortcut.preventDefault || shortcut.event.preventDefault()), // debounce(shortcut => timer(shortcut.throttleTime)), // tap(shortcut => shortcut.command({ event: shortcut.event, key: shortcut.key })), // tap(shortcut => this._pressed.next({ event: shortcut.event, key: shortcut.key })), // catchError(error => of(error)) // ); // this.subscription = this.keydown$.subscribe(); } /** * Remove subscription. */ ngOnDestroy(): void { if (this.subscription) { this.subscription.unsubscribe(); } } /** * Enable all keyboard shortcuts */ enable(): ShortcutsService { this.disabled = false; return this; } /** * Disable all keyboard shortcuts */ disable(): ShortcutsService { this.disabled = true; return this; } /** * Check if all keyboard shortcuts are disabled. */ isDisabled() { return this.disabled; } /** * Add new shortcut/s */ public add(shortcuts: ShortcutInput[] | ShortcutInput, instance?: any): ShortcutsService { shortcuts = Array.isArray(shortcuts) ? shortcuts : [shortcuts]; if (instance) { const [key] = [...shortcuts.map(shortcut => shortcut.key)]; this.bindOnDestroy(instance, key); } this._shortcuts.push(...this.parseCommand(shortcuts)); return this; } /** * bind to the component ngOnDestroy to remove related keys * when component is destroyed. * @param instance - component to remove keys when ngOnDestroy is called. * @param keys */ private bindOnDestroy(instance: any, keys: string | string[]): ShortcutsService { if (instance.ngOnDestroy) { instance[$$ngOnDestroy] = instance.ngOnDestroy; } const that = this; instance.ngOnDestroy = function() { const onDestroy = instance[$$ngOnDestroy]; if (onDestroy) { onDestroy.apply(this); } that.remove(keys); }; return this; } /** * Remove a command based on key or array of keys. * can be used for cleanup. * @param key * @returns */ public remove(key: string | string[]): ShortcutsService { const keys: string[] = Array.isArray(key) ? key : [key]; this._shortcuts = this._shortcuts.filter(shortcut => { return !shortcut.key.find(sKey => { return keys.filter(k => k === sKey).length > 0; }); }); return this; } /** * Returns an observable of keyboard shortcut filtered by a specific key. * @param key - the key to filter the observable by. */ public select(key: string): Observable { return this.pressed$.pipe( filter(({event, key: eventKeys}) => { return !!eventKeys.find(eventKey => eventKey === key); }) ); } /** * transforms a shortcut to: * a predicate function */ private getKeys = (command: string[]) => command.map(key => key.trim()).filter(key => key !== '+') .map(key => { // for modifiers like control key // look for event['ctrlKey'] // otherwise use the keyCode if (modifiers.hasOwnProperty(key)) { return event => !!event[modifiers[key]]; } return event => codes[key] ? event.keyCode === codes[key] || event.key === key : event.keyCode === key.toUpperCase().charCodeAt(0); }) /** * Parse each command using getKeys function */ private parseCommand(command: ShortcutInput | ShortcutInput[]): ParsedShortcut[] { const commands = Array.isArray(command) ? command : [command]; return commands.map( (cmd: ShortcutInput) => { const keys = Array.isArray(cmd.key) ? cmd.key : [cmd.key]; const priority = Math.max(...keys.map(key => key.split(' ').length)); const predicates = keys.map(key => this.getKeys(key.split(' '))); return { ...cmd, allowIn: cmd.allowIn || [], key: keys, throttle: isNill(cmd.throttleTime) ? this.throttleTime : cmd.throttleTime, priority: priority, predicates: predicates } as ParsedShortcut; }); } }