UNPKG

ng-hotkeys

Version:

ng-hotkeys for Angular 14+

1 lines 107 kB
{"version":3,"file":"ng-hotkeys.mjs","sources":["../../../projects/ng-hotkeys/src/lib/utils.ts","../../../projects/ng-hotkeys/src/lib/keys.ts","../../../projects/ng-hotkeys/src/lib/ng-hotkeys.service.ts","../../../projects/ng-hotkeys/src/lib/ng-hotkeys.component.ts","../../../projects/ng-hotkeys/src/lib/ng-hotkeys-help.service.ts","../../../projects/ng-hotkeys/src/lib/ng-hotkeys-select.service.ts","../../../projects/ng-hotkeys/src/lib/ng-hotkeys.interfaces.ts","../../../projects/ng-hotkeys/src/lib/ng-hotkeys.directive.ts","../../../projects/ng-hotkeys/src/lib/portal.ts","../../../projects/ng-hotkeys/src/lib/dom-portal-outlet.ts","../../../projects/ng-hotkeys/src/lib/ng-hotkeys-help-item.component.html","../../../projects/ng-hotkeys/src/lib/ng-hotkeys-help-item.component.ts","../../../projects/ng-hotkeys/src/lib/ng-hotkeys-help.component.html","../../../projects/ng-hotkeys/src/lib/ng-hotkeys-help.component.ts","../../../projects/ng-hotkeys/src/lib/ng-hotkeys.module.ts","../../../projects/ng-hotkeys/src/lib/polyfills.ts","../../../projects/ng-hotkeys/src/public_api.ts","../../../projects/ng-hotkeys/src/ng-hotkeys.ts"],"sourcesContent":["export function isFunction(x: any): x is Function {\n return typeof x === \"function\";\n}\n\nexport const invert = obj => {\n const new_obj = {};\n\n for (const prop in obj) {\n if (obj.hasOwnProperty(prop)) {\n new_obj[obj[prop]] = prop;\n }\n }\n return new_obj;\n};\n\nexport const any = (fn: Function, list: any[]) => {\n let idx = 0;\n while (idx < list.length) {\n if (fn(list[idx])) {\n return true;\n }\n idx += 1;\n }\n return false;\n};\n\nexport const identity = x => x;\n\n/**\n * @ignore\n * @param x\n * @returns boolean\n */\nexport const isNill = x => x == null;\n\n/**\n * @ignore\n * @param xs\n * @param key\n * @returns any\n */\nexport const groupBy = (xs, key) =>\n xs.reduce(\n (result, x) => ({\n ...result,\n [x[key]]: [...(result[x[key]] || []), x]\n }),\n {}\n );\n\n/**\n * @ignore\n * @param first\n * @param second\n * @returns any[]\n */\nexport const difference = (first: any[], second: any[]) =>\n first.filter(item => !second.includes(item));\n\n/**\n * @ignore\n * @param preds\n * @returns (...args) => boolean;\n */\nexport const allPass = preds => (...args) => {\n let idx = 0;\n const len = preds.length;\n while (idx < len) {\n if (!preds[idx].apply(this, args)) {\n return false;\n }\n idx += 1;\n }\n return true;\n};\nexport const prop = prop => object => object[prop];\n\nconst minMaxArrayProp = type => (property, array) =>\n Math[type].apply(Math, array.map(prop(property)));\n\nexport const maxArrayProp = (property, array) => {\n return array.reduce(\n (acc, curr) => {\n const propFn = prop(property);\n const currentValue = propFn(curr);\n const previousValue = propFn(acc);\n return currentValue > previousValue ? curr : acc;\n },\n { [property]: 0 }\n );\n};\n","import { invert } from './utils';\n\nconst isMac = typeof navigator !== \"undefined\" ? navigator.userAgent.includes(\"Mac OS\") : false;\n\nexport const modifiers = {\n shift: \"shiftKey\",\n ctrl: \"ctrlKey\",\n alt: \"altKey\",\n cmd: isMac ? \"metaKey\" : \"ctrlKey\",\n command: isMac ? \"metaKey\" : \"ctrlKey\",\n meta: isMac ? \"metaKey\" : \"ctrlKey\",\n \"left command\": \"metaKey\",\n \"right command\": \"MetaRight\",\n \"⌘\": isMac ? \"metaKey\" : \"ctrlKey\",\n option: \"altKey\",\n ctl: \"ctrlKey\",\n control: \"ctrlKey\"\n};\nexport const _SPECIAL_CASES = {\n plus: \"+\"\n};\n\nexport const symbols = {\n cmd: isMac ? \"⌘\" : \"Ctrl\",\n command: isMac ? \"⌘\" : \"Ctrl\",\n \"left command\": isMac ? \"⌘\" : \"Ctrl\",\n \"right command\": isMac ? \"⌘\" : \"Ctrl\",\n option: isMac ? \"⌥\" : \"Alt\",\n plus: \"+\",\n left: \"←\",\n right: \"→\",\n up: \"↑\",\n down: \"↓\",\n alt: isMac ? \"⌥\" : \"Alt\",\n ctrl: \"Ctrl\",\n control: \"Ctrl\",\n shift: \"⇧\"\n};\n\nexport const _MAP = {\n 8: \"backspace\",\n 9: \"tab\",\n 13: \"enter\",\n 16: \"shift\",\n 17: [\"ctrl\", \"control\"],\n 18: \"alt\",\n 20: \"capslock\",\n 27: [\"esc\", \"escape\"],\n 32: [\"space\", \"spc\"],\n 33: \"pageup\",\n 34: \"pagedown\",\n 35: \"end\",\n 36: \"home\",\n 37: \"left\",\n 38: \"up\",\n 39: \"right\",\n 40: \"down\",\n 45: \"ins\",\n 46: \"del\",\n 91: [\"meta\", \"cmd\", \"command\"],\n 93: [\"meta\", \"cmd\", \"command\"],\n 224: [\"meta\", \"cmd\", \"command\"]\n};\n\n/*\n * mapping for special characters so they can support\n *\n * this dictionary is only used incase you want to bind a\n * keyup or keydown event to one of these keys\n *\n */\nexport const _KEYCODE_MAP = {\n 106: \"*\",\n 107: \"+\",\n 109: \"-\",\n 110: \".\",\n 111: \"/\",\n 186: \";\",\n 187: \"=\",\n 188: \",\",\n 189: \"-\",\n 190: \".\",\n 191: \"/\",\n 192: \"`\",\n 219: \"[\",\n 220: \"\\\\\",\n 221: \"]\",\n 222: \"'\"\n};\n\n/**\n * this is a mapping of keys that require shift on a US keypad\n * back to the non shift equivelents\n *\n * this is so you can use keyup events with these keys\n *\n * note that this will only work reliably on US keyboards\n *\n */\nexport const _SHIFT_MAP = {\n \"`\": \"~\",\n \"1\": \"!\",\n \"2\": \"@\",\n \"3\": \"#\",\n \"4\": \"$\",\n \"5\": \"%\",\n \"6\": \"^\",\n \"7\": \"&\",\n \"8\": \"*\",\n \"9\": \"(\",\n \"0\": \")\",\n \"-\": \"_\",\n \"=\": \"+\",\n \";\": \":\",\n \"'\": '\"',\n \",\": \"<\",\n \".\": \">\",\n \"/\": \"?\",\n \"\\\\\": \"|\"\n};\nexport const _INVERTED_SHIFT_MAP = invert(_SHIFT_MAP);\n\n/**\n * loop through the f keys, f1 to f19 and add them to the map\n * programatically\n */\nfor (let i = 1; i < 20; ++i) {\n _MAP[111 + i] = \"f\" + i;\n}\n\n/**\n * loop through to map numbers on the numeric keypad\n */\nfor (let i = 0; i <= 9; ++i) {\n // This needs to use a string cause otherwise since 0 is falsey\n // event will never fire for numpad 0 pressed as part of a keydown\n // event.\n _MAP[i + 96] = i.toString();\n}\n","import { Inject, Injectable, OnDestroy } from \"@angular/core\";\nimport {\n _INVERTED_SHIFT_MAP,\n _KEYCODE_MAP,\n _MAP,\n _SHIFT_MAP,\n _SPECIAL_CASES,\n modifiers\n} from \"./keys\";\nimport {\n BehaviorSubject,\n fromEvent,\n Observable,\n Subject,\n Subscription,\n throwError,\n timer,\n of\n} from \"rxjs\";\nimport {\n ParsedShortcut,\n ShortcutEventOutput,\n ShortcutInput\n} from \"./ng-hotkeys.interfaces\";\nimport {\n catchError,\n filter,\n first,\n map,\n repeat,\n scan,\n switchMap,\n takeUntil,\n tap,\n throttle\n} from \"rxjs/operators\";\nimport { allPass, any, difference, identity, isFunction, isNill, maxArrayProp } from \"./utils\";\nimport { DOCUMENT } from \"@angular/common\";\n\n/**\n * @ignore\n * @type {number}\n */\nlet guid = 0;\n\n@Injectable({\n providedIn: \"root\"\n})\nexport class NgHotkeysService implements OnDestroy {\n /**\n * Parsed shortcuts\n * for each key create a predicate function\n */\n private _shortcuts: ParsedShortcut[] = [];\n\n private _sequences: ParsedShortcut[] = [];\n\n /**\n * Throttle the keypress event.\n */\n private throttleTime = 0;\n\n private _pressed = new Subject<ShortcutEventOutput>();\n\n /**\n * Streams of pressed events, can be used instead or with a command.\n */\n public pressed$ = this._pressed.asObservable();\n\n /**\n * Disable all keyboard shortcuts\n */\n private disabled = false;\n /**\n * @ignore\n * 2000 ms window to allow between key sequences otherwise\n * the sequence will reset.\n */\n private static readonly TIMEOUT_SEQUENCE = 1000;\n\n private _shortcutsSub = new BehaviorSubject<ParsedShortcut[]>([]);\n public shortcuts$ = this._shortcutsSub\n .asObservable()\n .pipe(filter(shortcuts => !!shortcuts.length));\n\n private _ignored = [\"INPUT\", \"TEXTAREA\", \"SELECT\"];\n\n /**\n * @ignore\n * Subscription for on destroy.\n */\n private readonly subscriptions: Subscription[] = [];\n\n /**\n * @ignore\n * @param shortcut\n */\n private isAllowed = (shortcut: ParsedShortcut) => {\n const target = shortcut.event.target as HTMLElement;\n if (target === shortcut.target) {\n return true;\n }\n if (shortcut.allowIn.length) {\n return !difference(this._ignored, shortcut.allowIn).includes(target.nodeName);\n }\n return !this._ignored.includes(target.nodeName);\n };\n\n /**\n * @ignore\n * @param event\n */\n private mapEvent = event => {\n return this._shortcuts\n .filter(shortcut => !shortcut.isSequence)\n .map(shortcut =>\n Object.assign({}, shortcut, {\n predicates: any(\n identity,\n shortcut.predicates.map((predicates: any) => allPass(predicates)(event))\n ),\n event: event\n })\n )\n .filter(shortcut => shortcut.predicates)\n .reduce((acc, shortcut) => (acc.priority > shortcut.priority ? acc : shortcut), {\n priority: 0\n } as ParsedShortcut);\n };\n\n private keydown$ = fromEvent(this.document, \"keydown\");\n /**\n * fixes for firefox prevent default\n * on click event on button focus:\n * see issue:\n * keeping this here for now, but it is commented out\n * Firefox reference bug:\n * https://bugzilla.mozilla.org/show_bug.cgi?id=1487102\n * and my repo:\n *\n * https://github.com/omridevk/ng-keyboard-shortcuts/issues/35\n */\n private ignore$ = this.pressed$.pipe(\n filter(e => e.event.defaultPrevented),\n switchMap(() => this.clicks$.pipe(first())),\n tap((e: any) => {\n e.preventDefault();\n e.stopPropagation();\n }),\n repeat()\n );\n /**\n * @ignore\n */\n private clicks$ = fromEvent(this.document, \"click\", { capture: true });\n\n private keyup$ = fromEvent(this.document, \"keyup\");\n\n /**\n * @ignore\n */\n private keydownCombo$ = this.keydown$.pipe(\n filter(_ => !this.disabled),\n map(this.mapEvent),\n filter(\n (shortcut: ParsedShortcut) =>\n !shortcut.target || shortcut.event.target === shortcut.target\n ),\n filter((shortcut: ParsedShortcut) => isFunction(shortcut.command)),\n filter(this.isAllowed),\n tap(shortcut => {\n if (!shortcut.preventDefault) {\n return;\n }\n shortcut.event.preventDefault();\n shortcut.event.stopPropagation();\n }),\n throttle(shortcut => timer(shortcut.throttleTime)),\n tap(shortcut => shortcut.command({ event: shortcut.event, key: shortcut.key })),\n tap(shortcut => this._pressed.next({ event: shortcut.event, key: shortcut.key })),\n takeUntil(this.keyup$),\n repeat(),\n catchError(error => throwError(error))\n );\n\n /**\n * @ignore\n */\n private timer$ = new Subject();\n /**\n * @ignore\n */\n private resetCounter$ = this.timer$\n .asObservable()\n .pipe(switchMap(() => timer(NgHotkeysService.TIMEOUT_SEQUENCE)));\n /**\n * @ignore\n */\n private keydownSequence$ = this.shortcuts$.pipe(\n map(shortcuts => shortcuts.filter(shortcut => shortcut.isSequence)),\n switchMap(sequences =>\n this.keydown$.pipe(\n map(event => {\n return {\n event,\n sequences\n };\n }),\n tap(val => this.timer$.next(val))\n )\n ),\n scan(\n (acc: { events: any[]; command?: any; sequences: any[] }, arg: any) => {\n let { event } = arg;\n const currentLength = acc.events.length;\n const sequences = currentLength ? acc.sequences : arg.sequences;\n let [characters] = this.characterFromEvent(event);\n characters = Array.isArray(characters)\n ? [...characters, event.key]\n : [characters, event.key];\n const result = sequences\n .map(sequence => {\n const sequences = sequence.sequence.filter(seque =>\n characters.some(\n key =>\n (_SPECIAL_CASES[seque[currentLength]] ||\n seque[currentLength]) === key\n )\n );\n const partialMatch = sequences.length > 0;\n if (sequence.fullMatch) {\n return sequence;\n }\n return {\n ...sequence,\n sequence: sequences,\n partialMatch,\n event: event,\n fullMatch:\n partialMatch &&\n this.isFullMatch({ command: sequence, events: acc.events })\n };\n })\n .filter(sequences => sequences.partialMatch || sequences.fullMatch);\n\n let [match] = result;\n if (!match || this.modifiersOn(event)) {\n return { events: [], sequences: this._sequences };\n }\n /*\n * handle case of \"?\" sequence and \"? a\" sequence\n * need to determine which one to trigger.\n * if both match, we pick the longer one (? a) in this case.\n */\n const guess = maxArrayProp(\"priority\", result);\n if (result.length > 1 && guess.fullMatch) {\n return { events: [], command: guess, sequences: this._sequences };\n }\n if (result.length > 1) {\n return { events: [...acc.events, event], command: result, sequences: result };\n }\n if (match.fullMatch) {\n return { events: [], command: match, sequences: this._sequences };\n }\n return { events: [...acc.events, event], command: result, sequences: result };\n },\n { events: [], sequences: [] }\n ),\n switchMap(({ command }) => {\n if (Array.isArray(command)) {\n /*\n * Add a timer to handle the case where for example:\n * a sequence \"?\" is registered and \"? a\" is registered as well\n * if the user does not hit any key for 500ms, the single sequence will trigger\n * if any keydown event occur, this timer will reset, given a chance to complete\n * the full sequence (? a) in this case.\n * This delay only occurs when single key sequence is the beginning of another sequence.\n */\n return timer(500).pipe(\n map(() => ({ command: command.filter(command => command.fullMatch)[0] }))\n );\n }\n return of({ command });\n }),\n takeUntil(this.pressed$),\n filter(({ command }) => command && command.fullMatch),\n map(({ command }) => command),\n filter((shortcut: ParsedShortcut) => isFunction(shortcut.command)),\n filter(\n (shortcut: ParsedShortcut) =>\n !shortcut.target || shortcut.event.target === shortcut.target\n ),\n filter(this.isAllowed),\n tap(shortcut => !shortcut.preventDefault || shortcut.event.preventDefault()),\n throttle(shortcut => timer(shortcut.throttleTime)),\n tap(shortcut => shortcut.command({ event: shortcut.event, key: shortcut.key })),\n tap(shortcut => this._pressed.next({ event: shortcut.event, key: shortcut.key })),\n takeUntil(this.resetCounter$),\n repeat()\n );\n\n /**\n * @ignore\n * @param command\n * @param events\n */\n private isFullMatch({ command, events }) {\n if (!command) {\n return false;\n }\n return command.sequence.some(sequence => {\n return sequence.length === events.length + 1;\n });\n }\n\n /**\n * @ignore\n */\n private get shortcuts() {\n return this._shortcuts;\n }\n\n /**\n * @ignore\n */\n constructor(@Inject(DOCUMENT) private document: any) {\n this.subscriptions.push(\n this.keydownSequence$.subscribe(),\n this.keydownCombo$.subscribe()\n // this.ignore$.subscribe()\n );\n }\n\n /**\n * @ignore\n * @param event\n */\n private _characterFromEvent(event): [string, boolean] {\n if (typeof event.which !== \"number\") {\n event.which = event.keyCode;\n }\n if (_SPECIAL_CASES[event.which]) {\n return [_SPECIAL_CASES[event.which], event.shiftKey];\n }\n if (_MAP[event.which]) {\n // for non keypress events the special maps are needed\n return [_MAP[event.which], event.shiftKey];\n }\n\n if (_KEYCODE_MAP[event.which]) {\n return [_KEYCODE_MAP[event.which], event.shiftKey];\n }\n // in case event key is lower case but registered key is upper case\n // return it in the lower case\n if (String.fromCharCode(event.which).toLowerCase() !== event.key) {\n return [String.fromCharCode(event.which).toLowerCase(), event.shiftKey];\n }\n return [event.key, event.shiftKey];\n }\n\n private characterFromEvent(event) {\n let [key, shiftKey] = this._characterFromEvent(event);\n if (shiftKey && _SHIFT_MAP[key]) {\n return [_SHIFT_MAP[key], shiftKey];\n }\n return [key, shiftKey];\n }\n\n /**\n * @ignore\n * Remove subscription.\n */\n ngOnDestroy(): void {\n this.subscriptions.forEach(sub => sub.unsubscribe());\n }\n\n /**\n * @ignore\n * @param shortcuts\n */\n private isSequence(shortcuts: string[]): boolean {\n return !shortcuts.some(shortcut => shortcut.includes(\"+\") || shortcut.length === 1);\n }\n\n /**\n * Add new shortcut/s\n */\n public add(shortcuts: ShortcutInput[] | ShortcutInput): string[] {\n shortcuts = Array.isArray(shortcuts) ? shortcuts : [shortcuts];\n const commands = this.parseCommand(shortcuts);\n commands.forEach(command => {\n if (command.isSequence) {\n this._sequences.push(command);\n return;\n }\n this._shortcuts.push(command);\n });\n setTimeout(() => {\n this._shortcutsSub.next([...this._shortcuts, ...this._sequences]);\n });\n return commands.map(command => command.id);\n }\n\n /**\n * Remove a command based on key or array of keys.\n * can be used for cleanup.\n * @returns\n * @param ids\n */\n public remove(ids: string | string[]): NgHotkeysService {\n ids = Array.isArray(ids) ? ids : [ids];\n this._shortcuts = this._shortcuts.filter(shortcut => !ids.includes(shortcut.id));\n this._sequences = this._sequences.filter(shortcut => !ids.includes(shortcut.id));\n setTimeout(() => {\n this._shortcutsSub.next([...this._shortcuts, ...this._sequences]);\n });\n return this;\n }\n\n /**\n * Returns an observable of keyboard shortcut filtered by a specific key.\n * @param key - the key to filter the observable by.\n */\n public select(key: string): Observable<ShortcutEventOutput> {\n return this.pressed$.pipe(\n filter(({ event, key: eventKeys }) => {\n eventKeys = Array.isArray(eventKeys) ? eventKeys : [eventKeys];\n return !!eventKeys.find(eventKey => eventKey === key);\n })\n );\n }\n\n /**\n * @ignore\n * transforms a shortcut to:\n * a predicate function\n */\n private getKeys = (keys: string[]) => {\n return keys\n .map(key => key.trim())\n .filter(key => key !== \"+\")\n .map(key => {\n // for modifiers like control key\n // look for event['ctrlKey']\n // otherwise use the keyCode\n key = _SPECIAL_CASES[key] || key;\n if (modifiers.hasOwnProperty(key)) {\n return event => {\n return !!event[modifiers[key]];\n };\n }\n\n return event => {\n const isUpper = key === key.toUpperCase();\n const isNonAlpha = (/[^a-zA-Z\\d\\s:]/).test(key);\n const inShiftMap = _INVERTED_SHIFT_MAP[key];\n let [characters, shiftKey] = this.characterFromEvent(event);\n const allModifiers = Object.keys(modifiers).map((key) => {\n return modifiers[key];\n })\n const hasModifiers = allModifiers.some(modifier => event[modifier]);\n characters = Array.isArray(characters)\n ? [...characters, event.key]\n : [characters, event.key];\n\n // if has modifiers:\n // we want to make sure it is not upper case letters\n // (because upper has modifiers so we want continue the check)\n // we also want to make sure it is not alphanumeric char like ? / ^ & and others (since those could require modifiers as well)\n // we also want to check this only if the length of\n // of the keys is one (i.e the command key is \"?\" or \"c\"\n // this while check is here to verify that:\n // if registered key like \"e\"\n // it won't be fired when clicking ctrl + e, or any modifiers + the key\n // we only want to trigger when the single key is clicked alone\n // thus all these edge cases.\n // hopefully this would cover all cases\n // TODO:: find a way simplify this\n if (hasModifiers\n && (!isUpper || isNonAlpha)\n && !inShiftMap\n && keys.length === 1) {\n return false;\n }\n return characters.some(char => {\n if (char === key && isUpper) {\n return true;\n }\n return key === char;\n });\n };\n });\n };\n\n\n /**\n * @ignore\n * @param event\n */\n private modifiersOn(event) {\n return [\"metaKey\", \"altKey\", \"ctrlKey\"].some(mod => event[mod]);\n }\n\n /**\n * @ignore\n * Parse each command using getKeys function\n */\n private parseCommand(command: ShortcutInput | ShortcutInput[]): ParsedShortcut[] {\n const commands = Array.isArray(command) ? command : [command];\n return commands.map(command => {\n const keys = Array.isArray(command.key) ? command.key : [command.key];\n const priority = Math.max(...keys.map(key => key.split(\" \").filter(identity).length));\n const predicates = keys.map(key => this.getKeys(key.split(\" \").filter(identity)));\n const isSequence = this.isSequence(keys);\n const sequence = isSequence\n ? keys.map(key =>\n key\n .split(\" \")\n .filter(identity)\n .map(key => key.trim())\n )\n : [];\n return {\n ...command,\n isSequence,\n sequence: isSequence ? sequence : [],\n allowIn: command.allowIn || [],\n key: keys,\n id: `${guid++}`,\n throttle: isNill(command.throttleTime) ? this.throttleTime : command.throttleTime,\n priority: priority,\n predicates: predicates\n } as ParsedShortcut;\n });\n }\n}\n","import {\n AfterViewInit,\n Component,\n Input,\n OnChanges,\n OnDestroy,\n OnInit,\n SimpleChanges\n} from \"@angular/core\";\nimport { NgHotkeysService } from './ng-hotkeys.service';\nimport { ShortcutInput, ShortcutEventOutput } from \"./ng-hotkeys.interfaces\";\nimport { Observable } from \"rxjs\";\n\n/**\n * A component to bind global shortcuts, can be used multiple times across the app\n * will remove registered shortcuts when element is removed from DOM.\n */\n@Component({\n selector: \"ng-hot-keys\",\n template: \"\"\n})\nexport class NgHotkeysComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {\n /**\n * A list of shortcuts.\n */\n @Input() shortcuts: ShortcutInput[] | ShortcutInput = [];\n\n /**\n * @ignore\n * list of registered keyboard shortcuts\n * used for clean up on NgDestroy.\n */\n private clearIds: string[] = [];\n\n /**\n * @ignore\n */\n private _disabled = false;\n /**\n * Disable all shortcuts for this component.\n */\n @Input() set disabled(value) {\n this._disabled = value;\n if (this.clearIds) {\n this.keyboard.remove(this.clearIds);\n this.clearIds = [];\n }\n if (value) {\n return;\n }\n this.clearIds = this.keyboard.add(this.shortcuts);\n }\n\n /**\n * @ignore\n * @param {NgHotkeysService} keyboard\n */\n constructor(private keyboard: NgHotkeysService) {}\n\n /**\n * @ignore\n */\n ngOnInit() {}\n\n /**\n * Select a key to listen to, will emit when the selected key is pressed.\n */\n public select(key: string): Observable<ShortcutEventOutput> {\n return this.keyboard.select(key);\n }\n\n /**\n * @ignore\n */\n ngAfterViewInit(): void {}\n\n /**\n * @ignore\n */\n ngOnChanges(changes: SimpleChanges): void {\n if (!changes.shortcuts || !changes.shortcuts.currentValue) {\n return;\n }\n if (this.clearIds) {\n this.keyboard.remove(this.clearIds);\n }\n if (!this._disabled) {\n setTimeout(() => (this.clearIds = this.keyboard.add(changes.shortcuts.currentValue)));\n }\n }\n\n /**\n * @ignore\n */\n ngOnDestroy(): void {\n this.keyboard.remove(this.clearIds);\n }\n}\n","import { Injectable } from \"@angular/core\";\nimport { NgHotkeysService } from './ng-hotkeys.service';\nimport { map } from \"rxjs/operators\";\n\n/**\n * Service to assist showing custom help screen\n */\n@Injectable({\n providedIn: \"root\"\n})\nexport class NgHotkeysHelpService {\n /**\n * @ignore\n * @param {NgHotkeysService} keyboard\n */\n constructor(private keyboard: NgHotkeysService) {}\n\n /**\n * Observable to provide access to all registered shortcuts in the app.\n * @type {Observable<any>}\n */\n public shortcuts$ = this.keyboard.shortcuts$.pipe(\n map(shortcuts =>\n shortcuts\n .filter(shortcut => Boolean(shortcut.label) && Boolean(shortcut.description))\n .map(({ key, label, description }) => ({\n key,\n label,\n description\n }))\n )\n );\n}\n","import { Injectable } from \"@angular/core\";\nimport { NgHotkeysService } from './ng-hotkeys.service';\n\n@Injectable({\n providedIn: \"root\"\n})\n/**\n * Use this service to listen to a specific keyboards events using Rxjs.\n * The shortcut must be declared in the app for the select to work.\n */\nexport class NgHotkeysSelectService {\n constructor(private keyboardService: NgHotkeysService) {}\n\n /**\n * Returns an observable of keyboard shortcut filtered by a specific key.\n * @param key - the key to filter the observable by.\n */\n public select(key: string) {\n return this.keyboardService.select(key);\n }\n}\n","/**\n * The shortcut input for the Directive\n */\nexport interface Shortcut {\n\n /**\n * A key or list of keys to listen to and fire the command.\n */\n key: string | string[];\n\n /**\n * callback to be called when shortcut is pressed.\n * @param event - the event out\n */\n command(event: ShortcutEventOutput): any;\n\n /**\n * Description for the command can be used for rendering help menu.\n */\n description?: string;\n\n /**\n * How much time to throttle in ms.\n */\n throttleTime?: number;\n\n /**\n * Label, can be used for grouping commands.\n */\n label?: string;\n\n /**\n * Prevent browser default, default: false\n */\n preventDefault?: boolean;\n}\n\n/**\n * The shortcut input type for ng-hotkeys component\n */\nexport interface ShortcutInput extends Shortcut {\n /**\n * textarea, select and input are ignored by default, this is used to override\n * this behavior.\n * allow in node names, accepts: [\"TEXTAREA\", \"SELECT\", \"INPUT]\n */\n allowIn?: AllowIn[];\n /**\n * Only trigger the command when the target is in focus.\n */\n target?: HTMLElement;\n}\n\nexport enum AllowIn {\n Textarea = 'TEXTAREA',\n Input = 'INPUT',\n Select = \"SELECT\"\n}\n\n/**\n * @ignore\n */\nexport interface ParsedShortcut extends ShortcutInput {\n key: string[];\n predicates: Function[][];\n id: string;\n priority?: number;\n\n isSequence: boolean;\n sequence?: string[][];\n event?: KeyboardEvent;\n}\n\n/**\n * The output type fired by the command when shortcut is triggered.\n */\nexport interface ShortcutEventOutput {\n /**\n * The browser keyboard event\n */\n event: KeyboardEvent;\n /**\n * The registered key that was triggered\n */\n key: string[] | string;\n}\n","import { Directive, ElementRef, Input, OnChanges, OnDestroy, SimpleChanges } from \"@angular/core\";\nimport { AllowIn, Shortcut } from \"./ng-hotkeys.interfaces\";\nimport { NgHotkeysService } from \"./ng-hotkeys.service\";\n\n/**\n * A directive to be used with \"focusable\" elements, like:\n * textarea, input, select.\n */\n@Directive({\n selector: \"[ngHotKeys]\"\n})\nexport class NgHotkeysDirective implements OnDestroy, OnChanges {\n /**\n * clearId to remove shortcuts.\n */\n private clearIds;\n /**\n * Shortcut inputs for the directive.\n * will only work when the element is in focus\n */\n @Input() ngHotKeys: Shortcut[];\n /**\n * @ignore\n * @type {boolean}\n * @private\n */\n private _disabled = false;\n\n /**\n * whether to disable the shortcuts for this directive\n * @param value\n */\n @Input() set disabled(value) {\n this._disabled = value;\n if (this.clearIds) {\n this.keyboard.remove(this.clearIds);\n }\n setTimeout(() => {\n if (value === false && this.ngHotKeys) {\n this.clearIds = this.keyboard.add(this.transformInput(this.ngHotKeys));\n }\n })\n\n }\n\n /**\n * @ignore\n * @param {NgHotkeysService} keyboard\n * @param {ElementRef} el\n */\n constructor(private keyboard: NgHotkeysService, private el: ElementRef) {}\n\n /**\n * @ignore\n * @param {Shortcut[]} shortcuts\n * @returns {any}\n */\n private transformInput(shortcuts: Shortcut[]) {\n return shortcuts.map(shortcut => ({\n ...shortcut,\n target: this.el.nativeElement,\n allowIn: [AllowIn.Select, AllowIn.Input, AllowIn.Textarea]\n }));\n }\n\n /**\n * @ignore\n */\n ngOnDestroy() {\n if (!this.clearIds) {\n return;\n }\n this.keyboard.remove(this.clearIds);\n }\n\n /**\n * @ignore\n * @param {SimpleChanges} changes\n */\n ngOnChanges(changes: SimpleChanges) {\n const { ngKeyboardShortcuts } = changes;\n if (this.clearIds) {\n this.keyboard.remove(this.clearIds);\n }\n if (!ngKeyboardShortcuts || !ngKeyboardShortcuts.currentValue) {\n return;\n }\n this.clearIds = this.keyboard.add(this.transformInput(ngKeyboardShortcuts.currentValue));\n }\n}\n","import {\n TemplateRef,\n ViewContainerRef,\n ElementRef,\n ComponentRef,\n EmbeddedViewRef,\n Injector,\n ComponentFactoryResolver,\n} from '@angular/core';\n\n/**\n * @ignore\n * Interface that can be used to generically type a class.\n */\nexport interface ComponentType<T> {\n new (...args: any[]): T;\n}\n\n/**\n * @ignore\n * A `Portal` is something that you want to render somewhere else.\n * It can be attach to / detached from a `PortalOutlet`.\n */\nexport abstract class Portal<T> {\n private _attachedHost: PortalOutlet | null;\n\n /** Attach this portal to a host. */\n attach(host: PortalOutlet): T {\n if (host == null) {\n // TODO: add error\n console.error(\"null portal error\");\n }\n\n if (host.hasAttached()) {\n console.error(\"portal already attached\");\n // throwPortalAlreadyAttachedError();\n }\n\n this._attachedHost = host;\n return <T> host.attach(this);\n }\n\n /** Detach this portal from its host */\n detach(): void {\n let host = this._attachedHost;\n\n if (host == null) {\n console.error(\"no portal attached!\");\n // throwNoPortalAttachedError();\n } else {\n this._attachedHost = null;\n host.detach();\n }\n }\n\n /** Whether this portal is attached to a host. */\n get isAttached(): boolean {\n return this._attachedHost != null;\n }\n\n /**\n * Sets the PortalOutlet reference without performing `attach()`. This is used directly by\n * the PortalOutlet when it is performing an `attach()` or `detach()`.\n */\n setAttachedHost(host: PortalOutlet | null) {\n this._attachedHost = host;\n }\n}\n\n\n/**\n * @ignore\n * A `ComponentPortal` is a portal that instantiates some Component upon attachment.\n */\nexport class ComponentPortal<T> extends Portal<ComponentRef<T>> {\n /** The type of the component that will be instantiated for attachment. */\n component: ComponentType<T>;\n\n /**\n * [Optional] Where the attached component should live in Angular's *logical* component tree.\n * This is different from where the component *renders*, which is determined by the PortalOutlet.\n * The origin is necessary when the host is outside of the Angular application context.\n */\n viewContainerRef?: ViewContainerRef | null;\n\n /** [Optional] Injector used for the instantiation of the component. */\n injector?: Injector | null;\n\n /**\n * Alternate `ComponentFactoryResolver` to use when resolving the associated component.\n * Defaults to using the resolver from the outlet that the portal is attached to.\n */\n componentFactoryResolver?: ComponentFactoryResolver | null;\n\n constructor(\n component: ComponentType<T>,\n viewContainerRef?: ViewContainerRef | null,\n injector?: Injector | null,\n componentFactoryResolver?: ComponentFactoryResolver | null) {\n super();\n this.component = component;\n this.viewContainerRef = viewContainerRef;\n this.injector = injector;\n this.componentFactoryResolver = componentFactoryResolver;\n }\n}\n\n/**\n * @ignore\n * A `TemplatePortal` is a portal that represents some embedded template (TemplateRef).\n */\nexport class TemplatePortal<C = any> extends Portal<C> {\n /** The embedded template that will be used to instantiate an embedded View in the host. */\n templateRef: TemplateRef<C>;\n\n /** Reference to the ViewContainer into which the template will be stamped out. */\n viewContainerRef: ViewContainerRef;\n\n /** Contextual data to be passed in to the embedded view. */\n context: C | undefined;\n\n constructor(template: TemplateRef<C>, viewContainerRef: ViewContainerRef, context?: C) {\n super();\n this.templateRef = template;\n this.viewContainerRef = viewContainerRef;\n this.context = context;\n }\n\n get origin(): ElementRef {\n return this.templateRef.elementRef;\n }\n\n /**\n * Attach the portal to the provided `PortalOutlet`.\n * When a context is provided it will override the `context` property of the `TemplatePortal`\n * instance.\n */\n attach(host: PortalOutlet, context: C | undefined = this.context): C {\n this.context = context;\n return super.attach(host);\n }\n\n detach(): void {\n this.context = undefined;\n return super.detach();\n }\n}\n\n\n/**\n * @ignore\n * A `PortalOutlet` is an space that can contain a single `Portal`.\n */\nexport interface PortalOutlet {\n /** Attaches a portal to this outlet. */\n attach(portal: Portal<any>): any;\n\n /** Detaches the currently attached portal from this outlet. */\n detach(): any;\n\n /** Performs cleanup before the outlet is destroyed. */\n dispose(): void;\n\n /** Whether there is currently a portal attached to this outlet. */\n hasAttached(): boolean;\n}\n\n\n/**\n * @ignore\n * Partial implementation of PortalOutlet that handles attaching\n * ComponentPortal and TemplatePortal.\n */\nexport abstract class BasePortalOutlet implements PortalOutlet {\n /** The portal currently attached to the host. */\n protected _attachedPortal: Portal<any> | null;\n\n /** A function that will permanently dispose this host. */\n private _disposeFn: (() => void) | null;\n\n /** Whether this host has already been permanently disposed. */\n private _isDisposed: boolean = false;\n\n /** Whether this host has an attached portal. */\n hasAttached(): boolean {\n return !!this._attachedPortal;\n }\n\n attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;\n attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;\n attach(portal: any): any;\n\n /** Attaches a portal. */\n attach(portal: Portal<any>): any {\n if (!portal) {\n console.error('null portal!');\n // throwNullPortalError();\n }\n\n if (this.hasAttached()) {\n console.error('portal already attached');\n // throwPortalAlreadyAttachedError();\n }\n\n if (this._isDisposed) {\n console.error('portal out already disposed');\n // throwPortalOutletAlreadyDisposedError();\n }\n\n if (portal instanceof ComponentPortal) {\n this._attachedPortal = portal;\n return this.attachComponentPortal(portal);\n } else if (portal instanceof TemplatePortal) {\n this._attachedPortal = portal;\n return this.attachTemplatePortal(portal);\n }\n console.error('unknown portal type');\n // throwUnknownPortalTypeError();\n }\n\n abstract attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T>;\n\n abstract attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C>;\n\n /** Detaches a previously attached portal. */\n detach(): void {\n if (this._attachedPortal) {\n this._attachedPortal.setAttachedHost(null);\n this._attachedPortal = null;\n }\n\n this._invokeDisposeFn();\n }\n\n /** Permanently dispose of this portal host. */\n dispose(): void {\n if (this.hasAttached()) {\n this.detach();\n }\n\n this._invokeDisposeFn();\n this._isDisposed = true;\n }\n\n /** @docs-private */\n setDisposeFn(fn: () => void) {\n this._disposeFn = fn;\n }\n\n private _invokeDisposeFn() {\n if (this._disposeFn) {\n this._disposeFn();\n this._disposeFn = null;\n }\n }\n}\n","import {\n ComponentFactoryResolver,\n ComponentRef,\n EmbeddedViewRef,\n ApplicationRef,\n Injector,\n} from '@angular/core';\nimport {BasePortalOutlet, ComponentPortal, TemplatePortal} from './portal';\n\n\n/**\n * @ignore\n * A PortalOutlet for attaching portals to an arbitrary DOM element outside of the Angular\n * application context.\n */\nexport class DomPortalOutlet extends BasePortalOutlet {\n constructor(\n /** Element into which the content is projected. */\n public outletElement: Element,\n private _componentFactoryResolver: ComponentFactoryResolver,\n private _appRef: ApplicationRef,\n private _defaultInjector: Injector) {\n super();\n }\n\n /**\n * Attach the given ComponentPortal to DOM element using the ComponentFactoryResolver.\n * @param portal Portal to be attached\n * @returns Reference to the created component.\n */\n attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {\n const resolver = portal.componentFactoryResolver || this._componentFactoryResolver;\n const componentFactory = resolver.resolveComponentFactory(portal.component);\n let componentRef: ComponentRef<T>;\n\n // If the portal specifies a ViewContainerRef, we will use that as the attachment point\n // for the component (in terms of Angular's component tree, not rendering).\n // When the ViewContainerRef is missing, we use the factory to create the component directly\n // and then manually attach the view to the application.\n if (portal.viewContainerRef) {\n componentRef = portal.viewContainerRef.createComponent(\n componentFactory,\n portal.viewContainerRef.length,\n portal.injector || portal.viewContainerRef.injector);\n\n this.setDisposeFn(() => componentRef.destroy());\n } else {\n componentRef = componentFactory.create(portal.injector || this._defaultInjector);\n this._appRef.attachView(componentRef.hostView);\n this.setDisposeFn(() => {\n this._appRef.detachView(componentRef.hostView);\n componentRef.destroy();\n });\n }\n // At this point the component has been instantiated, so we move it to the location in the DOM\n // where we want it to be rendered.\n this.outletElement.appendChild(this._getComponentRootNode(componentRef));\n\n return componentRef;\n }\n\n /**\n * Attaches a template portal to the DOM as an embedded view.\n * @param portal Portal to be attached.\n * @returns Reference to the created embedded view.\n */\n attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {\n let viewContainer = portal.viewContainerRef;\n let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context);\n viewRef.detectChanges();\n\n // The method `createEmbeddedView` will add the view as a child of the viewContainer.\n // But for the DomPortalOutlet the view can be added everywhere in the DOM\n // (e.g Overlay Container) To move the view to the specified host element. We just\n // re-append the existing root nodes.\n viewRef.rootNodes.forEach(rootNode => this.outletElement.appendChild(rootNode));\n\n this.setDisposeFn((() => {\n let index = viewContainer.indexOf(viewRef);\n if (index !== -1) {\n viewContainer.remove(index);\n }\n }));\n\n return viewRef;\n }\n\n /**\n * Clears out a portal from the DOM.\n */\n dispose(): void {\n super.dispose();\n if (this.outletElement.parentNode != null) {\n this.outletElement.parentNode.removeChild(this.outletElement);\n }\n }\n\n /** Gets the root HTMLElement for an instantiated component. */\n private _getComponentRootNode(componentRef: ComponentRef<any>): HTMLElement {\n return (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;\n }\n}\n","<div class=\"item\" [class.item--odd]=\"index % 2 !== 0\" *ngIf=\"shortcut.description\">\n <div class=\"description\">\n <span>{{shortcut.description}}</span>\n </div>\n <div class=\"keys\">\n <div *ngFor=\"let sKey of parsedKeys;let i = index\" class=\"key__container\">\n <span class=\"key\" *ngFor=\"let key of sKey;\">{{key}}</span>\n <span *ngIf=\"parsedKeys.length > 1 && i < parsedKeys.length - 1\" class=\"separator\"> / </span>\n </div>\n </div>\n</div>\n","import { Component, Input, OnInit } from \"@angular/core\";\nimport { Shortcut } from \"./ng-hotkeys.interfaces\";\nimport { symbols } from \"./keys\";\nimport { identity } from './utils';\n\n/**\n * @ignore\n */\n@Component({\n selector: \"ng-hot-keys-help-item\",\n templateUrl: \"./ng-hotkeys-help-item.component.html\",\n styleUrls: [\"./ng-hotkeys-help-item.component.css\"]\n})\nexport class NgHotkeysHelpItemComponent implements OnInit {\n public parsedKeys: string[][];\n\n @Input() index: number;\n\n @Input()\n set shortcut(shortcut: Shortcut) {\n const key = Array.isArray(shortcut.key) ? shortcut.key : [shortcut.key];\n this.parsedKeys = key.map(key =>\n key\n .split(\" \")\n .filter(identity)\n .filter(key => key !== \"+\")\n .map(key => {\n if (symbols[key]) {\n return symbols[key];\n }\n return key;\n })\n );\n this._shortcut = shortcut;\n }\n get shortcut() {\n return this._shortcut;\n }\n\n private _shortcut: Shortcut;\n\n constructor() {}\n\n ngOnInit() {}\n}\n","<ng-template>\n <div class=\"help-modal__container\" [attr.aria-labelledby]=\"'modal-' + title\" role=\"dialog\">\n <div class=\"{{className}}\" [@enterAnimation] *ngIf=\"showing\">\n <div class=\"title\">\n <h3 id=\"modal-{{title}}\" class=\"title__text\">{{title}}</h3>\n </div>\n <div class=\"help-modal__body\">\n <span *ngIf=\"!labels.length\">\n {{emptyMessage}}\n </span>\n <div>\n <ul *ngFor=\"let label of labels\" class=\"help-modal__list\">\n <h4 class=\"item-group-label\">{{label}}</h4>\n <ng-hot-keys-help-item\n *ngFor=\"let shortcut of shortcuts[label]; let i = index\"\n [shortcut]=\"shortcut\"\n [index]=\"i\"\n ></ng-hot-keys-help-item>\n </ul>\n </div>\n </div>\n </div>\n <div class=\"help-modal__backdrop\" [@overlayAnimation] (mousedown)=\"hide()\" *ngIf=\"showing\"></div>\n </div>\n</ng-template>\n","import {\n ApplicationRef,\n Component,\n ComponentFactoryResolver,\n ElementRef,\n Injector,\n Input,\n OnDestroy,\n OnInit,\n TemplateRef,\n ViewChild,\n ViewContainerRef\n} from \"@angular/core\";\nimport { DomPortalOutlet } from \"./dom-portal-outlet\";\nimport { TemplatePortal } from \"./portal\";\nimport { NgHotkeysService } from './ng-hotkeys.service';\nimport { NgHotkeysHelpService } from './ng-hotkeys-help.service';\nimport { animate, style, transition, trigger } from \"@angular/animations\";\nimport { distinctUntilChanged, map } from \"rxjs/operators\";\nimport { groupBy } from './utils';\nimport { SubscriptionLike } from \"rxjs\";\nimport { Shortcut } from './ng-hotkeys.interfaces';\n\n\n/**\n * @ignore\n */\nconst scrollAbleKeys = new Map([\n [31, 1],\n [38, 1],\n [39, 1],\n [40, 1]\n]);\n/**\n * @ignore\n */\nconst preventDefault = (ignore: string) => e => {\n const modal = e.target.closest(ignore);\n if (modal) {\n return;\n }\n e = e || window.event;\n if (e.preventDefault) e.preventDefault();\n e.returnValue = false;\n};\n/**\n * @ignore\n */\nconst preventDefaultForScrollKeys = e => {\n if (!scrollAbleKeys.has(e.keyCode)) {\n return;\n }\n preventDefault(e);\n return false;\n};\n/**\n * @ignore\n */\nlet scrollEvents = [\n { name: \"wheel\", callback: null },\n { name: \"touchmove\", callback: null },\n { name: \"DOMMouseScroll\", callback: null }\n];\n\n/**\n * @ignore\n */\nconst disableScroll = (ignore: string) => {\n scrollEvents = scrollEvents.map(event => {\n const callback = preventDefault(ignore);\n window.addEventListener(event.name, callback, { passive: false });\n return {\n ...event,\n callback\n };\n });\n window.addEventListener(\"keydown\", pr