ng-hotkeys
Version:
ng-hotkeys for Angular 14+
1,418 lines (1,405 loc) • 61.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Inject, Component, Input, Directive, TemplateRef, ViewChild, NgModule } from '@angular/core';
import { Subject, BehaviorSubject, fromEvent, timer, throwError, of } from 'rxjs';
import { filter, switchMap, first, tap, repeat, map, throttle, takeUntil, catchError, scan, distinctUntilChanged } from 'rxjs/operators';
import * as i1 from '@angular/common';
import { DOCUMENT, CommonModule } from '@angular/common';
import { trigger, transition, style, animate } from '@angular/animations';
function isFunction(x) {
return typeof x === "function";
}
const invert = obj => {
const new_obj = {};
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
new_obj[obj[prop]] = prop;
}
}
return new_obj;
};
const any = (fn, list) => {
let idx = 0;
while (idx < list.length) {
if (fn(list[idx])) {
return true;
}
idx += 1;
}
return false;
};
const identity = x => x;
/**
* @ignore
* @param x
* @returns boolean
*/
const isNill = x => x == null;
/**
* @ignore
* @param xs
* @param key
* @returns any
*/
const groupBy = (xs, key) => xs.reduce((result, x) => ({
...result,
[x[key]]: [...(result[x[key]] || []), x]
}), {});
/**
* @ignore
* @param first
* @param second
* @returns any[]
*/
const difference = (first, second) => first.filter(item => !second.includes(item));
/**
* @ignore
* @param preds
* @returns (...args) => boolean;
*/
const allPass = preds => (...args) => {
let idx = 0;
const len = preds.length;
while (idx < len) {
if (!preds[idx].apply(this, args)) {
return false;
}
idx += 1;
}
return true;
};
const prop = prop => object => object[prop];
const minMaxArrayProp = type => (property, array) => Math[type].apply(Math, array.map(prop(property)));
const maxArrayProp = (property, array) => {
return array.reduce((acc, curr) => {
const propFn = prop(property);
const currentValue = propFn(curr);
const previousValue = propFn(acc);
return currentValue > previousValue ? curr : acc;
}, { [property]: 0 });
};
const isMac = typeof navigator !== "undefined" ? navigator.userAgent.includes("Mac OS") : false;
const modifiers = {
shift: "shiftKey",
ctrl: "ctrlKey",
alt: "altKey",
cmd: isMac ? "metaKey" : "ctrlKey",
command: isMac ? "metaKey" : "ctrlKey",
meta: isMac ? "metaKey" : "ctrlKey",
"left command": "metaKey",
"right command": "MetaRight",
"⌘": isMac ? "metaKey" : "ctrlKey",
option: "altKey",
ctl: "ctrlKey",
control: "ctrlKey"
};
const _SPECIAL_CASES = {
plus: "+"
};
const symbols = {
cmd: isMac ? "⌘" : "Ctrl",
command: isMac ? "⌘" : "Ctrl",
"left command": isMac ? "⌘" : "Ctrl",
"right command": isMac ? "⌘" : "Ctrl",
option: isMac ? "⌥" : "Alt",
plus: "+",
left: "←",
right: "→",
up: "↑",
down: "↓",
alt: isMac ? "⌥" : "Alt",
ctrl: "Ctrl",
control: "Ctrl",
shift: "⇧"
};
const _MAP = {
8: "backspace",
9: "tab",
13: "enter",
16: "shift",
17: ["ctrl", "control"],
18: "alt",
20: "capslock",
27: ["esc", "escape"],
32: ["space", "spc"],
33: "pageup",
34: "pagedown",
35: "end",
36: "home",
37: "left",
38: "up",
39: "right",
40: "down",
45: "ins",
46: "del",
91: ["meta", "cmd", "command"],
93: ["meta", "cmd", "command"],
224: ["meta", "cmd", "command"]
};
/*
* mapping for special characters so they can support
*
* this dictionary is only used incase you want to bind a
* keyup or keydown event to one of these keys
*
*/
const _KEYCODE_MAP = {
106: "*",
107: "+",
109: "-",
110: ".",
111: "/",
186: ";",
187: "=",
188: ",",
189: "-",
190: ".",
191: "/",
192: "`",
219: "[",
220: "\\",
221: "]",
222: "'"
};
/**
* this is a mapping of keys that require shift on a US keypad
* back to the non shift equivelents
*
* this is so you can use keyup events with these keys
*
* note that this will only work reliably on US keyboards
*
*/
const _SHIFT_MAP = {
"`": "~",
"1": "!",
"2": "@",
"3": "#",
"4": "$",
"5": "%",
"6": "^",
"7": "&",
"8": "*",
"9": "(",
"0": ")",
"-": "_",
"=": "+",
";": ":",
"'": '"',
",": "<",
".": ">",
"/": "?",
"\\": "|"
};
const _INVERTED_SHIFT_MAP = invert(_SHIFT_MAP);
/**
* loop through the f keys, f1 to f19 and add them to the map
* programatically
*/
for (let i = 1; i < 20; ++i) {
_MAP[111 + i] = "f" + i;
}
/**
* loop through to map numbers on the numeric keypad
*/
for (let i = 0; i <= 9; ++i) {
// This needs to use a string cause otherwise since 0 is falsey
// event will never fire for numpad 0 pressed as part of a keydown
// event.
_MAP[i + 96] = i.toString();
}
/**
* @ignore
* @type {number}
*/
let guid = 0;
class NgHotkeysService {
/**
* @ignore
*/
constructor(document) {
this.document = document;
/**
* Parsed shortcuts
* for each key create a predicate function
*/
this._shortcuts = [];
this._sequences = [];
/**
* Throttle the keypress event.
*/
this.throttleTime = 0;
this._pressed = new Subject();
/**
* Streams of pressed events, can be used instead or with a command.
*/
this.pressed$ = this._pressed.asObservable();
/**
* Disable all keyboard shortcuts
*/
this.disabled = false;
this._shortcutsSub = new BehaviorSubject([]);
this.shortcuts$ = this._shortcutsSub
.asObservable()
.pipe(filter(shortcuts => !!shortcuts.length));
this._ignored = ["INPUT", "TEXTAREA", "SELECT"];
/**
* @ignore
* Subscription for on destroy.
*/
this.subscriptions = [];
/**
* @ignore
* @param shortcut
*/
this.isAllowed = (shortcut) => {
const target = shortcut.event.target;
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);
};
/**
* @ignore
* @param event
*/
this.mapEvent = event => {
return this._shortcuts
.filter(shortcut => !shortcut.isSequence)
.map(shortcut => Object.assign({}, shortcut, {
predicates: any(identity, shortcut.predicates.map((predicates) => allPass(predicates)(event))),
event: event
}))
.filter(shortcut => shortcut.predicates)
.reduce((acc, shortcut) => (acc.priority > shortcut.priority ? acc : shortcut), {
priority: 0
});
};
this.keydown$ = fromEvent(this.document, "keydown");
/**
* fixes for firefox prevent default
* on click event on button focus:
* see issue:
* keeping this here for now, but it is commented out
* Firefox reference bug:
* https://bugzilla.mozilla.org/show_bug.cgi?id=1487102
* and my repo:
*
* https://github.com/omridevk/ng-keyboard-shortcuts/issues/35
*/
this.ignore$ = this.pressed$.pipe(filter(e => e.event.defaultPrevented), switchMap(() => this.clicks$.pipe(first())), tap((e) => {
e.preventDefault();
e.stopPropagation();
}), repeat());
/**
* @ignore
*/
this.clicks$ = fromEvent(this.document, "click", { capture: true });
this.keyup$ = fromEvent(this.document, "keyup");
/**
* @ignore
*/
this.keydownCombo$ = this.keydown$.pipe(filter(_ => !this.disabled), map(this.mapEvent), filter((shortcut) => !shortcut.target || shortcut.event.target === shortcut.target), filter((shortcut) => isFunction(shortcut.command)), filter(this.isAllowed), tap(shortcut => {
if (!shortcut.preventDefault) {
return;
}
shortcut.event.preventDefault();
shortcut.event.stopPropagation();
}), throttle(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 })), takeUntil(this.keyup$), repeat(), catchError(error => throwError(error)));
/**
* @ignore
*/
this.timer$ = new Subject();
/**
* @ignore
*/
this.resetCounter$ = this.timer$
.asObservable()
.pipe(switchMap(() => timer(NgHotkeysService.TIMEOUT_SEQUENCE)));
/**
* @ignore
*/
this.keydownSequence$ = this.shortcuts$.pipe(map(shortcuts => shortcuts.filter(shortcut => shortcut.isSequence)), switchMap(sequences => this.keydown$.pipe(map(event => {
return {
event,
sequences
};
}), tap(val => this.timer$.next(val)))), scan((acc, arg) => {
let { event } = arg;
const currentLength = acc.events.length;
const sequences = currentLength ? acc.sequences : arg.sequences;
let [characters] = this.characterFromEvent(event);
characters = Array.isArray(characters)
? [...characters, event.key]
: [characters, event.key];
const result = sequences
.map(sequence => {
const sequences = sequence.sequence.filter(seque => characters.some(key => (_SPECIAL_CASES[seque[currentLength]] ||
seque[currentLength]) === key));
const partialMatch = sequences.length > 0;
if (sequence.fullMatch) {
return sequence;
}
return {
...sequence,
sequence: sequences,
partialMatch,
event: event,
fullMatch: partialMatch &&
this.isFullMatch({ command: sequence, events: acc.events })
};
})
.filter(sequences => sequences.partialMatch || sequences.fullMatch);
let [match] = result;
if (!match || this.modifiersOn(event)) {
return { events: [], sequences: this._sequences };
}
/*
* handle case of "?" sequence and "? a" sequence
* need to determine which one to trigger.
* if both match, we pick the longer one (? a) in this case.
*/
const guess = maxArrayProp("priority", result);
if (result.length > 1 && guess.fullMatch) {
return { events: [], command: guess, sequences: this._sequences };
}
if (result.length > 1) {
return { events: [...acc.events, event], command: result, sequences: result };
}
if (match.fullMatch) {
return { events: [], command: match, sequences: this._sequences };
}
return { events: [...acc.events, event], command: result, sequences: result };
}, { events: [], sequences: [] }), switchMap(({ command }) => {
if (Array.isArray(command)) {
/*
* Add a timer to handle the case where for example:
* a sequence "?" is registered and "? a" is registered as well
* if the user does not hit any key for 500ms, the single sequence will trigger
* if any keydown event occur, this timer will reset, given a chance to complete
* the full sequence (? a) in this case.
* This delay only occurs when single key sequence is the beginning of another sequence.
*/
return timer(500).pipe(map(() => ({ command: command.filter(command => command.fullMatch)[0] })));
}
return of({ command });
}), takeUntil(this.pressed$), filter(({ command }) => command && command.fullMatch), map(({ command }) => command), filter((shortcut) => isFunction(shortcut.command)), filter((shortcut) => !shortcut.target || shortcut.event.target === shortcut.target), filter(this.isAllowed), tap(shortcut => !shortcut.preventDefault || shortcut.event.preventDefault()), throttle(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 })), takeUntil(this.resetCounter$), repeat());
/**
* @ignore
* transforms a shortcut to:
* a predicate function
*/
this.getKeys = (keys) => {
return keys
.map(key => key.trim())
.filter(key => key !== "+")
.map(key => {
// for modifiers like control key
// look for event['ctrlKey']
// otherwise use the keyCode
key = _SPECIAL_CASES[key] || key;
if (modifiers.hasOwnProperty(key)) {
return event => {
return !!event[modifiers[key]];
};
}
return event => {
const isUpper = key === key.toUpperCase();
const isNonAlpha = (/[^a-zA-Z\d\s:]/).test(key);
const inShiftMap = _INVERTED_SHIFT_MAP[key];
let [characters, shiftKey] = this.characterFromEvent(event);
const allModifiers = Object.keys(modifiers).map((key) => {
return modifiers[key];
});
const hasModifiers = allModifiers.some(modifier => event[modifier]);
characters = Array.isArray(characters)
? [...characters, event.key]
: [characters, event.key];
// if has modifiers:
// we want to make sure it is not upper case letters
// (because upper has modifiers so we want continue the check)
// we also want to make sure it is not alphanumeric char like ? / ^ & and others (since those could require modifiers as well)
// we also want to check this only if the length of
// of the keys is one (i.e the command key is "?" or "c"
// this while check is here to verify that:
// if registered key like "e"
// it won't be fired when clicking ctrl + e, or any modifiers + the key
// we only want to trigger when the single key is clicked alone
// thus all these edge cases.
// hopefully this would cover all cases
// TODO:: find a way simplify this
if (hasModifiers
&& (!isUpper || isNonAlpha)
&& !inShiftMap
&& keys.length === 1) {
return false;
}
return characters.some(char => {
if (char === key && isUpper) {
return true;
}
return key === char;
});
};
});
};
this.subscriptions.push(this.keydownSequence$.subscribe(), this.keydownCombo$.subscribe()
// this.ignore$.subscribe()
);
}
/**
* @ignore
* @param command
* @param events
*/
isFullMatch({ command, events }) {
if (!command) {
return false;
}
return command.sequence.some(sequence => {
return sequence.length === events.length + 1;
});
}
/**
* @ignore
*/
get shortcuts() {
return this._shortcuts;
}
/**
* @ignore
* @param event
*/
_characterFromEvent(event) {
if (typeof event.which !== "number") {
event.which = event.keyCode;
}
if (_SPECIAL_CASES[event.which]) {
return [_SPECIAL_CASES[event.which], event.shiftKey];
}
if (_MAP[event.which]) {
// for non keypress events the special maps are needed
return [_MAP[event.which], event.shiftKey];
}
if (_KEYCODE_MAP[event.which]) {
return [_KEYCODE_MAP[event.which], event.shiftKey];
}
// in case event key is lower case but registered key is upper case
// return it in the lower case
if (String.fromCharCode(event.which).toLowerCase() !== event.key) {
return [String.fromCharCode(event.which).toLowerCase(), event.shiftKey];
}
return [event.key, event.shiftKey];
}
characterFromEvent(event) {
let [key, shiftKey] = this._characterFromEvent(event);
if (shiftKey && _SHIFT_MAP[key]) {
return [_SHIFT_MAP[key], shiftKey];
}
return [key, shiftKey];
}
/**
* @ignore
* Remove subscription.
*/
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
/**
* @ignore
* @param shortcuts
*/
isSequence(shortcuts) {
return !shortcuts.some(shortcut => shortcut.includes("+") || shortcut.length === 1);
}
/**
* Add new shortcut/s
*/
add(shortcuts) {
shortcuts = Array.isArray(shortcuts) ? shortcuts : [shortcuts];
const commands = this.parseCommand(shortcuts);
commands.forEach(command => {
if (command.isSequence) {
this._sequences.push(command);
return;
}
this._shortcuts.push(command);
});
setTimeout(() => {
this._shortcutsSub.next([...this._shortcuts, ...this._sequences]);
});
return commands.map(command => command.id);
}
/**
* Remove a command based on key or array of keys.
* can be used for cleanup.
* @returns
* @param ids
*/
remove(ids) {
ids = Array.isArray(ids) ? ids : [ids];
this._shortcuts = this._shortcuts.filter(shortcut => !ids.includes(shortcut.id));
this._sequences = this._sequences.filter(shortcut => !ids.includes(shortcut.id));
setTimeout(() => {
this._shortcutsSub.next([...this._shortcuts, ...this._sequences]);
});
return this;
}
/**
* Returns an observable of keyboard shortcut filtered by a specific key.
* @param key - the key to filter the observable by.
*/
select(key) {
return this.pressed$.pipe(filter(({ event, key: eventKeys }) => {
eventKeys = Array.isArray(eventKeys) ? eventKeys : [eventKeys];
return !!eventKeys.find(eventKey => eventKey === key);
}));
}
/**
* @ignore
* @param event
*/
modifiersOn(event) {
return ["metaKey", "altKey", "ctrlKey"].some(mod => event[mod]);
}
/**
* @ignore
* Parse each command using getKeys function
*/
parseCommand(command) {
const commands = Array.isArray(command) ? command : [command];
return commands.map(command => {
const keys = Array.isArray(command.key) ? command.key : [command.key];
const priority = Math.max(...keys.map(key => key.split(" ").filter(identity).length));
const predicates = keys.map(key => this.getKeys(key.split(" ").filter(identity)));
const isSequence = this.isSequence(keys);
const sequence = isSequence
? keys.map(key => key
.split(" ")
.filter(identity)
.map(key => key.trim()))
: [];
return {
...command,
isSequence,
sequence: isSequence ? sequence : [],
allowIn: command.allowIn || [],
key: keys,
id: `${guid++}`,
throttle: isNill(command.throttleTime) ? this.throttleTime : command.throttleTime,
priority: priority,
predicates: predicates
};
});
}
}
/**
* @ignore
* 2000 ms window to allow between key sequences otherwise
* the sequence will reset.
*/
NgHotkeysService.TIMEOUT_SEQUENCE = 1000;
NgHotkeysService.ɵfac = function NgHotkeysService_Factory(t) { return new (t || NgHotkeysService)(i0.ɵɵinject(DOCUMENT)); };
NgHotkeysService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: NgHotkeysService, factory: NgHotkeysService.ɵfac, providedIn: "root" });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(NgHotkeysService, [{
type: Injectable,
args: [{
providedIn: "root"
}]
}], function () { return [{ type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }]; }, null); })();
/**
* A component to bind global shortcuts, can be used multiple times across the app
* will remove registered shortcuts when element is removed from DOM.
*/
class NgHotkeysComponent {
/**
* @ignore
* @param {NgHotkeysService} keyboard
*/
constructor(keyboard) {
this.keyboard = keyboard;
/**
* A list of shortcuts.
*/
this.shortcuts = [];
/**
* @ignore
* list of registered keyboard shortcuts
* used for clean up on NgDestroy.
*/
this.clearIds = [];
/**
* @ignore
*/
this._disabled = false;
}
/**
* Disable all shortcuts for this component.
*/
set disabled(value) {
this._disabled = value;
if (this.clearIds) {
this.keyboard.remove(this.clearIds);
this.clearIds = [];
}
if (value) {
return;
}
this.clearIds = this.keyboard.add(this.shortcuts);
}
/**
* @ignore
*/
ngOnInit() { }
/**
* Select a key to listen to, will emit when the selected key is pressed.
*/
select(key) {
return this.keyboard.select(key);
}
/**
* @ignore
*/
ngAfterViewInit() { }
/**
* @ignore
*/
ngOnChanges(changes) {
if (!changes.shortcuts || !changes.shortcuts.currentValue) {
return;
}
if (this.clearIds) {
this.keyboard.remove(this.clearIds);
}
if (!this._disabled) {
setTimeout(() => (this.clearIds = this.keyboard.add(changes.shortcuts.currentValue)));
}
}
/**
* @ignore
*/
ngOnDestroy() {
this.keyboard.remove(this.clearIds);
}
}
NgHotkeysComponent.ɵfac = function NgHotkeysComponent_Factory(t) { return new (t || NgHotkeysComponent)(i0.ɵɵdirectiveInject(NgHotkeysService)); };
NgHotkeysComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: NgHotkeysComponent, selectors: [["ng-hot-keys"]], inputs: { shortcuts: "shortcuts", disabled: "disabled" }, features: [i0.ɵɵNgOnChangesFeature], decls: 0, vars: 0, template: function NgHotkeysComponent_Template(rf, ctx) { }, encapsulation: 2 });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(NgHotkeysComponent, [{
type: Component,
args: [{
selector: "ng-hot-keys",
template: ""
}]
}], function () { return [{ type: NgHotkeysService }]; }, { shortcuts: [{
type: Input
}], disabled: [{
type: Input
}] }); })();
/**
* Service to assist showing custom help screen
*/
class NgHotkeysHelpService {
/**
* @ignore
* @param {NgHotkeysService} keyboard
*/
constructor(keyboard) {
this.keyboard = keyboard;
/**
* Observable to provide access to all registered shortcuts in the app.
* @type {Observable<any>}
*/
this.shortcuts$ = this.keyboard.shortcuts$.pipe(map(shortcuts => shortcuts
.filter(shortcut => Boolean(shortcut.label) && Boolean(shortcut.description))
.map(({ key, label, description }) => ({
key,
label,
description
}))));
}
}
NgHotkeysHelpService.ɵfac = function NgHotkeysHelpService_Factory(t) { return new (t || NgHotkeysHelpService)(i0.ɵɵinject(NgHotkeysService)); };
NgHotkeysHelpService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: NgHotkeysHelpService, factory: NgHotkeysHelpService.ɵfac, providedIn: "root" });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(NgHotkeysHelpService, [{
type: Injectable,
args: [{
providedIn: "root"
}]
}], function () { return [{ type: NgHotkeysService }]; }, null); })();
/**
* Use this service to listen to a specific keyboards events using Rxjs.
* The shortcut must be declared in the app for the select to work.
*/
class NgHotkeysSelectService {
constructor(keyboardService) {
this.keyboardService = keyboardService;
}
/**
* Returns an observable of keyboard shortcut filtered by a specific key.
* @param key - the key to filter the observable by.
*/
select(key) {
return this.keyboardService.select(key);
}
}
NgHotkeysSelectService.ɵfac = function NgHotkeysSelectService_Factory(t) { return new (t || NgHotkeysSelectService)(i0.ɵɵinject(NgHotkeysService)); };
NgHotkeysSelectService.ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: NgHotkeysSelectService, factory: NgHotkeysSelectService.ɵfac, providedIn: "root" });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(NgHotkeysSelectService, [{
type: Injectable,
args: [{
providedIn: "root"
}]
}], function () { return [{ type: NgHotkeysService }]; }, null); })();
var AllowIn;
(function (AllowIn) {
AllowIn["Textarea"] = "TEXTAREA";
AllowIn["Input"] = "INPUT";
AllowIn["Select"] = "SELECT";
})(AllowIn || (AllowIn = {}));
/**
* A directive to be used with "focusable" elements, like:
* textarea, input, select.
*/
class NgHotkeysDirective {
/**
* @ignore
* @param {NgHotkeysService} keyboard
* @param {ElementRef} el
*/
constructor(keyboard, el) {
this.keyboard = keyboard;
this.el = el;
/**
* @ignore
* @type {boolean}
* @private
*/
this._disabled = false;
}
/**
* whether to disable the shortcuts for this directive
* @param value
*/
set disabled(value) {
this._disabled = value;
if (this.clearIds) {
this.keyboard.remove(this.clearIds);
}
setTimeout(() => {
if (value === false && this.ngHotKeys) {
this.clearIds = this.keyboard.add(this.transformInput(this.ngHotKeys));
}
});
}
/**
* @ignore
* @param {Shortcut[]} shortcuts
* @returns {any}
*/
transformInput(shortcuts) {
return shortcuts.map(shortcut => ({
...shortcut,
target: this.el.nativeElement,
allowIn: [AllowIn.Select, AllowIn.Input, AllowIn.Textarea]
}));
}
/**
* @ignore
*/
ngOnDestroy() {
if (!this.clearIds) {
return;
}
this.keyboard.remove(this.clearIds);
}
/**
* @ignore
* @param {SimpleChanges} changes
*/
ngOnChanges(changes) {
const { ngKeyboardShortcuts } = changes;
if (this.clearIds) {
this.keyboard.remove(this.clearIds);
}
if (!ngKeyboardShortcuts || !ngKeyboardShortcuts.currentValue) {
return;
}
this.clearIds = this.keyboard.add(this.transformInput(ngKeyboardShortcuts.currentValue));
}
}
NgHotkeysDirective.ɵfac = function NgHotkeysDirective_Factory(t) { return new (t || NgHotkeysDirective)(i0.ɵɵdirectiveInject(NgHotkeysService), i0.ɵɵdirectiveInject(i0.ElementRef)); };
NgHotkeysDirective.ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: NgHotkeysDirective, selectors: [["", "ngHotKeys", ""]], inputs: { ngHotKeys: "ngHotKeys", disabled: "disabled" }, features: [i0.ɵɵNgOnChangesFeature] });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(NgHotkeysDirective, [{
type: Directive,
args: [{
selector: "[ngHotKeys]"
}]
}], function () { return [{ type: NgHotkeysService }, { type: i0.ElementRef }]; }, { ngHotKeys: [{
type: Input
}], disabled: [{
type: Input
}] }); })();
/**
* @ignore
* A `Portal` is something that you want to render somewhere else.
* It can be attach to / detached from a `PortalOutlet`.
*/
class Portal {
/** Attach this portal to a host. */
attach(host) {
if (host == null) {
// TODO: add error
console.error("null portal error");
}
if (host.hasAttached()) {
console.error("portal already attached");
// throwPortalAlreadyAttachedError();
}
this._attachedHost = host;
return host.attach(this);
}
/** Detach this portal from its host */
detach() {
let host = this._attachedHost;
if (host == null) {
console.error("no portal attached!");
// throwNoPortalAttachedError();
}
else {
this._attachedHost = null;
host.detach();
}
}
/** Whether this portal is attached to a host. */
get isAttached() {
return this._attachedHost != null;
}
/**
* Sets the PortalOutlet reference without performing `attach()`. This is used directly by
* the PortalOutlet when it is performing an `attach()` or `detach()`.
*/
setAttachedHost(host) {
this._attachedHost = host;
}
}
/**
* @ignore
* A `ComponentPortal` is a portal that instantiates some Component upon attachment.
*/
class ComponentPortal extends Portal {
constructor(component, viewContainerRef, injector, componentFactoryResolver) {
super();
this.component = component;
this.viewContainerRef = viewContainerRef;
this.injector = injector;
this.componentFactoryResolver = componentFactoryResolver;
}
}
/**
* @ignore
* A `TemplatePortal` is a portal that represents some embedded template (TemplateRef).
*/
class TemplatePortal extends Portal {
constructor(template, viewContainerRef, context) {
super();
this.templateRef = template;
this.viewContainerRef = viewContainerRef;
this.context = context;
}
get origin() {
return this.templateRef.elementRef;
}
/**
* Attach the portal to the provided `PortalOutlet`.
* When a context is provided it will override the `context` property of the `TemplatePortal`
* instance.
*/
attach(host, context = this.context) {
this.context = context;
return super.attach(host);
}
detach() {
this.context = undefined;
return super.detach();
}
}
/**
* @ignore
* Partial implementation of PortalOutlet that handles attaching
* ComponentPortal and TemplatePortal.
*/
class BasePortalOutlet {
constructor() {
/** Whether this host has already been permanently disposed. */
this._isDisposed = false;
}
/** Whether this host has an attached portal. */
hasAttached() {
return !!this._attachedPortal;
}
/** Attaches a portal. */
attach(portal) {
if (!portal) {
console.error('null portal!');
// throwNullPortalError();
}
if (this.hasAttached()) {
console.error('portal already attached');
// throwPortalAlreadyAttachedError();
}
if (this._isDisposed) {
console.error('portal out already disposed');
// throwPortalOutletAlreadyDisposedError();
}
if (portal instanceof ComponentPortal) {
this._attachedPortal = portal;
return this.attachComponentPortal(portal);
}
else if (portal instanceof TemplatePortal) {
this._attachedPortal = portal;
return this.attachTemplatePortal(portal);
}
console.error('unknown portal type');
// throwUnknownPortalTypeError();
}
/** Detaches a previously attached portal. */
detach() {
if (this._attachedPortal) {
this._attachedPortal.setAttachedHost(null);
this._attachedPortal = null;
}
this._invokeDisposeFn();
}
/** Permanently dispose of this portal host. */
dispose() {
if (this.hasAttached()) {
this.detach();
}
this._invokeDisposeFn();
this._isDisposed = true;
}
/** @docs-private */
setDisposeFn(fn) {
this._disposeFn = fn;
}
_invokeDisposeFn() {
if (this._disposeFn) {
this._disposeFn();
this._disposeFn = null;
}
}
}
/**
* @ignore
* A PortalOutlet for attaching portals to an arbitrary DOM element outside of the Angular
* application context.
*/
class DomPortalOutlet extends BasePortalOutlet {
constructor(
/** Element into which the content is projected. */
outletElement, _componentFactoryResolver, _appRef, _defaultInjector) {
super();
this.outletElement = outletElement;
this._componentFactoryResolver = _componentFactoryResolver;
this._appRef = _appRef;
this._defaultInjector = _defaultInjector;
}
/**
* Attach the given ComponentPortal to DOM element using the ComponentFactoryResolver.
* @param portal Portal to be attached
* @returns Reference to the created component.
*/
attachComponentPortal(portal) {
const resolver = portal.componentFactoryResolver || this._componentFactoryResolver;
const componentFactory = resolver.resolveComponentFactory(portal.component);
let componentRef;
// If the portal specifies a ViewContainerRef, we will use that as the attachment point
// for the component (in terms of Angular's component tree, not rendering).
// When the ViewContainerRef is missing, we use the factory to create the component directly
// and then manually attach the view to the application.
if (portal.viewContainerRef) {
componentRef = portal.viewContainerRef.createComponent(componentFactory, portal.viewContainerRef.length, portal.injector || portal.viewContainerRef.injector);
this.setDisposeFn(() => componentRef.destroy());
}
else {
componentRef = componentFactory.create(portal.injector || this._defaultInjector);
this._appRef.attachView(componentRef.hostView);
this.setDisposeFn(() => {
this._appRef.detachView(componentRef.hostView);
componentRef.destroy();
});
}
// At this point the component has been instantiated, so we move it to the location in the DOM
// where we want it to be rendered.
this.outletElement.appendChild(this._getComponentRootNode(componentRef));
return componentRef;
}
/**
* Attaches a template portal to the DOM as an embedded view.
* @param portal Portal to be attached.
* @returns Reference to the created embedded view.
*/
attachTemplatePortal(portal) {
let viewContainer = portal.viewContainerRef;
let viewRef = viewContainer.createEmbeddedView(portal.templateRef, portal.context);
viewRef.detectChanges();
// The method `createEmbeddedView` will add the view as a child of the viewContainer.
// But for the DomPortalOutlet the view can be added everywhere in the DOM
// (e.g Overlay Container) To move the view to the specified host element. We just
// re-append the existing root nodes.
viewRef.rootNodes.forEach(rootNode => this.outletElement.appendChild(rootNode));
this.setDisposeFn((() => {
let index = viewContainer.indexOf(viewRef);
if (index !== -1) {
viewContainer.remove(index);
}
}));
return viewRef;
}
/**
* Clears out a portal from the DOM.
*/
dispose() {
super.dispose();
if (this.outletElement.parentNode != null) {
this.outletElement.parentNode.removeChild(this.outletElement);
}
}
/** Gets the root HTMLElement for an instantiated component. */
_getComponentRootNode(componentRef) {
return componentRef.hostView.rootNodes[0];
}
}
function NgHotkeysHelpItemComponent_div_0_div_5_span_1_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "span", 8);
i0.ɵɵtext(1);
i0.ɵɵelementEnd();
} if (rf & 2) {
const key_r6 = ctx.$implicit;
i0.ɵɵadvance(1);
i0.ɵɵtextInterpolate(key_r6);
} }
function NgHotkeysHelpItemComponent_div_0_div_5_span_2_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "span", 9);
i0.ɵɵtext(1, " / ");
i0.ɵɵelementEnd();
} }
function NgHotkeysHelpItemComponent_div_0_div_5_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div", 5);
i0.ɵɵtemplate(1, NgHotkeysHelpItemComponent_div_0_div_5_span_1_Template, 2, 1, "span", 6);
i0.ɵɵtemplate(2, NgHotkeysHelpItemComponent_div_0_div_5_span_2_Template, 2, 0, "span", 7);
i0.ɵɵelementEnd();
} if (rf & 2) {
const sKey_r2 = ctx.$implicit;
const i_r3 = ctx.index;
const ctx_r1 = i0.ɵɵnextContext(2);
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngForOf", sKey_r2);
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngIf", ctx_r1.parsedKeys.length > 1 && i_r3 < ctx_r1.parsedKeys.length - 1);
} }
function NgHotkeysHelpItemComponent_div_0_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div", 1)(1, "div", 2)(2, "span");
i0.ɵɵtext(3);
i0.ɵɵelementEnd()();
i0.ɵɵelementStart(4, "div", 3);
i0.ɵɵtemplate(5, NgHotkeysHelpItemComponent_div_0_div_5_Template, 3, 2, "div", 4);
i0.ɵɵelementEnd()();
} if (rf & 2) {
const ctx_r0 = i0.ɵɵnextContext();
i0.ɵɵclassProp("item--odd", ctx_r0.index % 2 !== 0);
i0.ɵɵadvance(3);
i0.ɵɵtextInterpolate(ctx_r0.shortcut.description);
i0.ɵɵadvance(2);
i0.ɵɵproperty("ngForOf", ctx_r0.parsedKeys);
} }
/**
* @ignore
*/
class NgHotkeysHelpItemComponent {
constructor() { }
set shortcut(shortcut) {
const key = Array.isArray(shortcut.key) ? shortcut.key : [shortcut.key];
this.parsedKeys = key.map(key => key
.split(" ")
.filter(identity)
.filter(key => key !== "+")
.map(key => {
if (symbols[key]) {
return symbols[key];
}
return key;
}));
this._shortcut = shortcut;
}
get shortcut() {
return this._shortcut;
}
ngOnInit() { }
}
NgHotkeysHelpItemComponent.ɵfac = function NgHotkeysHelpItemComponent_Factory(t) { return new (t || NgHotkeysHelpItemComponent)(); };
NgHotkeysHelpItemComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: NgHotkeysHelpItemComponent, selectors: [["ng-hot-keys-help-item"]], inputs: { index: "index", shortcut: "shortcut" }, decls: 1, vars: 1, consts: [["class", "item", 3, "item--odd", 4, "ngIf"], [1, "item"], [1, "description"], [1, "keys"], ["class", "key__container", 4, "ngFor", "ngForOf"], [1, "key__container"], ["class", "key", 4, "ngFor", "ngForOf"], ["class", "separator", 4, "ngIf"], [1, "key"], [1, "separator"]], template: function NgHotkeysHelpItemComponent_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵtemplate(0, NgHotkeysHelpItemComponent_div_0_Template, 6, 4, "div", 0);
} if (rf & 2) {
i0.ɵɵproperty("ngIf", ctx.shortcut.description);
} }, dependencies: [i1.NgForOf, i1.NgIf], styles: [".key[_ngcontent-%COMP%]{border:1px solid #CCCCCC;border-radius:4px;padding:5px 12px;margin-right:5px;background-color:#f5f5f5}.key__container[_ngcontent-%COMP%]{display:inline-block}.separator[_ngcontent-%COMP%]{margin-right:5px}.keys[_ngcontent-%COMP%]{float:right}.item[_ngcontent-%COMP%]{background-color:#ebebeb;padding:12px}.description[_ngcontent-%COMP%]{min-width:168px;display:inline-block;color:#333}.item--odd[_ngcontent-%COMP%]{background-color:#fff}"] });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(NgHotkeysHelpItemComponent, [{
type: Component,
args: [{ selector: "ng-hot-keys-help-item", template: "<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", styles: [".key{border:1px solid #CCCCCC;border-radius:4px;padding:5px 12px;margin-right:5px;background-color:#f5f5f5}.key__container{display:inline-block}.separator{margin-right:5px}.keys{float:right}.item{background-color:#ebebeb;padding:12px}.description{min-width:168px;display:inline-block;color:#333}.item--odd{background-color:#fff}\n"] }]
}], function () { return []; }, { index: [{
type: Input
}], shortcut: [{
type: Input
}] }); })();
function NgHotkeysHelpComponent_ng_template_0_div_1_span_5_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "span");
i0.ɵɵtext(1);
i0.ɵɵelementEnd();
} if (rf & 2) {
const ctx_r3 = i0.ɵɵnextContext(3);
i0.ɵɵadvance(1);
i0.ɵɵtextInterpolate1(" ", ctx_r3.emptyMessage, " ");
} }
function NgHotkeysHelpComponent_ng_template_0_div_1_ul_7_ng_hot_keys_help_item_3_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelement(0, "ng-hot-keys-help-item", 11);
} if (rf & 2) {
const shortcut_r7 = ctx.$implicit;
const i_r8 = ctx.index;
i0.ɵɵproperty("shortcut", shortcut_r7)("index", i_r8);
} }
function NgHotkeysHelpComponent_ng_template_0_div_1_ul_7_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "ul", 8)(1, "h4", 9);
i0.ɵɵtext(2);
i0.ɵɵelementEnd();
i0.ɵɵtemplate(3, NgHotkeysHelpComponent_ng_template_0_div_1_ul_7_ng_hot_keys_help_item_3_Template, 1, 2, "ng-hot-keys-help-item", 10);
i0.ɵɵelementEnd();
} if (rf & 2) {
const label_r5 = ctx.$implicit;
const ctx_r4 = i0.ɵɵnextContext(3);
i0.ɵɵadvance(2);
i0.ɵɵtextInterpolate(label_r5);
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngForOf", ctx_r4.shortcuts[label_r5]);
} }
function NgHotkeysHelpComponent_ng_template_0_div_1_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div")(1, "div", 3)(2, "h3", 4);
i0.ɵɵtext(3);
i0.ɵɵelementEnd()();
i0.ɵɵelementStart(4, "div", 5);
i0.ɵɵtemplate(5, NgHotkeysHelpComponent_ng_template_0_div_1_span_5_Template, 2, 1, "span", 6);
i0.ɵɵelementStart(6, "div");
i0.ɵɵtemplate(7, NgHotkeysHelpComponent_ng_template_0_div_1_ul_7_Template, 4, 2, "ul", 7);
i0.ɵɵelementEnd()()();
} if (rf & 2) {
const ctx_r1 = i0.ɵɵnextContext(2);
i0.ɵɵclassMap(ctx_r1.className);
i0.ɵɵproperty("@enterAnimation", undefined);
i0.ɵɵadvance(2);
i0.ɵɵpropertyInterpolate1("id", "modal-", ctx_r1.title, "");
i0.ɵɵadvance(1);
i0.ɵɵtextInterpolate(ctx_r1.title);
i0.ɵɵadvance(2);
i0.ɵɵproperty("ngIf", !ctx_r1.labels.length);
i0.ɵɵadvance(2);
i0.ɵɵproperty("ngForOf", ctx_r1.labels);
} }
function NgHotkeysHelpComponent_ng_template_0_div_2_Template(rf, ctx) { if (rf & 1) {
const _r10 = i0.ɵɵgetCurrentView();
i0.ɵɵelementStart(0, "div", 12);
i0.ɵɵlistener("mousedown", function NgHotkeysHelpComponent_ng_template_0_div_2_Template_div_mousedown_0_listener() { i0.ɵɵrestoreView(_r10); const ctx_r9 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r9.hide()); });
i0.ɵɵelementEnd();
} if (rf & 2) {
i0.ɵɵproperty("@overlayAnimation", undefined);
} }
function NgHotkeysHelpComponent_ng_template_0_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div", 0);
i0.ɵɵtemplate(1, NgHotkeysHelpComponent_ng_template_0_div_1_Template, 8, 8, "div", 1);
i0.ɵɵtemplate(2, NgHotkeysHelpComponent_ng_template_0_div_2_Template, 1, 1, "div", 2);
i0.ɵɵelementEnd();
} if (rf & 2) {
const ctx_r0 = i0.ɵɵnextContext();
i0.ɵɵattribute("aria-labelledby", "modal-" + ctx_r0.title);
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngIf", ctx_r0.showing);
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngIf", ctx_r0.showing);
} }
/**
* @ignore
*/
const scrollAbleKeys = new Map([
[31, 1],
[38, 1],
[39, 1],
[40, 1]
]);
/**
* @ignore
*/
const preventDefault = (ignore) => e => {
const modal = e.target.closest(ignore);
if (modal) {
return;
}
e = e || window.event;
if (e.preventDefault)
e.preventDefault();
e.returnValue = false;
};
/**
* @ignore
*/
const preventDefaultForScrollKeys = e => {
if (!scrollAbleKeys.has(e.keyCode)) {
return;
}
preventDefault(e);
return false;
};
/**
* @ignore
*/
let scrollEvents = [
{ name: "wheel", callback: null },
{ name: "touchmove", callback: null },
{ name: "DOMMouseScroll", callback: null }
];
/**
* @ignore
*/
const disableScroll = (ignore) => {
scrollEvents = scrollEvents.map(event => {
const callback = preventDefault(ignore);
window.addEventListener(event.name, callback, { passive: false });
return {
...event,
callback
};
});
window.addEventListener("keydown", preventDefaultForScrollKeys);
};
/**
* @ignore
*/
const enableScroll = () => {
scrollEvents = scrollEvents.map(event => {
window.removeEventListener(event.name, event.callback);
return {
...event,
callback: null
};
});
window.removeEventListener("keydown", preventDefaultForScrollKeys);
};
/**
* A Component to show all registered shortcut in the app
* it is shown as a modal
*/
class NgHotkeysHelpComponent {
/**
* @ignore
*/
constructor(componentFactoryResolver, appRef, keyboard, element, keyboardHelp, viewContainer, injector) {
this.componentFactoryResolver = componentFactoryResolver;
this.appRef = appRef;
this.keyboard = keyboard;
this.element = element;
this.keyboardHelp = keyboardHelp;
this.viewContainer = viewContainer;
this.injector = injector;
/**
* Disable scrolling while modal is open
*/
this.disableScrolling = true;
this.className = "help-modal";
/**
* The title of the help screen
* @default: "Keyboard shortcuts"
*/
this.title = "Keyboard shortcuts";
/**
* What message to show when no shortcuts are available on the page.
* @default "No shortcuts available"
*/
this.emptyMessage = "No shortcuts available";
/**
* @ignore
*/
this.showing = false;
this.bodyPortalHost = new DomPortalOutlet(document.body, this.componentFactoryResolver, this.appRef, this.injector);
}
/**
* The shortcut to show/hide the help screen
*/
set key(value) {
this._key = value;
if (!value) {
return;
}
if (this.clearIds) {
this.keyboard.remove(this.clearIds);
}
this.clearIds = this.addShortcut({
key: value,
preventDefault: true,
command: () => this.toggle(),
description: this.keyDescription,
label: this.keyLabel
});
}
addShortcut(shortcut) {
return this.keyboard.add(shortcut);
}
set closeKey(value) {
this._closeKey = value;
if (!value) {
return;
}
if (this.closeKeyIds) {
this.keyboard.remove(this.closeKeyIds);
}
this.closeKeyIds = this.addShortcut({
key: value,
preventDefault: true,
command: () => this.hide(),
description: this.closeKeyDescription,
label: this.closeKeyDescription
});
}
/**
* Reveal the help screen manually.
*/
reveal() {
this.hide();
if (this.disableScrolling) {
disableScroll(`.${this.className}`);
}
const portal = new TemplatePortal(this.template, this.viewContainer);
this.bodyPortalHost.attach(portal);
this.showing = true;
return this;
}
/**
* Check if help screen is visible.
* @returns boolean
*/
visible() {
return this.bodyPortalHost.hasAttached();
}
/**
* Hide the help screen manually.
*/
hide() {
if (this.disableScrolling) {
enableScroll();
}
if (!this.bodyPortalHost.hasAttached()) {
return this;
}
this.bodyPortalHost.detach();
this.showing = false;