├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── karma.conf.js ├── ng-package.json ├── package.json ├── src ├── lib │ ├── hotkey.model.ts │ ├── hotkey.module.ts │ ├── hotkey.options.ts │ ├── hotkeys-cheatsheet │ │ ├── hotkeys-cheatsheet.component.css │ │ ├── hotkeys-cheatsheet.component.html │ │ ├── hotkeys-cheatsheet.component.spec.ts │ │ └── hotkeys-cheatsheet.component.ts │ ├── hotkeys.directive.ts │ └── hotkeys.service.ts ├── public-api.ts └── test.ts ├── tsconfig.json ├── tsconfig.lib.json ├── tsconfig.lib.prod.json ├── tsconfig.spec.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | *.iml 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | .history/* 34 | 35 | # misc 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | .angular 45 | 46 | # System Files 47 | .DS_Store 48 | Thumbs.db 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nick Richardson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular2-hotkeys 2 | Angular 16 and Ivy Compatible. Older versions might work but isn't officially tested. 3 | 4 | ## Versions compatibility 5 | v2.4.0 - Angular 11 (most likely lower Angular versions) 6 | 7 | v13.*.* - Angular 13 (most likely Angular 12) 8 | 9 | v15.*.* - Angular 15 10 | 11 | v16.*.* - Angular 16 12 | 13 | ## Installation 14 | 15 | To install this library, run: 16 | 17 | ```bash 18 | $ npm install angular2-hotkeys --save 19 | ``` 20 | 21 | ## Examples 22 | First, import the HotkeyModule into your root AppModule 23 | 24 | ```typescript 25 | import {HotkeyModule} from 'angular2-hotkeys'; 26 | ``` 27 | 28 | Then, add HotkeyModule.forRoot() to your AppModule's import array 29 | 30 | ```typescript 31 | @NgModule({ 32 | imports : [CommonModule, HotkeyModule.forRoot(), ...], 33 | }) 34 | export class AppModule {} 35 | ``` 36 | 37 | If you have any sub/feature modules that also use hotkeys, import the HotkeyModule (but NOT .forRoot()) 38 | ```typescript 39 | @NgModule({ 40 | imports : [CommonModule, HotkeyModule, ...], 41 | }) 42 | export class SharedModule {} 43 | ``` 44 | 45 | Then inject the service into your constructor and add a new hotkey 46 | 47 | ```typescript 48 | constructor(private _hotkeysService: HotkeysService) { 49 | this._hotkeysService.add(new Hotkey('meta+shift+g', (event: KeyboardEvent): boolean => { 50 | console.log('Typed hotkey'); 51 | return false; // Prevent bubbling 52 | })); 53 | } 54 | ``` 55 | It also handles passing an array of hotkey combinations for a single callback 56 | ```typescript 57 | this._hotkeysService.add(new Hotkey(['meta+shift+g', 'alt+shift+s'], (event: KeyboardEvent, combo: string): ExtendedKeyboardEvent => { 58 | console.log('Combo: ' + combo); // 'Combo: meta+shift+g' or 'Combo: alt+shift+s' 59 | let e: ExtendedKeyboardEvent = event; 60 | e.returnValue = false; // Prevent bubbling 61 | return e; 62 | })); 63 | ``` 64 | 65 | Your callback must return either a boolean or an "ExtendedKeyboardEvent". 66 | 67 | For more information on what hotkeys can be used, check out 68 | 69 | This library is a work in progress and any issues/pull-requests are welcomed! 70 | Based off of the [angular-hotkeys library](https://github.com/chieffancypants/angular-hotkeys) 71 | 72 | ## Cheat Sheet 73 | 74 | To enable the cheat sheet, simply add `` to your top level component template. 75 | The `HotkeysService` will automatically register the `?` key combo to toggle the cheat sheet. 76 | 77 | **NB!** Only hotkeys that have a description will apear on the cheat sheet. The Hotkey constructor takes a description as 78 | an optional fourth parameter as a string or optionally as a function for dynamic descriptions. 79 | 80 | ```typescript 81 | this._hotkeysService.add(new Hotkey('meta+shift+g', (event: KeyboardEvent): boolean => { 82 | console.log('Secret message'); 83 | return false; 84 | }, undefined, 'Send a secret message to the console.')); 85 | ``` 86 | 87 | The third parameter, given as `undefined`, can be used to allow the Hotkey to fire in INPUT, SELECT or TEXTAREA tags. 88 | 89 | ### Cheat Sheet Customization 90 | 91 | 1. You can now pass in custom options in `HotkeyModule.forRoot(options: IHotkeyOptions)`. 92 | 93 | ```typescript 94 | export interface IHotkeyOptions { 95 | /** 96 | * Disable the cheat sheet popover dialog? Default: false 97 | */ 98 | disableCheatSheet?: boolean; 99 | /** 100 | * Key combination to trigger the cheat sheet. Default: '?' 101 | */ 102 | cheatSheetHotkey?: string; 103 | /** 104 | * Use also ESC for closing the cheat sheet. Default: false 105 | */ 106 | cheatSheetCloseEsc?: boolean; 107 | /** 108 | * Description for the ESC key for closing the cheat sheet (if enabed). Default: 'Hide this help menu' 109 | */ 110 | cheatSheetCloseEscDescription?: string; 111 | /** 112 | * Description for the cheat sheet hot key in the cheat sheet. Default: 'Show / hide this help menu' 113 | */ 114 | cheatSheetDescription?: string; 115 | }; 116 | ``` 117 | 118 | 2. You can also customize the title of the cheat sheet component. 119 | 120 | ```html 121 | 122 | 123 | ``` 124 | 125 | ## TODO 126 | 1. Create unit and E2E tests 127 | 128 | ## Development 129 | 130 | To generate all `* 131 | }.js`, `*.js.map` and `*.d.ts` files: 132 | 133 | ```bash 134 | $ npm run tsc 135 | ``` 136 | 137 | ## License 138 | 139 | MIT © [Nick Richardson](nick.richardson@mediapixeldesign.com) 140 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "./", 8 | "projects": { 9 | "angular2-hotkeys": { 10 | "projectType": "library", 11 | "root": "./", 12 | "sourceRoot": "src", 13 | "prefix": "lib", 14 | "architect": { 15 | "build": { 16 | "builder": "@angular-devkit/build-angular:ng-packagr", 17 | "options": { 18 | "tsConfig": "tsconfig.lib.json", 19 | "project": "ng-package.json" 20 | }, 21 | "configurations": { 22 | "production": { 23 | "tsConfig": "tsconfig.lib.prod.json" 24 | } 25 | } 26 | }, 27 | "test": { 28 | "builder": "@angular-devkit/build-angular:karma", 29 | "options": { 30 | "main": "src/test.ts", 31 | "tsConfig": "tsconfig.spec.json", 32 | "karmaConfig": "karma.conf.js" 33 | } 34 | }, 35 | "lint": { 36 | "builder": "@angular-devkit/build-angular:tslint", 37 | "options": { 38 | "tsConfig": [ 39 | "tsconfig.lib.json", 40 | "tsconfig.spec.json" 41 | ], 42 | "exclude": [ 43 | "**/node_modules/**" 44 | ] 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/angular2-hotkeys'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "./dist", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | }, 7 | "allowedNonPeerDependencies": [ 8 | "mousetrap", 9 | "@types/mousetrap" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-hotkeys", 3 | "version": "16.0.1", 4 | "scripts": { 5 | "build": "ng build angular2-hotkeys", 6 | "build:release": "ng build angular2-hotkeys --configuration production", 7 | "build:watch": "ng build angular2-hotkeys --watch", 8 | "ng": "ng", 9 | "lint": "ng lint", 10 | "release": "npm run build:release && npm publish dist/", 11 | "release:beta": "npm run build:release && npm publish dist/ --tag beta", 12 | "start": "ng serve", 13 | "test": "ng test" 14 | }, 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:brtnshrdr/angular2-hotkeys.git" 19 | }, 20 | "author": { 21 | "name": "Nick Richardson", 22 | "email": "nick.richardson@mediapixeldesign.com" 23 | }, 24 | "keywords": [ 25 | "angular", 26 | "angular2", 27 | "hotkeys", 28 | "keyboard", 29 | "shortcut" 30 | ], 31 | "bugs": { 32 | "url": "git@github.com:brtnshrdr/angular2-hotkeys.git/issues" 33 | }, 34 | "dependencies": { 35 | "mousetrap": "^1.6.5", 36 | "@types/mousetrap": "^1.6.9", 37 | "tslib": "^2.3.1" 38 | }, 39 | "devDependencies": { 40 | "@angular-devkit/build-angular": "^16.2.0", 41 | "@angular-devkit/core": "^16.2.0", 42 | "@angular/animations": "^16.2.0", 43 | "@angular/cli": "^16.2.0", 44 | "@angular/common": "^16.2.0", 45 | "@angular/compiler": "^16.2.0", 46 | "@angular/compiler-cli": "^16.2.0", 47 | "@angular/core": "^16.2.0", 48 | "@angular/forms": "^16.2.0", 49 | "@angular/language-service": "^16.2.0", 50 | "@angular/platform-browser": "^16.2.0", 51 | "@angular/platform-browser-dynamic": "^16.2.0", 52 | "@angular/router": "^16.2.0", 53 | "@types/jasmine": "~3.10.3", 54 | "@types/jasminewd2": "~2.0.10", 55 | "@types/node": "^18.11.9 ", 56 | "codelyzer": "^6.0.2", 57 | "jasmine-core": "~4.0.0", 58 | "jasmine-spec-reporter": "~7.0.0", 59 | "karma": "~6.3.13", 60 | "karma-chrome-launcher": "~3.1.0", 61 | "karma-coverage-istanbul-reporter": "~3.0.3", 62 | "karma-jasmine": "~4.0.1", 63 | "karma-jasmine-html-reporter": "^1.7.0", 64 | "ng-packagr": "^16.2.0", 65 | "protractor": "~7.0.0", 66 | "rxjs": "~7.5.2", 67 | "ts-node": "~10.4.0", 68 | "tslint": "~6.1.0", 69 | "typescript": "~5.1.6", 70 | "zone.js": "~0.13.1" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/hotkey.model.ts: -------------------------------------------------------------------------------- 1 | export interface ExtendedKeyboardEvent extends KeyboardEvent { 2 | returnValue: boolean; // IE returnValue 3 | } 4 | 5 | export class Hotkey { 6 | private formattedHotkey: string[]; 7 | 8 | static symbolize(combo: string): string { 9 | const map: any = { 10 | command: '\u2318', // ⌘ 11 | shift: '\u21E7', // ⇧ 12 | left: '\u2190', // ← 13 | right: '\u2192', // → 14 | up: '\u2191', // ↑ 15 | down: '\u2193', // ↓ 16 | // tslint:disable-next-line:object-literal-key-quotes 17 | 'return': '\u23CE', // ⏎ 18 | backspace: '\u232B' // ⌫ 19 | }; 20 | const comboSplit: string[] = combo.split('+'); 21 | 22 | for (let i = 0; i < comboSplit.length; i++) { 23 | // try to resolve command / ctrl based on OS: 24 | if (comboSplit[i] === 'mod') { 25 | if (window.navigator && window.navigator.platform.indexOf('Mac') >= 0) { 26 | comboSplit[i] = 'command'; 27 | } else { 28 | comboSplit[i] = 'ctrl'; 29 | } 30 | } 31 | 32 | comboSplit[i] = map[comboSplit[i]] || comboSplit[i]; 33 | } 34 | 35 | return comboSplit.join(' + '); 36 | } 37 | 38 | /** 39 | * Creates a new Hotkey for Mousetrap binding 40 | * 41 | * @param combo mousetrap key binding 42 | * @param callback method to call when key is pressed 43 | * @param allowIn an array of tag names to allow this combo in ('INPUT', 'SELECT', and/or 'TEXTAREA') 44 | * @param description description for the help menu 45 | * @param action the type of event to listen for (for mousetrap) 46 | * @param persistent if true, the binding is preserved upon route changes 47 | */ 48 | constructor(public combo: string | string[], public callback: (event: KeyboardEvent, combo: string) => ExtendedKeyboardEvent | boolean, 49 | public allowIn?: string[], public description?: string | Function, public action?: string, 50 | public persistent?: boolean) { 51 | this.combo = (Array.isArray(combo) ? combo : [combo as string]); 52 | this.allowIn = allowIn || []; 53 | this.description = description || ''; 54 | } 55 | 56 | get formatted(): string[] { 57 | if (!this.formattedHotkey) { 58 | 59 | const sequence: string[] = [...this.combo] as Array; 60 | for (let i = 0; i < sequence.length; i++) { 61 | sequence[i] = Hotkey.symbolize(sequence[i]); 62 | } 63 | this.formattedHotkey = sequence; 64 | } 65 | return this.formattedHotkey; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/hotkey.module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders, NgModule } from '@angular/core'; 2 | import { HotkeysDirective } from './hotkeys.directive'; 3 | import { HotkeysCheatsheetComponent } from './hotkeys-cheatsheet/hotkeys-cheatsheet.component'; 4 | import { CommonModule } from '@angular/common'; 5 | import { HotkeyOptions, IHotkeyOptions } from './hotkey.options'; 6 | import { HotkeysService } from './hotkeys.service'; 7 | 8 | @NgModule({ 9 | declarations: [HotkeysDirective, HotkeysCheatsheetComponent], 10 | imports: [CommonModule], 11 | exports: [HotkeysDirective, HotkeysCheatsheetComponent] 12 | }) 13 | export class HotkeyModule { 14 | // noinspection JSUnusedGlobalSymbols 15 | static forRoot(options: IHotkeyOptions = {}): ModuleWithProviders { 16 | return { 17 | ngModule : HotkeyModule, 18 | providers : [ 19 | HotkeysService, 20 | {provide : HotkeyOptions, useValue : options} 21 | ] 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/hotkey.options.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export interface IHotkeyOptions { 4 | /** 5 | * Disable the cheat sheet popover dialog? Default: false 6 | */ 7 | disableCheatSheet?: boolean; 8 | /** 9 | * Key combination to trigger the cheat sheet. Default: '?' 10 | */ 11 | cheatSheetHotkey?: string; 12 | 13 | /** 14 | * Use also ESC for closing the cheat sheet. Default: false 15 | */ 16 | cheatSheetCloseEsc?: boolean; 17 | /** 18 | * Description for the ESC key for closing the cheat sheet (if enabed). Default: 'Hide this help menu' 19 | */ 20 | cheatSheetCloseEscDescription?: string; 21 | /** 22 | * Description for the cheat sheet hot key in the cheat sheet. Default: 'Show / hide this help menu' 23 | */ 24 | cheatSheetDescription?: string; 25 | } 26 | 27 | export const HotkeyOptions = new InjectionToken('HotkeyOptions'); 28 | -------------------------------------------------------------------------------- /src/lib/hotkeys-cheatsheet/hotkeys-cheatsheet.component.css: -------------------------------------------------------------------------------- 1 | .cfp-hotkeys-container { 2 | display: table !important; 3 | position: fixed; 4 | width: 100%; 5 | height: 100%; 6 | top: 0; 7 | left: 0; 8 | color: #333; 9 | font-size: 1em; 10 | background-color: rgba(255, 255, 255, 0.9); 11 | } 12 | 13 | .cfp-hotkeys-container.fade { 14 | z-index: -1024; 15 | visibility: hidden; 16 | opacity: 0; 17 | -webkit-transition: opacity 0.15s linear; 18 | -moz-transition: opacity 0.15s linear; 19 | -o-transition: opacity 0.15s linear; 20 | transition: opacity 0.15s linear; 21 | } 22 | 23 | .cfp-hotkeys-container.fade.in { 24 | z-index: 10002; 25 | visibility: visible; 26 | opacity: 1; 27 | } 28 | 29 | .cfp-hotkeys-title { 30 | font-weight: bold; 31 | text-align: center; 32 | font-size: 1.2em; 33 | } 34 | 35 | .cfp-hotkeys { 36 | width: 100%; 37 | height: 100%; 38 | display: table-cell; 39 | vertical-align: middle; 40 | } 41 | 42 | .cfp-hotkeys table { 43 | margin: auto; 44 | color: #333; 45 | } 46 | 47 | .cfp-content { 48 | display: table-cell; 49 | vertical-align: middle; 50 | } 51 | 52 | .cfp-hotkeys-keys { 53 | padding: 5px; 54 | text-align: right; 55 | } 56 | 57 | .cfp-hotkeys-key { 58 | display: inline-block; 59 | color: #fff; 60 | background-color: #333; 61 | border: 1px solid #333; 62 | border-radius: 5px; 63 | text-align: center; 64 | margin-right: 5px; 65 | box-shadow: inset 0 1px 0 #666, 0 1px 0 #bbb; 66 | padding: 5px 9px; 67 | font-size: 1em; 68 | } 69 | 70 | .cfp-hotkeys-text { 71 | padding-left: 10px; 72 | font-size: 1em; 73 | } 74 | 75 | .cfp-hotkeys-close { 76 | position: fixed; 77 | top: 20px; 78 | right: 20px; 79 | font-size: 2em; 80 | font-weight: bold; 81 | padding: 5px 10px; 82 | border: 1px solid #ddd; 83 | border-radius: 5px; 84 | min-height: 45px; 85 | min-width: 45px; 86 | text-align: center; 87 | } 88 | 89 | .cfp-hotkeys-close:hover { 90 | background-color: #fff; 91 | cursor: pointer; 92 | } 93 | 94 | @media all and (max-width: 500px) { 95 | .cfp-hotkeys { 96 | font-size: 0.8em; 97 | } 98 | } 99 | 100 | @media all and (min-width: 750px) { 101 | .cfp-hotkeys { 102 | font-size: 1.2em; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/lib/hotkeys-cheatsheet/hotkeys-cheatsheet.component.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/lib/hotkeys-cheatsheet/hotkeys-cheatsheet.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { HotkeysCheatsheetComponent } from './hotkeys-cheatsheet.component'; 3 | 4 | describe('HotkeysCheatsheetComponent', () => { 5 | let component: HotkeysCheatsheetComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach(async(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [HotkeysCheatsheetComponent] 11 | }) 12 | .compileComponents(); 13 | })); 14 | 15 | beforeEach(() => { 16 | fixture = TestBed.createComponent(HotkeysCheatsheetComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/lib/hotkeys-cheatsheet/hotkeys-cheatsheet.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnDestroy, OnInit } from '@angular/core'; 2 | import { Hotkey } from '../hotkey.model'; 3 | import { HotkeysService } from '../hotkeys.service'; 4 | import {BehaviorSubject, Subscription} from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'hotkeys-cheatsheet', 8 | templateUrl: './hotkeys-cheatsheet.component.html', 9 | styleUrls: ['./hotkeys-cheatsheet.component.css'] 10 | }) 11 | export class HotkeysCheatsheetComponent implements OnInit, OnDestroy { 12 | helpVisible$ = new BehaviorSubject(false); 13 | @Input() title = 'Keyboard Shortcuts:'; 14 | subscription: Subscription; 15 | 16 | hotkeys: Hotkey[]; 17 | 18 | constructor(private hotkeysService: HotkeysService) { 19 | } 20 | 21 | public ngOnInit(): void { 22 | this.subscription = this.hotkeysService.cheatSheetToggle.subscribe((isOpen) => { 23 | if (isOpen !== false) { 24 | this.hotkeys = this.hotkeysService.hotkeys.filter(hotkey => hotkey.description); 25 | } 26 | 27 | if (isOpen === false) { 28 | this.helpVisible$.next(false); 29 | } else { 30 | this.toggleCheatSheet(); 31 | } 32 | }); 33 | } 34 | 35 | public ngOnDestroy(): void { 36 | if (this.subscription) { 37 | this.subscription.unsubscribe(); 38 | } 39 | } 40 | 41 | public toggleCheatSheet(): void { 42 | this.helpVisible$.next(!this.helpVisible$.value); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/hotkeys.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; 2 | import { ExtendedKeyboardEvent, Hotkey } from './hotkey.model'; 3 | import { HotkeysService } from './hotkeys.service'; 4 | import { MousetrapInstance } from 'mousetrap'; 5 | import * as Mousetrap from 'mousetrap'; 6 | 7 | @Directive({ 8 | selector: '[hotkeys]', 9 | providers: [HotkeysService] 10 | }) 11 | export class HotkeysDirective implements OnInit, OnDestroy { 12 | @Input() hotkeys: { [combo: string]: (event: KeyboardEvent, combo: string) => ExtendedKeyboardEvent }[]; 13 | 14 | private mousetrap: MousetrapInstance; 15 | private hotkeysList: Hotkey[] = []; 16 | private oldHotkeys: Hotkey[] = []; 17 | 18 | constructor(private hotkeysService: HotkeysService, private elementRef: ElementRef) { 19 | // Bind hotkeys to the current element (and any children) 20 | this.mousetrap = new (Mousetrap as any).default(this.elementRef.nativeElement); 21 | } 22 | 23 | ngOnInit() { 24 | for (const hotkey of this.hotkeys) { 25 | const combo = Object.keys(hotkey)[0]; 26 | const hotkeyObj: Hotkey = new Hotkey(combo, hotkey[combo]); 27 | const oldHotkey: Hotkey = this.hotkeysService.get(combo) as Hotkey; 28 | if (oldHotkey !== null) { // We let the user overwrite callbacks temporarily if you specify it in HTML 29 | this.oldHotkeys.push(oldHotkey); 30 | this.hotkeysService.remove(oldHotkey); 31 | } 32 | this.hotkeysList.push(hotkeyObj); 33 | this.mousetrap.bind(hotkeyObj.combo, hotkeyObj.callback); 34 | } 35 | } 36 | 37 | ngOnDestroy() { 38 | for (const hotkey of this.hotkeysList) { 39 | this.mousetrap.unbind(hotkey.combo); 40 | } 41 | this.hotkeysService.add(this.oldHotkeys); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/hotkeys.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { Hotkey } from './hotkey.model'; 3 | import { Subject } from 'rxjs'; 4 | import { HotkeyOptions, IHotkeyOptions } from './hotkey.options'; 5 | import { MousetrapInstance } from 'mousetrap'; 6 | import * as Mousetrap from 'mousetrap'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class HotkeysService { 12 | hotkeys: Hotkey[] = []; 13 | pausedHotkeys: Hotkey[] = []; 14 | mousetrap: MousetrapInstance; 15 | cheatSheetToggle: Subject = new Subject(); 16 | 17 | private preventIn = ['INPUT', 'SELECT', 'TEXTAREA']; 18 | 19 | constructor(@Inject(HotkeyOptions) private options: IHotkeyOptions) { 20 | // noinspection JSUnusedGlobalSymbols,JSUnusedLocalSymbols 21 | Mousetrap.prototype.stopCallback = (event: KeyboardEvent, element: HTMLElement, combo: string, callback: Function) => { 22 | // if the element has the class "mousetrap" then no need to stop 23 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 24 | return false; 25 | } 26 | return (element.contentEditable && element.contentEditable === 'true'); 27 | }; 28 | this.mousetrap = new (Mousetrap as any).default(); 29 | this.initCheatSheet(); 30 | } 31 | 32 | private initCheatSheet() { 33 | if (!this.options.disableCheatSheet) { 34 | this.add(new Hotkey( 35 | this.options.cheatSheetHotkey || '?', 36 | function(_: KeyboardEvent) { 37 | this.cheatSheetToggle.next(); 38 | }.bind(this), 39 | [], 40 | this.options.cheatSheetDescription || 'Show / hide this help menu', 41 | )); 42 | } 43 | 44 | if (this.options.cheatSheetCloseEsc) { 45 | this.add(new Hotkey( 46 | 'esc', 47 | function(_: KeyboardEvent) { 48 | this.cheatSheetToggle.next(false); 49 | }.bind(this), 50 | ['HOTKEYS-CHEATSHEET'], 51 | this.options.cheatSheetCloseEscDescription || 'Hide this help menu', 52 | )); 53 | } 54 | 55 | } 56 | 57 | add(hotkey: Hotkey | Hotkey[], specificEvent?: string): Hotkey | Hotkey[] { 58 | if (Array.isArray(hotkey)) { 59 | const temp: Hotkey[] = []; 60 | for (const key of hotkey) { 61 | temp.push(this.add(key, specificEvent) as Hotkey); 62 | } 63 | return temp; 64 | } 65 | this.remove(hotkey); 66 | this.hotkeys.push(hotkey as Hotkey); 67 | this.mousetrap.bind((hotkey as Hotkey).combo, (event: KeyboardEvent, combo: string) => { 68 | let shouldExecute = true; 69 | 70 | // if the callback is executed directly `hotkey.get('w').callback()` 71 | // there will be no event, so just execute the callback. 72 | if (event) { 73 | const target: HTMLElement = (event.target || event.srcElement) as HTMLElement; // srcElement is IE only 74 | const nodeName: string = target.nodeName.toUpperCase(); 75 | 76 | // check if the input has a mousetrap class, and skip checking preventIn if so 77 | if ((' ' + target.className + ' ').indexOf(' mousetrap ') > -1) { 78 | shouldExecute = true; 79 | } else if (this.preventIn.indexOf(nodeName) > -1 && 80 | (hotkey as Hotkey).allowIn.map(allow => allow.toUpperCase()).indexOf(nodeName) === -1) { 81 | // don't execute callback if the event was fired from inside an element listed in preventIn but not in allowIn 82 | shouldExecute = false; 83 | } 84 | } 85 | 86 | if (shouldExecute) { 87 | return (hotkey as Hotkey).callback.apply(this, [event, combo]); 88 | } 89 | }, specificEvent); 90 | return hotkey; 91 | } 92 | 93 | remove(hotkey?: Hotkey | Hotkey[], specificEvent?: string): Hotkey | Hotkey[] { 94 | const temp: Hotkey[] = []; 95 | if (!hotkey) { 96 | for (const key of this.hotkeys) { 97 | temp.push(this.remove(key, specificEvent) as Hotkey); 98 | } 99 | return temp; 100 | } 101 | if (Array.isArray(hotkey)) { 102 | for (const key of hotkey) { 103 | temp.push(this.remove(key) as Hotkey); 104 | } 105 | return temp; 106 | } 107 | const index = this.findHotkey(hotkey as Hotkey); 108 | if (index > -1) { 109 | this.hotkeys.splice(index, 1); 110 | this.mousetrap.unbind((hotkey as Hotkey).combo, specificEvent); 111 | return hotkey; 112 | } 113 | return null; 114 | } 115 | 116 | get(combo?: string | string[]): Hotkey | Hotkey[] { 117 | if (!combo) { 118 | return this.hotkeys; 119 | } 120 | if (Array.isArray(combo)) { 121 | const temp: Hotkey[] = []; 122 | for (const key of combo) { 123 | temp.push(this.get(key) as Hotkey); 124 | } 125 | return temp; 126 | } 127 | for (const hotkey of this.hotkeys) { 128 | if (hotkey.combo.indexOf(combo as string) > -1) { 129 | return hotkey; 130 | } 131 | } 132 | return null; 133 | } 134 | 135 | // noinspection JSUnusedGlobalSymbols 136 | pause(hotkey?: Hotkey | Hotkey[]): Hotkey | Hotkey[] { 137 | if (!hotkey) { 138 | return this.pause(this.hotkeys); 139 | } 140 | if (Array.isArray(hotkey)) { 141 | const temp: Hotkey[] = []; 142 | for (const key of hotkey.slice()) { 143 | temp.push(this.pause(key) as Hotkey); 144 | } 145 | return temp; 146 | } 147 | this.remove(hotkey); 148 | this.pausedHotkeys.push(hotkey as Hotkey); 149 | return hotkey; 150 | } 151 | 152 | // noinspection JSUnusedGlobalSymbols 153 | unpause(hotkey?: Hotkey | Hotkey[]): Hotkey | Hotkey[] { 154 | if (!hotkey) { 155 | return this.unpause(this.pausedHotkeys); 156 | } 157 | if (Array.isArray(hotkey)) { 158 | const temp: Hotkey[] = []; 159 | for (const key of hotkey.slice()) { 160 | temp.push(this.unpause(key) as Hotkey); 161 | } 162 | return temp; 163 | } 164 | const index: number = this.pausedHotkeys.indexOf(hotkey as Hotkey); 165 | if (index > -1) { 166 | this.add(hotkey); 167 | return this.pausedHotkeys.splice(index, 1); 168 | } 169 | return null; 170 | } 171 | 172 | // noinspection JSUnusedGlobalSymbols 173 | reset() { 174 | this.mousetrap.reset(); 175 | this.hotkeys = []; 176 | this.pausedHotkeys = []; 177 | this.initCheatSheet(); 178 | } 179 | 180 | private findHotkey(hotkey: Hotkey): number { 181 | return this.hotkeys.indexOf(hotkey); 182 | } 183 | } 184 | 185 | -------------------------------------------------------------------------------- /src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of angular2-hotkeys 3 | */ 4 | 5 | export * from './lib/hotkeys.service'; 6 | export * from './lib/hotkeys.directive'; 7 | export * from './lib/hotkeys-cheatsheet/hotkeys-cheatsheet.component'; 8 | export * from './lib/hotkey.model'; 9 | export * from './lib/hotkey.options'; 10 | export * from './lib/hotkey.module'; 11 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | // First, initialize the Angular testing environment. 12 | getTestBed().initTestEnvironment( 13 | BrowserDynamicTestingModule, 14 | platformBrowserDynamicTesting() 15 | ); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "ES2022", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ], 21 | "paths": { 22 | "angular2-hotkeys": [ 23 | "dist/" 24 | ] 25 | }, 26 | "useDefineForClassFields": false 27 | }, 28 | "angularCompilerOptions": { 29 | "fullTemplateTypeCheck": true, 30 | "strictInjectionParameters": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/lib", 5 | "declaration": true, 6 | "inlineSources": true, 7 | "types": [], 8 | "lib": [ 9 | "dom", 10 | "es2018" 11 | ] 12 | }, 13 | "angularCompilerOptions": { 14 | "skipTemplateCodegen": true, 15 | "strictMetadataEmit": true, 16 | "enableResourceInlining": true 17 | }, 18 | "exclude": [ 19 | "src/test.ts", 20 | "**/*.spec.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "angularCompilerOptions": { 4 | "compilationMode": "partial" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-parens": false, 15 | "arrow-return-shorthand": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "interface-name": false, 24 | "curly": true, 25 | "max-classes-per-file": false, 26 | "max-line-length": [ 27 | true, 28 | { 29 | "limit": 140, 30 | "ignore-pattern": "^import |^export {(.*?)}" 31 | } 32 | ], 33 | "member-access": false, 34 | "eofline": true, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "import-spacing": true, 47 | "indent": { 48 | "options": [ 49 | "spaces" 50 | ] 51 | }, 52 | "no-consecutive-blank-lines": false, 53 | "no-console": [ 54 | true, 55 | "debug", 56 | "info", 57 | "time", 58 | "timeEnd", 59 | "trace" 60 | ], 61 | "no-empty": false, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-non-null-assertion": true, 67 | "no-redundant-jsdoc": true, 68 | "no-switch-case-fall-through": true, 69 | "no-var-requires": false, 70 | "object-literal-key-quotes": [ 71 | true, 72 | "as-needed" 73 | ], 74 | "object-literal-sort-keys": false, 75 | "ordered-imports": false, 76 | "quotemark": [ 77 | true, 78 | "single" 79 | ], 80 | "trailing-comma": false, 81 | "component-class-suffix": true, 82 | "contextual-lifecycle": true, 83 | "directive-class-suffix": true, 84 | "no-conflicting-lifecycle": true, 85 | "no-host-metadata-property": true, 86 | "no-input-rename": true, 87 | "no-inputs-metadata-property": true, 88 | "no-output-native": true, 89 | "no-output-on-prefix": true, 90 | "no-output-rename": true, 91 | "no-outputs-metadata-property": true, 92 | "template-banana-in-box": true, 93 | "semicolon": { 94 | "options": [ 95 | "always" 96 | ] 97 | }, 98 | "space-before-function-paren": { 99 | "options": { 100 | "anonymous": "never", 101 | "asyncArrow": "always", 102 | "constructor": "never", 103 | "method": "never", 104 | "named": "never" 105 | } 106 | }, 107 | "template-no-negated-async": true, 108 | "use-lifecycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "typedef-whitespace": { 111 | "options": [ 112 | { 113 | "call-signature": "nospace", 114 | "index-signature": "nospace", 115 | "parameter": "nospace", 116 | "property-declaration": "nospace", 117 | "variable-declaration": "nospace" 118 | }, 119 | { 120 | "call-signature": "onespace", 121 | "index-signature": "onespace", 122 | "parameter": "onespace", 123 | "property-declaration": "onespace", 124 | "variable-declaration": "onespace" 125 | } 126 | ] 127 | }, 128 | "directive-selector": [ 129 | false, 130 | "attribute", 131 | "lib", 132 | "camelCase" 133 | ], 134 | "component-selector": [ 135 | false, 136 | "element", 137 | "lib", 138 | "kebab-case" 139 | ], 140 | "variable-name": { 141 | "options": [ 142 | "ban-keywords", 143 | "check-format", 144 | "allow-pascal-case" 145 | ] 146 | }, 147 | "whitespace": { 148 | "options": [ 149 | "check-branch", 150 | "check-decl", 151 | "check-operator", 152 | "check-separator", 153 | "check-type", 154 | "check-typecast" 155 | ] 156 | } 157 | } 158 | } 159 | --------------------------------------------------------------------------------