├── .gitignore ├── bower.json ├── .editorconfig ├── LICENSE ├── package.json ├── test ├── karma.conf.js └── hotkeys.coffee ├── src ├── hotkeys.css └── hotkeys.js ├── Gruntfile.js ├── .jshintrc └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/coverage 3 | bower_components/ 4 | .coveralls.yml 5 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-hotkeys", 3 | "main": [ 4 | "build/hotkeys.js", 5 | "build/hotkeys.css" 6 | ], 7 | "ignore": [ 8 | "**/.*", 9 | "node_modules", 10 | "components", 11 | "test", 12 | "example" 13 | ], 14 | "devDependencies": { 15 | "mousetrap": "~1.5.2", 16 | "angular-mocks": "~1.2.15", 17 | "angular-route": "~1.2.15" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Wes Cruver 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-hotkeys", 3 | "author": "Wes Cruver", 4 | "version": "1.7.0", 5 | "license": "MIT", 6 | "description": "Automatic keyboard shortcuts for your Angular Apps", 7 | "homepage": "https://chieffancypants.github.io/angular-hotkeys", 8 | "main": "build/hotkeys.js", 9 | "keywords": [ 10 | "angular", 11 | "angularjs", 12 | "keyboard", 13 | "shortcut", 14 | "hotkeys" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/chieffancypants/angular-hotkeys.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/chieffancypants/angular-hotkeys/issues" 22 | }, 23 | "scripts": { 24 | "test": "node_modules/karma/bin/karma start test/karma.conf.js" 25 | }, 26 | "devDependencies": { 27 | "grunt": "~0.4.1", 28 | "grunt-contrib-concat": "^0.5.1", 29 | "grunt-contrib-cssmin": "^0.12.3", 30 | "grunt-contrib-jshint": "~0.6.4", 31 | "grunt-contrib-uglify": "^0.9.1", 32 | "grunt-contrib-watch": "^0.6.1", 33 | "grunt-karma": "^0.11.0", 34 | "grunt-ng-annotate": "^0.3.0", 35 | "karma": "~0.12.0", 36 | "karma-chrome-launcher": "~0.1.0", 37 | "karma-coffee-preprocessor": "~0.1.0", 38 | "karma-coverage": "~0.1.0", 39 | "karma-firefox-launcher": "~0.1.0", 40 | "karma-html2js-preprocessor": "~0.1.0", 41 | "karma-jasmine": "~0.1.3", 42 | "karma-phantomjs-launcher": "^0.2.0", 43 | "karma-script-launcher": "~0.1.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sun Sep 15 2013 20:18:09 GMT-0400 (EDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | frameworks: ['jasmine'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | '../bower_components/angular/angular.js', 18 | '../bower_components/angular-mocks/angular-mocks.js', 19 | '../bower_components/angular-route/angular-route.js', 20 | '../bower_components/mousetrap/mousetrap.js', 21 | '../bower_components/mousetrap/tests/libs/key-event.js', 22 | '../src/*.js', 23 | '*.coffee' 24 | ], 25 | 26 | 27 | // list of files to exclude 28 | exclude: [ 29 | 30 | ], 31 | 32 | 33 | // test results reporter to use 34 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 35 | reporters: ['progress', 'coverage'], 36 | 37 | 38 | // web server port 39 | port: 9876, 40 | 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | 46 | // level of logging 47 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 48 | logLevel: config.LOG_INFO, 49 | 50 | 51 | // enable / disable watching file and executing tests whenever any file changes 52 | autoWatch: true, 53 | 54 | 55 | // Start these browsers, currently available: 56 | // - Chrome 57 | // - ChromeCanary 58 | // - Firefox 59 | // - Opera 60 | // - Safari (only Mac) 61 | // - PhantomJS 62 | // - IE (only Windows) 63 | browsers: ['PhantomJS'], 64 | 65 | coverageReporter: { 66 | type : 'html', 67 | dir : 'coverage/', 68 | }, 69 | 70 | preprocessors: { 71 | '../src/*.js': ['coverage'], 72 | '*.coffee': 'coffee' 73 | }, 74 | 75 | 76 | // If browser does not capture in given timeout [ms], kill it 77 | captureTimeout: 60000, 78 | 79 | 80 | // Continuous Integration mode 81 | // if true, it capture browsers, run tests and exit 82 | singleRun: true, 83 | 84 | }); 85 | }; 86 | -------------------------------------------------------------------------------- /src/hotkeys.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 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | grunt.initConfig({ 5 | 6 | // Metadata. 7 | pkg: grunt.file.readJSON('package.json'), 8 | banner: '/*! \n * <%= pkg.title || pkg.name %> v<%= pkg.version %>\n' + 9 | ' * <%= pkg.homepage %>\n' + 10 | ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' + 11 | ' * License: <%= pkg.license %>\n' + 12 | ' */\n', 13 | 14 | // Task configuration. 15 | uglify: { 16 | options: { 17 | banner: '<%= banner %>', 18 | report: 'gzip' 19 | }, 20 | build: { 21 | src: ['build/hotkeys.js', 'bower_components/mousetrap/mousetrap.js'], 22 | dest: 'build/hotkeys.min.js' 23 | } 24 | }, 25 | 26 | ngAnnotate: { 27 | options: { 28 | singleQuotes: true, 29 | }, 30 | source: { 31 | expand: true, 32 | cwd: 'src', 33 | src: ['*.js'], 34 | dest: 'build' 35 | } 36 | }, 37 | 38 | cssmin: { 39 | options: { 40 | banner: '<%= banner %>', 41 | report: 'gzip' 42 | }, 43 | minify: { 44 | src: 'src/hotkeys.css', 45 | dest: 'build/hotkeys.min.css' 46 | } 47 | }, 48 | 49 | karma: { 50 | unit: { 51 | configFile: 'test/karma.conf.js', 52 | singleRun: true, 53 | coverageReporter: { 54 | type: 'text', 55 | dir: 'coverage/' 56 | } 57 | }, 58 | watch: { 59 | configFile: 'test/karma.conf.js', 60 | singleRun: false, 61 | reporters: ['progress'] // Don't display coverage 62 | } 63 | }, 64 | 65 | jshint: { 66 | jshintrc: '.jshintrc', 67 | gruntfile: { 68 | src: 'Gruntfile.js' 69 | }, 70 | src: { 71 | src: ['src/*.js'] 72 | } 73 | }, 74 | 75 | concat: { 76 | build: { 77 | options: { 78 | banner: '<%= banner %>' 79 | }, 80 | files: { 81 | 'build/hotkeys.css': 'src/hotkeys.css', 82 | 'build/hotkeys.js': ['build/hotkeys.js', 'bower_components/mousetrap/mousetrap.js'], 83 | } 84 | } 85 | }, 86 | 87 | watch: { 88 | scripts: { 89 | files: ['src/*.js'], 90 | tasks: ['uglify', 'concat:build'], 91 | options: { 92 | spawn: false, 93 | }, 94 | }, 95 | css: { 96 | files: ['src/*.css'], 97 | tasks: ['cssmin', 'concat:build'], 98 | options: { 99 | spawn: false, 100 | }, 101 | } 102 | }, 103 | 104 | }); 105 | 106 | grunt.loadNpmTasks('grunt-contrib-uglify'); 107 | grunt.loadNpmTasks('grunt-ng-annotate'); 108 | grunt.loadNpmTasks('grunt-contrib-jshint'); 109 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 110 | grunt.loadNpmTasks('grunt-contrib-concat'); 111 | grunt.loadNpmTasks('grunt-contrib-watch'); 112 | grunt.loadNpmTasks('grunt-karma'); 113 | 114 | grunt.registerTask('default', ['jshint', 'karma:unit', 'ngAnnotate', 'uglify', 'cssmin', 'concat:build']); 115 | grunt.registerTask('test', ['karma:watch']); 116 | grunt.registerTask('build', ['default']); 117 | 118 | }; 119 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "angular": false 4 | }, 5 | 6 | "bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.). 7 | "curly" : true, // Require {} for every new block or scope. 8 | "eqeqeq" : true, // Require triple equals i.e. `===`. 9 | "forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`. 10 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 11 | "latedef" : true, // Prohibit variable use before definition. 12 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 13 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 14 | "noempty" : true, // Prohibit use of empty blocks. 15 | "nonew" : true, // Prohibit use of constructors for side-effects. 16 | "plusplus" : false, // Prohibit use of `++` & `--`. 17 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 18 | "undef" : true, // Require all non-global variables be declared before they are used. 19 | "strict" : false, // Require `use strict` pragma in every file. 20 | "trailing" : true, // Prohibit trailing whitespaces. 21 | 22 | 23 | // Relaxing options: 24 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 25 | "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 26 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 27 | "eqnull" : false, // Tolerate use of `== null`. 28 | "es5" : false, // Allow EcmaScript 5 syntax. 29 | "esnext" : false, // Allow ES.next specific features such as `const` and `let`. 30 | "evil" : false, // Tolerate use of `eval`. 31 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 32 | "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. 33 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). 34 | "iterator" : false, // Allow usage of __iterator__ property. 35 | "lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block. 36 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 37 | "laxcomma" : false, // Suppress warnings about comma-first coding style. 38 | "loopfunc" : false, // Allow functions to be defined within loops. 39 | "multistr" : false, // Tolerate multi-line strings. 40 | "onecase" : false, // Tolerate switches with just one case. 41 | "proto" : false, // Tolerate __proto__ property. This property is deprecated. 42 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 43 | "scripturl" : false, // Tolerate script-targeted URLs. 44 | "smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. 45 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 46 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 47 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 48 | "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. 49 | 50 | // == Environments ==================================================== 51 | // 52 | // These options pre-define global variables that are exposed by 53 | // popular JavaScript libraries and runtime environments—such as 54 | // browser or node.js. 55 | 56 | "browser" : true, // Standard browser globals e.g. `window`, `document`. 57 | "couch" : false, // Enable globals exposed by CouchDB. 58 | "devel" : false, // Allow development statements e.g. `console.log();`. 59 | "dojo" : false, // Enable globals exposed by Dojo Toolkit. 60 | "jquery" : false, // Enable globals exposed by jQuery JavaScript library. 61 | "mootools" : false, // Enable globals exposed by MooTools JavaScript framework. 62 | "node" : false, // Enable globals available when code is running inside of the NodeJS runtime environment. 63 | "nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape. 64 | "prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework. 65 | "rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment. 66 | "wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host. 67 | 68 | // == JSLint Legacy =================================================== 69 | // 70 | // These options are legacy from JSLint. Aside from bug fixes they will 71 | // not be improved in any way and might be removed at any point. 72 | 73 | "nomen" : false, // Prohibit use of initial or trailing underbars in names. 74 | "onevar" : false, // Allow only one `var` statement per function. 75 | "passfail" : false, // Stop on first error. 76 | "white" : false, // Check against strict whitespace and indentation rules. 77 | 78 | // == Undocumented Options ============================================ 79 | // 80 | // While I've found these options in [example1][2] and [example2][3] 81 | // they are not described in the [JSHint Options documentation][4]. 82 | // 83 | // [4]: http://www.jshint.com/options/ 84 | 85 | "maxerr" : 100, // Maximum errors before stopping. 86 | "predef" : [ // Extra globals. 87 | //"exampleVar", 88 | //"anotherCoolGlobal", 89 | //"iLoveDouglas" 90 | ], 91 | "indent" : 2 // Specify indentation spacing 92 | 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-hotkeys 2 | ================ 3 | Configuration-centric keyboard shortcuts for your Angular apps. 4 | 5 | [![Coverage Status](https://coveralls.io/repos/chieffancypants/angular-hotkeys/badge.png?branch=master)](https://coveralls.io/r/chieffancypants/angular-hotkeys?branch=master) 6 | ![Build Status](https://magnum-ci.com/status/89743485de3e7311dfc9793e26f39b41.png) 7 | 8 | **Requirements**: Angular 1.2+ 9 | 10 | ### Features: 11 | - Define hotkeys on an entire route, automatically binding and unbinding them as you navigate 12 | - Automatic listing of shortcuts when users hit the `?` key 13 | - Super duper unit tests 14 | 15 | 16 | ### Installation: 17 | 18 | #### via bower: 19 | 20 | ``` 21 | $ bower install chieffancypants/angular-hotkeys --save 22 | ``` 23 | 24 | #### via npm: 25 | 26 | ``` 27 | $ npm install angular-hotkeys --save 28 | ``` 29 | 30 | 31 | *please use either the minified or unminified file in the `build` directory* 32 | 33 | ### Why I made this: 34 | Other projects out there rely too heavily on HTML markup for keyboard shortcuts. For example: 35 | 36 | ```html 37 |
38 |
39 |
40 |
41 | ``` 42 | 43 | While this is a great approach for many Angular apps, some applications do not have a 1 to 1 relationship between DOM elements and controller methods. In my case, many methods on the controller were **only** accessible through the keyboard. 44 | 45 | Additionally, this only allows you to pass a function reference, you can't pass arguments to the function you intend to call. So instead of simply calling `seek(currentTime + 30)` and `seek(currentTime + 60)`, I needed to create a ton of helper functions on the scope (such as `forward30` and `forward60`), and litter my HTML like this: 46 | 47 | ```html 48 |
59 |
60 |
61 |
62 | 63 | ``` 64 | 65 | With a few dozen shortcuts, this left the DOM really messy, and with multiple views and directive templates, it was next to impossible to remember where all the different shortcuts were. This became a maintenance nightmare. 66 | 67 | 68 | ### Usage: 69 | You can either define hotkeys in your Controller, or in your Route configuration (or both). To start, though, require the lib as a dependency for your angular app: 70 | 71 | ```js 72 | angular.module('myApp', ['ngRoute', 'cfp.hotkeys']); 73 | ``` 74 | 75 | Behind the scenes, I'm using the [Mousetrap](https://github.com/ccampbell/mousetrap) library to manage the key bindings. Check out the docs there for more information on what kind of key combinations can be used. This library is included in the files from the `build` directory, so there is no need to install and include Mousetrap separately. 76 | 77 | **Update:** [A YouTube video tutorial was created for this project](https://www.youtube.com/watch?v=silr0L7rJOY). Thanks guys! 78 | 79 | 80 | #### Binding hotkeys in controllers: 81 | It is important to note that by default, hotkeys bound using the `hotkeys.add()` 82 | method are persistent, meaning they will continue to exist through route 83 | changes, DOM manipulation, or anything else. 84 | 85 | However, it is possible to bind the hotkey to a particular scope, and when that 86 | scope is destroyed, the hotkey is automatically removed. This should be 87 | considered the best practice when binding hotkeys from a controller. For this 88 | usage example, see the `hotkeys.bindTo()` method below: 89 | 90 | ```js 91 | angular.module('myApp').controller('NavbarCtrl', function($scope, hotkeys) { 92 | $scope.volume = 5; 93 | 94 | // You can pass it an object. This hotkey will not be unbound unless manually removed 95 | // using the hotkeys.del() method 96 | hotkeys.add({ 97 | combo: 'ctrl+up', 98 | description: 'This one goes to 11', 99 | callback: function() { 100 | $scope.volume += 1; 101 | } 102 | }); 103 | 104 | // when you bind it to the controller's scope, it will automatically unbind 105 | // the hotkey when the scope is destroyed (due to ng-if or something that changes the DOM) 106 | hotkeys.bindTo($scope) 107 | .add({ 108 | combo: 'w', 109 | description: 'blah blah', 110 | callback: function() {} 111 | }) 112 | // you can chain these methods for ease of use: 113 | .add ({...}); 114 | 115 | }); 116 | ``` 117 | 118 | #### Binding hotkeys in routes: 119 | You can also define hotkeys on an entire route, and this lib will bind and unbind them as you navigate the app. 120 | 121 | ```js 122 | angular.module('myApp').config(function ($routeProvider) { 123 | $routeProvider.when('/', { 124 | controller: 'RestaurantsController', 125 | templateUrl: 'views/restaurants.html', 126 | hotkeys: [ 127 | ['p', 'Sort by price', 'sort(price)'] 128 | ] 129 | }); 130 | }); 131 | ``` 132 | 133 | #### Binding hotkeys in directives: 134 | Lastly, even though binding hotkeys in your templates/html tends to be a bad idea, it can be super useful for simple shortcuts. Think along the lines of a modal directive where you simply want to bind to the escape key or something equally simple. Accomplishing this within a controller is too much overhead, and it may lead to code-reuse. 135 | 136 | Example of how directive-based hotkeys works: 137 | 138 | ```html 139 | 140 | ``` 141 | 142 | #### Cheatsheet 143 | 144 | A cheatsheet is created automatically for you, showing which hotkeys are available for the current route, along with a description as to what it does. The default binding to show the cheatsheet is `?`. Be sure to include the `build/hotkeys.css` stylesheet. [Cheatsheet demo](http://chieffancypants.github.io/angular-hotkeys/#features) 145 | 146 | **Disable the cheatsheet:** 147 | 148 | Disabling the cheatsheet can be accomplished by configuring the `hotkeysProvider`: 149 | 150 | ```js 151 | angular.module('myApp', ['cfp.hotkeys']) 152 | .config(function(hotkeysProvider) { 153 | hotkeysProvider.includeCheatSheet = false; 154 | }) 155 | ``` 156 | 157 | ### Configuration 158 | 159 | **Disable ngRoute integration:** 160 | 161 | To prevent listening for $routeChangeSuccess events use `hotkeysProvider`. 162 | This option defaults to false if ngRoute module is not loaded: 163 | 164 | ```js 165 | angular.module('myApp', ['cfp.hotkeys']) 166 | .config(function(hotkeysProvider) { 167 | hotkeysProvider.useNgRoute = false; 168 | }) 169 | ``` 170 | 171 | **Cheatsheet template:** 172 | 173 | ```js 174 | angular.module('myApp', ['cfp.hotkeys']) 175 | .config(function(hotkeysProvider) { 176 | hotkeysProvider.template = '
...
'; 177 | }) 178 | ``` 179 | 180 | **Header and footer:** 181 | 182 | You can specify a custom header and footer for the cheatsheet. Both are HTML, and if the header is set it will override the normal title. 183 | 184 | ```js 185 | angular.module('myApp', ['cfp.hotkeys']) 186 | .config(function(hotkeysProvider) { 187 | hotkeysProvider.templateHeader = '
...
'; 188 | hotkeysProvider.templateFooter = ''; 189 | }) 190 | ``` 191 | 192 | ### API 193 | 194 | #### hotkeys.add(object) 195 | `object`: An object with the following parameters: 196 | - `combo`: They keyboard combo (shortcut) you want to bind to 197 | - `description`: [OPTIONAL] The description for what the combo does and is only used for the Cheat Sheet. If it is not supplied, it will not show up, and in effect, allows you to have unlisted hotkeys. 198 | - `callback`: The function to execute when the key(s) are pressed. Passes along two arguments, `event` and `hotkey` 199 | - `action`: [OPTIONAL] The type of event to listen for, such as `keypress`, `keydown` or `keyup`. Usage of this parameter is discouraged as the underlying library will pick the most suitable option automatically. This should only be necessary in advanced situations. 200 | - `allowIn`: [OPTIONAL] an array of tag names to allow this combo in ('INPUT', 'SELECT', and/or 'TEXTAREA') 201 | 202 | ```js 203 | hotkeys.add({ 204 | combo: 'ctrl+w', 205 | description: 'Description goes here', 206 | callback: function(event, hotkey) { 207 | event.preventDefault(); 208 | } 209 | }); 210 | 211 | // this hotkey will not show up on the cheat sheet: 212 | hotkeys.add({ 213 | combo: 'ctrl+x', 214 | callback: function(event, hotkey) {...} 215 | }); 216 | ``` 217 | 218 | #### hotkeys.get(key) 219 | Returns the Hotkey object 220 | 221 | ```js 222 | hotkeys.get('ctrl+w'); 223 | // -> Hotkey { combo: ['ctrl+w'], description: 'Description goes here', callback: function (event, hotkey) } 224 | ``` 225 | 226 | #### hotkeys.del(key) 227 | Removes and unbinds a hotkey 228 | 229 | ```js 230 | hotkeys.del('ctrl+w'); 231 | ``` 232 | 233 | ### Allowing hotkeys in form elements 234 | By default, Mousetrap prevents hotkey callbacks from firing when their event originates from an `input`, `select`, or `textarea` element. To enable hotkeys in these elements, specify them in the `allowIn` parameter: 235 | ```js 236 | hotkeys.add({ 237 | combo: 'ctrl+w', 238 | description: 'Description goes here', 239 | allowIn: ['INPUT', 'SELECT', 'TEXTAREA'], 240 | callback: function(event, hotkey) { 241 | event.preventDefault(); 242 | } 243 | }); 244 | ``` 245 | 246 | ## Credits: 247 | 248 | Muchas gracias to Craig Campbell for his [Mousetrap](https://github.com/ccampbell/mousetrap) library, which provides the underlying library for handling keyboard shortcuts. 249 | -------------------------------------------------------------------------------- /test/hotkeys.coffee: -------------------------------------------------------------------------------- 1 | 2 | describe 'Angular Hotkeys', -> 3 | 4 | hotkeys = scope = $rootScope = $rootElement = $window = null 5 | 6 | beforeEach -> 7 | module 'cfp.hotkeys', (hotkeysProvider) -> 8 | hotkeysProvider.useNgRoute = true 9 | return 10 | 11 | result = null 12 | inject (_$rootElement_, _$rootScope_, _hotkeys_) -> 13 | hotkeys = _hotkeys_ 14 | $rootElement = _$rootElement_ 15 | $rootScope = _$rootScope_ 16 | scope = $rootScope.$new() 17 | 18 | afterEach -> 19 | hotkeys.del('w') 20 | t = document.getElementById('cfp-test') 21 | if t 22 | t.parentNode.removeChild(t) 23 | 24 | it 'should insert the help menu into the dom', -> 25 | children = angular.element($rootElement).children() 26 | expect(children.hasClass('cfp-hotkeys-container')).toBe true 27 | 28 | it 'add(args)', -> 29 | hotkeys.add 'w', 'description here', -> 30 | expect(hotkeys.get('w').description).toBe 'description here' 31 | expect(hotkeys.get('x')).toBe false 32 | 33 | it 'add(object)', -> 34 | callback = false 35 | hotkeys.add 36 | combo: 'w' 37 | description: 'description' 38 | callback: () -> 39 | callback = true 40 | 41 | expect(hotkeys.get('w').description).toBe 'description' 42 | 43 | # Test callback: 44 | expect(callback).toBe false 45 | KeyEvent.simulate('w'.charCodeAt(0), 90) 46 | expect(callback).toBe true 47 | 48 | it 'description should be optional', -> 49 | # func argument style: 50 | hotkeys.add 'w', -> 51 | expect(hotkeys.get('w').description).toBe '$$undefined$$' 52 | 53 | # object style: 54 | hotkeys.add 55 | combo: 'e' 56 | callback: -> 57 | 58 | expect(hotkeys.get('e').description).toBe '$$undefined$$' 59 | 60 | it 'del()', -> 61 | hotkeys.add 'w', -> 62 | expect(hotkeys.get('w').description).toBe '$$undefined$$' 63 | hotkeys.del 'w' 64 | expect(hotkeys.get('w')).toBe false 65 | 66 | it 'should toggle help when ? is pressed', -> 67 | expect(angular.element($rootElement).children().hasClass('in')).toBe false 68 | KeyEvent.simulate('?'.charCodeAt(0), 90) 69 | expect(angular.element($rootElement).children().hasClass('in')).toBe true 70 | 71 | it 'should bind esc when the cheatsheet is shown', -> 72 | expect(hotkeys.get('esc')).toBe false 73 | expect(angular.element($rootElement).children().hasClass('in')).toBe false 74 | KeyEvent.simulate('?'.charCodeAt(0), 90) 75 | expect(angular.element($rootElement).children().hasClass('in')).toBe true 76 | expect(hotkeys.get('esc').combo).toEqual ['esc'] 77 | KeyEvent.simulate('?'.charCodeAt(0), 90) 78 | expect(hotkeys.get('esc')).toBe false 79 | 80 | it 'should remember previously bound ESC when cheatsheet is shown', -> 81 | expect(hotkeys.get('esc')).toBe false 82 | 83 | # bind something to escape: 84 | hotkeys.add 'esc', 'temp', () -> 85 | expect(hotkeys.get('esc').description).toBe 'temp' 86 | originalCallback = hotkeys.get('esc').callback 87 | 88 | # show the cheat-sheet which will overwrite the esc key. however, we want to 89 | # show the original combo description in the callback, yet have the new 90 | # callback bound to remove the cheatsheet from view. 91 | KeyEvent.simulate('?'.charCodeAt(0), 90) 92 | expect(hotkeys.get('esc').description).toBe 'temp' 93 | expect(hotkeys.get('esc').callback).not.toBe originalCallback 94 | 95 | # hide the cheat sheet to verify the previous esc binding is back 96 | KeyEvent.simulate('?'.charCodeAt(0), 90) 97 | expect(hotkeys.get('esc').description).toBe 'temp' 98 | 99 | it 'should (un)bind based on route changes', -> 100 | # fake a route change: 101 | expect(hotkeys.get('w e s')).toBe false 102 | $rootScope.$broadcast('$routeChangeSuccess', { hotkeys: [['w e s', 'Do something Amazing!', 'callme("ishmael")']] }); 103 | expect(hotkeys.get('w e s').combo).toEqual ['w e s'] 104 | 105 | # ensure hotkey is unbound when the route changes 106 | $rootScope.$broadcast('$routeChangeSuccess', {}); 107 | expect(hotkeys.get('w e s')).toBe false 108 | 109 | it 'should callback when the hotkey is pressed', -> 110 | executed = false 111 | 112 | hotkeys.add 'w', -> 113 | executed = true 114 | 115 | KeyEvent.simulate('w'.charCodeAt(0), 90) 116 | expect(executed).toBe true 117 | 118 | it 'should callback according to action', -> 119 | keypressA = false; 120 | keypressB = false; 121 | 122 | hotkeys.add 'a', -> 123 | keypressA = true 124 | , 'keyup' 125 | 126 | hotkeys.add 'b', -> 127 | keypressB = true 128 | 129 | KeyEvent.simulate('a'.charCodeAt(0), 90) 130 | KeyEvent.simulate('b'.charCodeAt(0), 90) 131 | expect(keypressA).toBe false 132 | expect(keypressB).toBe true 133 | expect(hotkeys.get('a').action).toBe 'keyup' 134 | 135 | it 'should allow to invoke hotkey.callback programmatically without event object', -> 136 | called = false; 137 | 138 | hotkeys.add 'a', -> 139 | called = true 140 | , 'keyup' 141 | 142 | hotkeys.get('a').callback() 143 | expect(called).toBe true 144 | 145 | 146 | it 'should run routes-defined hotkey callbacks when scope is available', -> 147 | executed = false 148 | passedArg = null 149 | 150 | $rootScope.callme = (arg) -> 151 | executed = true 152 | passedArg = arg 153 | 154 | $rootScope.$broadcast '$routeChangeSuccess', 155 | hotkeys: [['w', 'Do something Amazing!', 'callme("ishmael")']] 156 | scope: $rootScope 157 | 158 | expect(executed).toBe false 159 | KeyEvent.simulate('w'.charCodeAt(0), 90) 160 | expect(executed).toBe true 161 | expect(passedArg).toBe 'ishmael' 162 | 163 | it 'should callback when hotkey is pressed in input field and allowIn INPUT is configured', -> 164 | executed = no 165 | 166 | $body = angular.element document.body 167 | $input = angular.element '' 168 | $body.prepend $input 169 | 170 | hotkeys.add 171 | combo: 'w' 172 | allowIn: ['INPUT'] 173 | callback: -> executed = yes 174 | 175 | KeyEvent.simulate('w'.charCodeAt(0), 90, undefined, $input[0]) 176 | expect(executed).toBe yes 177 | 178 | it 'should callback when hotkey is pressed in select field and allowIn SELECT is configured', -> 179 | executed = no 180 | 181 | $body = angular.element document.body 182 | $select = angular.element '