├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── demo └── index.html ├── gulpfile.js ├── package.json ├── release ├── angular-mousetrap-service.js └── angular-mousetrap-service.min.js └── src ├── angular-mousetrap-service.js └── before-plugins.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Campos 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 | angular-mousetrap-service 2 | ========================= 3 | [![npm version](https://badge.fury.io/js/angular-mousetrap-service.svg)](http://badge.fury.io/js/angular-mousetrap-service) 4 | [![Bower version](https://badge.fury.io/bo/angular-mousetrap-service.svg)](http://badge.fury.io/bo/angular-mousetrap-service) 5 | 6 | [AngularJS](https://angularjs.org) Wrapper for [mousetrap.js](http://craig.is/killing/mice) whithout registering it in the global scope. 7 | 8 | 9 | How to use 10 | ---------- 11 | 12 | 1. Install angular-mousetrap-service. 13 | Using bower: `bower install --save angular-mousetrap-service` 14 | Or, using npm: `npm install --save angular-mousetrap-service` 15 | 16 | 2. Include angular-mousetrap-service into your project. 17 | ```HTML 18 | 19 | ``` 20 | 21 | 3. Add ```angular-mousetrap``` module in the dependencies of a angular module: 22 | ```JavaScript 23 | angular.module('exampleApp', ['angular-mousetrap']) 24 | ``` 25 | 26 | 4. Inject the Mousetrap service into a controller, a directive, etc: 27 | ```JavaScript 28 | .controller('ExampleCtrl', ['$scope','Mousetrap', function($scope, Mousetrap){ 29 | Mousetrap.bind('ctrl+s', function() { 30 | //... 31 | }); 32 | }]); 33 | ``` 34 | 35 | Obs.: See the [Demo page](http://the-darc.github.io/angular-mousetrap/) for pratical real example. 36 | 37 | How to contribute 38 | ----------------- 39 | 40 | I am very glad to see this project living with pull requests. 41 | 42 | LICENSE 43 | ------- 44 | 45 | Copyright (c) 2015 Daniel Campos 46 | 47 | Licensed under the MIT license. 48 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-mousetrap-service", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/the-darc/angular-mousetrap", 5 | "authors": [ 6 | "darc " 7 | ], 8 | "description": "AngularJS Service for dependency injection of mousetrap.js", 9 | "main": "release/service.min.js", 10 | "keywords": [ 11 | "mousetrap", 12 | "angular", 13 | "keyboard", 14 | "shortcuts", 15 | "events" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components" 22 | ], 23 | "devDependencies": { 24 | "mousetrap": "~1.4.6" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularJS Service for dependency injection of mousetrap.js 6 | 7 | 8 | 30 | 31 | 32 |
33 |

angular-mousetrap-service

34 |
35 | 36 | 37 |
38 |
39 |

Type the key(s) '{{configs.keys}}': {{configs.typed}}

40 |
41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | path = require('path'), 3 | plugins = require('gulp-load-plugins')({ 4 | config: path.join(__dirname, 'package.json') 5 | }); 6 | 7 | gulp.task('build', function() { 8 | var pkg = require('./bower.json'); 9 | 10 | var header = ['/**', 11 | ' * <%= pkg.name %>', 12 | ' * <%= pkg.description %>', 13 | ' * @version v<%= pkg.version %>', 14 | ' * @link <%= pkg.homepage %>', 15 | ' * @license <%= pkg.license %>', 16 | ' */', 17 | '(function (angular) {', 18 | ' var window = {};', 19 | '', 20 | ''].join('\n'); 21 | 22 | var footer = [ 23 | '', 24 | '})(angular);', 25 | ''].join('\n'); 26 | 27 | gulp.src([ 28 | 'bower_components/mousetrap/mousetrap.js', 29 | 'src/before-plugins.js', 30 | 'bower_components/mousetrap/plugins/pause/mousetrap-pause.js', 31 | 'bower_components/mousetrap/plugins/bind-dictionary/mousetrap-bind-dictionary.js', 32 | 'src/angular-mousetrap-service.js' 33 | ]) 34 | .pipe(plugins.concat('angular-mousetrap-service.js')) 35 | .pipe(plugins.header(header, {pkg: pkg})) 36 | .pipe(plugins.footer(footer)) 37 | .pipe(gulp.dest('./release/')) 38 | .pipe(plugins.uglify()) 39 | .pipe(plugins.concat('angular-mousetrap-service.min.js')) 40 | .pipe(gulp.dest('./release/')); 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-mousetrap-service", 3 | "version": "0.1.0", 4 | "description": "AngularJS Service for dependency injection of mousetrap.js", 5 | "main": "release/service.min.js", 6 | "scripts": { 7 | "build": "gulp build" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/the-darc/angular-mousetrap.git" 12 | }, 13 | "keywords": [ 14 | "mousetrap", 15 | "angular", 16 | "keyboard", 17 | "shortcuts", 18 | "events" 19 | ], 20 | "author": "darc.tec@gmail.com", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/the-darc/angular-mousetrap/issues" 24 | }, 25 | "homepage": "https://github.com/the-darc/angular-mousetrap", 26 | "dependencies": { 27 | "gulp": "^3.8.10", 28 | "gulp-concat": "^2.4.3", 29 | "gulp-footer": "^1.0.5", 30 | "gulp-header": "^1.2.2", 31 | "gulp-load-plugins": "^0.8.0", 32 | "gulp-uglify": "^1.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /release/angular-mousetrap-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * angular-mousetrap-service 3 | * AngularJS Service for dependency injection of mousetrap.js 4 | * @version v0.1.0 5 | * @link https://github.com/the-darc/angular-mousetrap 6 | * @license MIT 7 | */ 8 | (function (angular) { 9 | var window = {}; 10 | 11 | /*global define:false */ 12 | /** 13 | * Copyright 2013 Craig Campbell 14 | * 15 | * Licensed under the Apache License, Version 2.0 (the "License"); 16 | * you may not use this file except in compliance with the License. 17 | * You may obtain a copy of the License at 18 | * 19 | * http://www.apache.org/licenses/LICENSE-2.0 20 | * 21 | * Unless required by applicable law or agreed to in writing, software 22 | * distributed under the License is distributed on an "AS IS" BASIS, 23 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | * See the License for the specific language governing permissions and 25 | * limitations under the License. 26 | * 27 | * Mousetrap is a simple keyboard shortcut library for Javascript with 28 | * no external dependencies 29 | * 30 | * @version 1.4.6 31 | * @url craig.is/killing/mice 32 | */ 33 | (function(window, document, undefined) { 34 | 35 | /** 36 | * mapping of special keycodes to their corresponding keys 37 | * 38 | * everything in this dictionary cannot use keypress events 39 | * so it has to be here to map to the correct keycodes for 40 | * keyup/keydown events 41 | * 42 | * @type {Object} 43 | */ 44 | var _MAP = { 45 | 8: 'backspace', 46 | 9: 'tab', 47 | 13: 'enter', 48 | 16: 'shift', 49 | 17: 'ctrl', 50 | 18: 'alt', 51 | 20: 'capslock', 52 | 27: 'esc', 53 | 32: 'space', 54 | 33: 'pageup', 55 | 34: 'pagedown', 56 | 35: 'end', 57 | 36: 'home', 58 | 37: 'left', 59 | 38: 'up', 60 | 39: 'right', 61 | 40: 'down', 62 | 45: 'ins', 63 | 46: 'del', 64 | 91: 'meta', 65 | 93: 'meta', 66 | 224: 'meta' 67 | }, 68 | 69 | /** 70 | * mapping for special characters so they can support 71 | * 72 | * this dictionary is only used incase you want to bind a 73 | * keyup or keydown event to one of these keys 74 | * 75 | * @type {Object} 76 | */ 77 | _KEYCODE_MAP = { 78 | 106: '*', 79 | 107: '+', 80 | 109: '-', 81 | 110: '.', 82 | 111 : '/', 83 | 186: ';', 84 | 187: '=', 85 | 188: ',', 86 | 189: '-', 87 | 190: '.', 88 | 191: '/', 89 | 192: '`', 90 | 219: '[', 91 | 220: '\\', 92 | 221: ']', 93 | 222: '\'' 94 | }, 95 | 96 | /** 97 | * this is a mapping of keys that require shift on a US keypad 98 | * back to the non shift equivelents 99 | * 100 | * this is so you can use keyup events with these keys 101 | * 102 | * note that this will only work reliably on US keyboards 103 | * 104 | * @type {Object} 105 | */ 106 | _SHIFT_MAP = { 107 | '~': '`', 108 | '!': '1', 109 | '@': '2', 110 | '#': '3', 111 | '$': '4', 112 | '%': '5', 113 | '^': '6', 114 | '&': '7', 115 | '*': '8', 116 | '(': '9', 117 | ')': '0', 118 | '_': '-', 119 | '+': '=', 120 | ':': ';', 121 | '\"': '\'', 122 | '<': ',', 123 | '>': '.', 124 | '?': '/', 125 | '|': '\\' 126 | }, 127 | 128 | /** 129 | * this is a list of special strings you can use to map 130 | * to modifier keys when you specify your keyboard shortcuts 131 | * 132 | * @type {Object} 133 | */ 134 | _SPECIAL_ALIASES = { 135 | 'option': 'alt', 136 | 'command': 'meta', 137 | 'return': 'enter', 138 | 'escape': 'esc', 139 | 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl' 140 | }, 141 | 142 | /** 143 | * variable to store the flipped version of _MAP from above 144 | * needed to check if we should use keypress or not when no action 145 | * is specified 146 | * 147 | * @type {Object|undefined} 148 | */ 149 | _REVERSE_MAP, 150 | 151 | /** 152 | * a list of all the callbacks setup via Mousetrap.bind() 153 | * 154 | * @type {Object} 155 | */ 156 | _callbacks = {}, 157 | 158 | /** 159 | * direct map of string combinations to callbacks used for trigger() 160 | * 161 | * @type {Object} 162 | */ 163 | _directMap = {}, 164 | 165 | /** 166 | * keeps track of what level each sequence is at since multiple 167 | * sequences can start out with the same sequence 168 | * 169 | * @type {Object} 170 | */ 171 | _sequenceLevels = {}, 172 | 173 | /** 174 | * variable to store the setTimeout call 175 | * 176 | * @type {null|number} 177 | */ 178 | _resetTimer, 179 | 180 | /** 181 | * temporary state where we will ignore the next keyup 182 | * 183 | * @type {boolean|string} 184 | */ 185 | _ignoreNextKeyup = false, 186 | 187 | /** 188 | * temporary state where we will ignore the next keypress 189 | * 190 | * @type {boolean} 191 | */ 192 | _ignoreNextKeypress = false, 193 | 194 | /** 195 | * are we currently inside of a sequence? 196 | * type of action ("keyup" or "keydown" or "keypress") or false 197 | * 198 | * @type {boolean|string} 199 | */ 200 | _nextExpectedAction = false; 201 | 202 | /** 203 | * loop through the f keys, f1 to f19 and add them to the map 204 | * programatically 205 | */ 206 | for (var i = 1; i < 20; ++i) { 207 | _MAP[111 + i] = 'f' + i; 208 | } 209 | 210 | /** 211 | * loop through to map numbers on the numeric keypad 212 | */ 213 | for (i = 0; i <= 9; ++i) { 214 | _MAP[i + 96] = i; 215 | } 216 | 217 | /** 218 | * cross browser add event method 219 | * 220 | * @param {Element|HTMLDocument} object 221 | * @param {string} type 222 | * @param {Function} callback 223 | * @returns void 224 | */ 225 | function _addEvent(object, type, callback) { 226 | if (object.addEventListener) { 227 | object.addEventListener(type, callback, false); 228 | return; 229 | } 230 | 231 | object.attachEvent('on' + type, callback); 232 | } 233 | 234 | /** 235 | * takes the event and returns the key character 236 | * 237 | * @param {Event} e 238 | * @return {string} 239 | */ 240 | function _characterFromEvent(e) { 241 | 242 | // for keypress events we should return the character as is 243 | if (e.type == 'keypress') { 244 | var character = String.fromCharCode(e.which); 245 | 246 | // if the shift key is not pressed then it is safe to assume 247 | // that we want the character to be lowercase. this means if 248 | // you accidentally have caps lock on then your key bindings 249 | // will continue to work 250 | // 251 | // the only side effect that might not be desired is if you 252 | // bind something like 'A' cause you want to trigger an 253 | // event when capital A is pressed caps lock will no longer 254 | // trigger the event. shift+a will though. 255 | if (!e.shiftKey) { 256 | character = character.toLowerCase(); 257 | } 258 | 259 | return character; 260 | } 261 | 262 | // for non keypress events the special maps are needed 263 | if (_MAP[e.which]) { 264 | return _MAP[e.which]; 265 | } 266 | 267 | if (_KEYCODE_MAP[e.which]) { 268 | return _KEYCODE_MAP[e.which]; 269 | } 270 | 271 | // if it is not in the special map 272 | 273 | // with keydown and keyup events the character seems to always 274 | // come in as an uppercase character whether you are pressing shift 275 | // or not. we should make sure it is always lowercase for comparisons 276 | return String.fromCharCode(e.which).toLowerCase(); 277 | } 278 | 279 | /** 280 | * checks if two arrays are equal 281 | * 282 | * @param {Array} modifiers1 283 | * @param {Array} modifiers2 284 | * @returns {boolean} 285 | */ 286 | function _modifiersMatch(modifiers1, modifiers2) { 287 | return modifiers1.sort().join(',') === modifiers2.sort().join(','); 288 | } 289 | 290 | /** 291 | * resets all sequence counters except for the ones passed in 292 | * 293 | * @param {Object} doNotReset 294 | * @returns void 295 | */ 296 | function _resetSequences(doNotReset) { 297 | doNotReset = doNotReset || {}; 298 | 299 | var activeSequences = false, 300 | key; 301 | 302 | for (key in _sequenceLevels) { 303 | if (doNotReset[key]) { 304 | activeSequences = true; 305 | continue; 306 | } 307 | _sequenceLevels[key] = 0; 308 | } 309 | 310 | if (!activeSequences) { 311 | _nextExpectedAction = false; 312 | } 313 | } 314 | 315 | /** 316 | * finds all callbacks that match based on the keycode, modifiers, 317 | * and action 318 | * 319 | * @param {string} character 320 | * @param {Array} modifiers 321 | * @param {Event|Object} e 322 | * @param {string=} sequenceName - name of the sequence we are looking for 323 | * @param {string=} combination 324 | * @param {number=} level 325 | * @returns {Array} 326 | */ 327 | function _getMatches(character, modifiers, e, sequenceName, combination, level) { 328 | var i, 329 | callback, 330 | matches = [], 331 | action = e.type; 332 | 333 | // if there are no events related to this keycode 334 | if (!_callbacks[character]) { 335 | return []; 336 | } 337 | 338 | // if a modifier key is coming up on its own we should allow it 339 | if (action == 'keyup' && _isModifier(character)) { 340 | modifiers = [character]; 341 | } 342 | 343 | // loop through all callbacks for the key that was pressed 344 | // and see if any of them match 345 | for (i = 0; i < _callbacks[character].length; ++i) { 346 | callback = _callbacks[character][i]; 347 | 348 | // if a sequence name is not specified, but this is a sequence at 349 | // the wrong level then move onto the next match 350 | if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) { 351 | continue; 352 | } 353 | 354 | // if the action we are looking for doesn't match the action we got 355 | // then we should keep going 356 | if (action != callback.action) { 357 | continue; 358 | } 359 | 360 | // if this is a keypress event and the meta key and control key 361 | // are not pressed that means that we need to only look at the 362 | // character, otherwise check the modifiers as well 363 | // 364 | // chrome will not fire a keypress if meta or control is down 365 | // safari will fire a keypress if meta or meta+shift is down 366 | // firefox will fire a keypress if meta or control is down 367 | if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { 368 | 369 | // when you bind a combination or sequence a second time it 370 | // should overwrite the first one. if a sequenceName or 371 | // combination is specified in this call it does just that 372 | // 373 | // @todo make deleting its own method? 374 | var deleteCombo = !sequenceName && callback.combo == combination; 375 | var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level; 376 | if (deleteCombo || deleteSequence) { 377 | _callbacks[character].splice(i, 1); 378 | } 379 | 380 | matches.push(callback); 381 | } 382 | } 383 | 384 | return matches; 385 | } 386 | 387 | /** 388 | * takes a key event and figures out what the modifiers are 389 | * 390 | * @param {Event} e 391 | * @returns {Array} 392 | */ 393 | function _eventModifiers(e) { 394 | var modifiers = []; 395 | 396 | if (e.shiftKey) { 397 | modifiers.push('shift'); 398 | } 399 | 400 | if (e.altKey) { 401 | modifiers.push('alt'); 402 | } 403 | 404 | if (e.ctrlKey) { 405 | modifiers.push('ctrl'); 406 | } 407 | 408 | if (e.metaKey) { 409 | modifiers.push('meta'); 410 | } 411 | 412 | return modifiers; 413 | } 414 | 415 | /** 416 | * prevents default for this event 417 | * 418 | * @param {Event} e 419 | * @returns void 420 | */ 421 | function _preventDefault(e) { 422 | if (e.preventDefault) { 423 | e.preventDefault(); 424 | return; 425 | } 426 | 427 | e.returnValue = false; 428 | } 429 | 430 | /** 431 | * stops propogation for this event 432 | * 433 | * @param {Event} e 434 | * @returns void 435 | */ 436 | function _stopPropagation(e) { 437 | if (e.stopPropagation) { 438 | e.stopPropagation(); 439 | return; 440 | } 441 | 442 | e.cancelBubble = true; 443 | } 444 | 445 | /** 446 | * actually calls the callback function 447 | * 448 | * if your callback function returns false this will use the jquery 449 | * convention - prevent default and stop propogation on the event 450 | * 451 | * @param {Function} callback 452 | * @param {Event} e 453 | * @returns void 454 | */ 455 | function _fireCallback(callback, e, combo, sequence) { 456 | 457 | // if this event should not happen stop here 458 | if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo, sequence)) { 459 | return; 460 | } 461 | 462 | if (callback(e, combo) === false) { 463 | _preventDefault(e); 464 | _stopPropagation(e); 465 | } 466 | } 467 | 468 | /** 469 | * handles a character key event 470 | * 471 | * @param {string} character 472 | * @param {Array} modifiers 473 | * @param {Event} e 474 | * @returns void 475 | */ 476 | function _handleKey(character, modifiers, e) { 477 | var callbacks = _getMatches(character, modifiers, e), 478 | i, 479 | doNotReset = {}, 480 | maxLevel = 0, 481 | processedSequenceCallback = false; 482 | 483 | // Calculate the maxLevel for sequences so we can only execute the longest callback sequence 484 | for (i = 0; i < callbacks.length; ++i) { 485 | if (callbacks[i].seq) { 486 | maxLevel = Math.max(maxLevel, callbacks[i].level); 487 | } 488 | } 489 | 490 | // loop through matching callbacks for this key event 491 | for (i = 0; i < callbacks.length; ++i) { 492 | 493 | // fire for all sequence callbacks 494 | // this is because if for example you have multiple sequences 495 | // bound such as "g i" and "g t" they both need to fire the 496 | // callback for matching g cause otherwise you can only ever 497 | // match the first one 498 | if (callbacks[i].seq) { 499 | 500 | // only fire callbacks for the maxLevel to prevent 501 | // subsequences from also firing 502 | // 503 | // for example 'a option b' should not cause 'option b' to fire 504 | // even though 'option b' is part of the other sequence 505 | // 506 | // any sequences that do not match here will be discarded 507 | // below by the _resetSequences call 508 | if (callbacks[i].level != maxLevel) { 509 | continue; 510 | } 511 | 512 | processedSequenceCallback = true; 513 | 514 | // keep a list of which sequences were matches for later 515 | doNotReset[callbacks[i].seq] = 1; 516 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq); 517 | continue; 518 | } 519 | 520 | // if there were no sequence matches but we are still here 521 | // that means this is a regular match so we should fire that 522 | if (!processedSequenceCallback) { 523 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); 524 | } 525 | } 526 | 527 | // if the key you pressed matches the type of sequence without 528 | // being a modifier (ie "keyup" or "keypress") then we should 529 | // reset all sequences that were not matched by this event 530 | // 531 | // this is so, for example, if you have the sequence "h a t" and you 532 | // type "h e a r t" it does not match. in this case the "e" will 533 | // cause the sequence to reset 534 | // 535 | // modifier keys are ignored because you can have a sequence 536 | // that contains modifiers such as "enter ctrl+space" and in most 537 | // cases the modifier key will be pressed before the next key 538 | // 539 | // also if you have a sequence such as "ctrl+b a" then pressing the 540 | // "b" key will trigger a "keypress" and a "keydown" 541 | // 542 | // the "keydown" is expected when there is a modifier, but the 543 | // "keypress" ends up matching the _nextExpectedAction since it occurs 544 | // after and that causes the sequence to reset 545 | // 546 | // we ignore keypresses in a sequence that directly follow a keydown 547 | // for the same character 548 | var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress; 549 | if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) { 550 | _resetSequences(doNotReset); 551 | } 552 | 553 | _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown'; 554 | } 555 | 556 | /** 557 | * handles a keydown event 558 | * 559 | * @param {Event} e 560 | * @returns void 561 | */ 562 | function _handleKeyEvent(e) { 563 | 564 | // normalize e.which for key events 565 | // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion 566 | if (typeof e.which !== 'number') { 567 | e.which = e.keyCode; 568 | } 569 | 570 | var character = _characterFromEvent(e); 571 | 572 | // no character found then stop 573 | if (!character) { 574 | return; 575 | } 576 | 577 | // need to use === for the character check because the character can be 0 578 | if (e.type == 'keyup' && _ignoreNextKeyup === character) { 579 | _ignoreNextKeyup = false; 580 | return; 581 | } 582 | 583 | Mousetrap.handleKey(character, _eventModifiers(e), e); 584 | } 585 | 586 | /** 587 | * determines if the keycode specified is a modifier key or not 588 | * 589 | * @param {string} key 590 | * @returns {boolean} 591 | */ 592 | function _isModifier(key) { 593 | return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; 594 | } 595 | 596 | /** 597 | * called to set a 1 second timeout on the specified sequence 598 | * 599 | * this is so after each key press in the sequence you have 1 second 600 | * to press the next key before you have to start over 601 | * 602 | * @returns void 603 | */ 604 | function _resetSequenceTimer() { 605 | clearTimeout(_resetTimer); 606 | _resetTimer = setTimeout(_resetSequences, 1000); 607 | } 608 | 609 | /** 610 | * reverses the map lookup so that we can look for specific keys 611 | * to see what can and can't use keypress 612 | * 613 | * @return {Object} 614 | */ 615 | function _getReverseMap() { 616 | if (!_REVERSE_MAP) { 617 | _REVERSE_MAP = {}; 618 | for (var key in _MAP) { 619 | 620 | // pull out the numeric keypad from here cause keypress should 621 | // be able to detect the keys from the character 622 | if (key > 95 && key < 112) { 623 | continue; 624 | } 625 | 626 | if (_MAP.hasOwnProperty(key)) { 627 | _REVERSE_MAP[_MAP[key]] = key; 628 | } 629 | } 630 | } 631 | return _REVERSE_MAP; 632 | } 633 | 634 | /** 635 | * picks the best action based on the key combination 636 | * 637 | * @param {string} key - character for key 638 | * @param {Array} modifiers 639 | * @param {string=} action passed in 640 | */ 641 | function _pickBestAction(key, modifiers, action) { 642 | 643 | // if no action was picked in we should try to pick the one 644 | // that we think would work best for this key 645 | if (!action) { 646 | action = _getReverseMap()[key] ? 'keydown' : 'keypress'; 647 | } 648 | 649 | // modifier keys don't work as expected with keypress, 650 | // switch to keydown 651 | if (action == 'keypress' && modifiers.length) { 652 | action = 'keydown'; 653 | } 654 | 655 | return action; 656 | } 657 | 658 | /** 659 | * binds a key sequence to an event 660 | * 661 | * @param {string} combo - combo specified in bind call 662 | * @param {Array} keys 663 | * @param {Function} callback 664 | * @param {string=} action 665 | * @returns void 666 | */ 667 | function _bindSequence(combo, keys, callback, action) { 668 | 669 | // start off by adding a sequence level record for this combination 670 | // and setting the level to 0 671 | _sequenceLevels[combo] = 0; 672 | 673 | /** 674 | * callback to increase the sequence level for this sequence and reset 675 | * all other sequences that were active 676 | * 677 | * @param {string} nextAction 678 | * @returns {Function} 679 | */ 680 | function _increaseSequence(nextAction) { 681 | return function() { 682 | _nextExpectedAction = nextAction; 683 | ++_sequenceLevels[combo]; 684 | _resetSequenceTimer(); 685 | }; 686 | } 687 | 688 | /** 689 | * wraps the specified callback inside of another function in order 690 | * to reset all sequence counters as soon as this sequence is done 691 | * 692 | * @param {Event} e 693 | * @returns void 694 | */ 695 | function _callbackAndReset(e) { 696 | _fireCallback(callback, e, combo); 697 | 698 | // we should ignore the next key up if the action is key down 699 | // or keypress. this is so if you finish a sequence and 700 | // release the key the final key will not trigger a keyup 701 | if (action !== 'keyup') { 702 | _ignoreNextKeyup = _characterFromEvent(e); 703 | } 704 | 705 | // weird race condition if a sequence ends with the key 706 | // another sequence begins with 707 | setTimeout(_resetSequences, 10); 708 | } 709 | 710 | // loop through keys one at a time and bind the appropriate callback 711 | // function. for any key leading up to the final one it should 712 | // increase the sequence. after the final, it should reset all sequences 713 | // 714 | // if an action is specified in the original bind call then that will 715 | // be used throughout. otherwise we will pass the action that the 716 | // next key in the sequence should match. this allows a sequence 717 | // to mix and match keypress and keydown events depending on which 718 | // ones are better suited to the key provided 719 | for (var i = 0; i < keys.length; ++i) { 720 | var isFinal = i + 1 === keys.length; 721 | var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action); 722 | _bindSingle(keys[i], wrappedCallback, action, combo, i); 723 | } 724 | } 725 | 726 | /** 727 | * Converts from a string key combination to an array 728 | * 729 | * @param {string} combination like "command+shift+l" 730 | * @return {Array} 731 | */ 732 | function _keysFromString(combination) { 733 | if (combination === '+') { 734 | return ['+']; 735 | } 736 | 737 | return combination.split('+'); 738 | } 739 | 740 | /** 741 | * Gets info for a specific key combination 742 | * 743 | * @param {string} combination key combination ("command+s" or "a" or "*") 744 | * @param {string=} action 745 | * @returns {Object} 746 | */ 747 | function _getKeyInfo(combination, action) { 748 | var keys, 749 | key, 750 | i, 751 | modifiers = []; 752 | 753 | // take the keys from this pattern and figure out what the actual 754 | // pattern is all about 755 | keys = _keysFromString(combination); 756 | 757 | for (i = 0; i < keys.length; ++i) { 758 | key = keys[i]; 759 | 760 | // normalize key names 761 | if (_SPECIAL_ALIASES[key]) { 762 | key = _SPECIAL_ALIASES[key]; 763 | } 764 | 765 | // if this is not a keypress event then we should 766 | // be smart about using shift keys 767 | // this will only work for US keyboards however 768 | if (action && action != 'keypress' && _SHIFT_MAP[key]) { 769 | key = _SHIFT_MAP[key]; 770 | modifiers.push('shift'); 771 | } 772 | 773 | // if this key is a modifier then add it to the list of modifiers 774 | if (_isModifier(key)) { 775 | modifiers.push(key); 776 | } 777 | } 778 | 779 | // depending on what the key combination is 780 | // we will try to pick the best event for it 781 | action = _pickBestAction(key, modifiers, action); 782 | 783 | return { 784 | key: key, 785 | modifiers: modifiers, 786 | action: action 787 | }; 788 | } 789 | 790 | /** 791 | * binds a single keyboard combination 792 | * 793 | * @param {string} combination 794 | * @param {Function} callback 795 | * @param {string=} action 796 | * @param {string=} sequenceName - name of sequence if part of sequence 797 | * @param {number=} level - what part of the sequence the command is 798 | * @returns void 799 | */ 800 | function _bindSingle(combination, callback, action, sequenceName, level) { 801 | 802 | // store a direct mapped reference for use with Mousetrap.trigger 803 | _directMap[combination + ':' + action] = callback; 804 | 805 | // make sure multiple spaces in a row become a single space 806 | combination = combination.replace(/\s+/g, ' '); 807 | 808 | var sequence = combination.split(' '), 809 | info; 810 | 811 | // if this pattern is a sequence of keys then run through this method 812 | // to reprocess each pattern one key at a time 813 | if (sequence.length > 1) { 814 | _bindSequence(combination, sequence, callback, action); 815 | return; 816 | } 817 | 818 | info = _getKeyInfo(combination, action); 819 | 820 | // make sure to initialize array if this is the first time 821 | // a callback is added for this key 822 | _callbacks[info.key] = _callbacks[info.key] || []; 823 | 824 | // remove an existing match if there is one 825 | _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level); 826 | 827 | // add this call back to the array 828 | // if it is a sequence put it at the beginning 829 | // if not put it at the end 830 | // 831 | // this is important because the way these are processed expects 832 | // the sequence ones to come first 833 | _callbacks[info.key][sequenceName ? 'unshift' : 'push']({ 834 | callback: callback, 835 | modifiers: info.modifiers, 836 | action: info.action, 837 | seq: sequenceName, 838 | level: level, 839 | combo: combination 840 | }); 841 | } 842 | 843 | /** 844 | * binds multiple combinations to the same callback 845 | * 846 | * @param {Array} combinations 847 | * @param {Function} callback 848 | * @param {string|undefined} action 849 | * @returns void 850 | */ 851 | function _bindMultiple(combinations, callback, action) { 852 | for (var i = 0; i < combinations.length; ++i) { 853 | _bindSingle(combinations[i], callback, action); 854 | } 855 | } 856 | 857 | // start! 858 | _addEvent(document, 'keypress', _handleKeyEvent); 859 | _addEvent(document, 'keydown', _handleKeyEvent); 860 | _addEvent(document, 'keyup', _handleKeyEvent); 861 | 862 | var Mousetrap = { 863 | 864 | /** 865 | * binds an event to mousetrap 866 | * 867 | * can be a single key, a combination of keys separated with +, 868 | * an array of keys, or a sequence of keys separated by spaces 869 | * 870 | * be sure to list the modifier keys first to make sure that the 871 | * correct key ends up getting bound (the last key in the pattern) 872 | * 873 | * @param {string|Array} keys 874 | * @param {Function} callback 875 | * @param {string=} action - 'keypress', 'keydown', or 'keyup' 876 | * @returns void 877 | */ 878 | bind: function(keys, callback, action) { 879 | keys = keys instanceof Array ? keys : [keys]; 880 | _bindMultiple(keys, callback, action); 881 | return this; 882 | }, 883 | 884 | /** 885 | * unbinds an event to mousetrap 886 | * 887 | * the unbinding sets the callback function of the specified key combo 888 | * to an empty function and deletes the corresponding key in the 889 | * _directMap dict. 890 | * 891 | * TODO: actually remove this from the _callbacks dictionary instead 892 | * of binding an empty function 893 | * 894 | * the keycombo+action has to be exactly the same as 895 | * it was defined in the bind method 896 | * 897 | * @param {string|Array} keys 898 | * @param {string} action 899 | * @returns void 900 | */ 901 | unbind: function(keys, action) { 902 | return Mousetrap.bind(keys, function() {}, action); 903 | }, 904 | 905 | /** 906 | * triggers an event that has already been bound 907 | * 908 | * @param {string} keys 909 | * @param {string=} action 910 | * @returns void 911 | */ 912 | trigger: function(keys, action) { 913 | if (_directMap[keys + ':' + action]) { 914 | _directMap[keys + ':' + action]({}, keys); 915 | } 916 | return this; 917 | }, 918 | 919 | /** 920 | * resets the library back to its initial state. this is useful 921 | * if you want to clear out the current keyboard shortcuts and bind 922 | * new ones - for example if you switch to another page 923 | * 924 | * @returns void 925 | */ 926 | reset: function() { 927 | _callbacks = {}; 928 | _directMap = {}; 929 | return this; 930 | }, 931 | 932 | /** 933 | * should we stop this event before firing off callbacks 934 | * 935 | * @param {Event} e 936 | * @param {Element} element 937 | * @return {boolean} 938 | */ 939 | stopCallback: function(e, element) { 940 | 941 | // if the element has the class "mousetrap" then no need to stop 942 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 943 | return false; 944 | } 945 | 946 | // stop for input, select, and textarea 947 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable; 948 | }, 949 | 950 | /** 951 | * exposes _handleKey publicly so it can be overwritten by extensions 952 | */ 953 | handleKey: _handleKey 954 | }; 955 | 956 | // expose mousetrap to the global object 957 | window.Mousetrap = Mousetrap; 958 | 959 | // expose mousetrap as an AMD module 960 | if (typeof define === 'function' && define.amd) { 961 | define(Mousetrap); 962 | } 963 | }) (window, document); 964 | 965 | var Mousetrap = window.Mousetrap; 966 | 967 | /** 968 | * adds a pause and unpause method to Mousetrap 969 | * this allows you to enable or disable keyboard shortcuts 970 | * without having to reset Mousetrap and rebind everything 971 | */ 972 | /* global Mousetrap:true */ 973 | Mousetrap = (function(Mousetrap) { 974 | var self = Mousetrap, 975 | _originalStopCallback = self.stopCallback, 976 | enabled = true; 977 | 978 | self.stopCallback = function(e, element, combo) { 979 | if (!enabled) { 980 | return true; 981 | } 982 | 983 | return _originalStopCallback(e, element, combo); 984 | }; 985 | 986 | self.pause = function() { 987 | enabled = false; 988 | }; 989 | 990 | self.unpause = function() { 991 | enabled = true; 992 | }; 993 | 994 | return self; 995 | }) (Mousetrap); 996 | 997 | /** 998 | * Overwrites default Mousetrap.bind method to optionally accept 999 | * an object to bind multiple key events in a single call 1000 | * 1001 | * You can pass it in like: 1002 | * 1003 | * Mousetrap.bind({ 1004 | * 'a': function() { console.log('a'); }, 1005 | * 'b': function() { console.log('b'); } 1006 | * }); 1007 | * 1008 | * And can optionally pass in 'keypress', 'keydown', or 'keyup' 1009 | * as a second argument 1010 | * 1011 | */ 1012 | /* global Mousetrap:true */ 1013 | Mousetrap = (function(Mousetrap) { 1014 | var self = Mousetrap, 1015 | _oldBind = self.bind, 1016 | args; 1017 | 1018 | self.bind = function() { 1019 | args = arguments; 1020 | 1021 | // normal call 1022 | if (typeof args[0] == 'string' || args[0] instanceof Array) { 1023 | return _oldBind(args[0], args[1], args[2]); 1024 | } 1025 | 1026 | // object passed in 1027 | for (var key in args[0]) { 1028 | if (args[0].hasOwnProperty(key)) { 1029 | _oldBind(key, args[0][key], args[1]); 1030 | } 1031 | } 1032 | }; 1033 | 1034 | return self; 1035 | }) (Mousetrap); 1036 | 1037 | angular.module('angular-mousetrap', []) 1038 | .factory('Mousetrap', [function(){ 1039 | return window.Mousetrap; 1040 | }]); 1041 | 1042 | })(angular); 1043 | -------------------------------------------------------------------------------- /release/angular-mousetrap-service.min.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};!function(e,t){function n(e,t,n){return e.addEventListener?void e.addEventListener(t,n,!1):void e.attachEvent("on"+t,n)}function r(e){if("keypress"==e.type){var t=String.fromCharCode(e.which);return e.shiftKey||(t=t.toLowerCase()),t}return K[e.which]?K[e.which]:q[e.which]?q[e.which]:String.fromCharCode(e.which).toLowerCase()}function o(e,t){return e.sort().join(",")===t.sort().join(",")}function i(e){e=e||{};var t,n=!1;for(t in N)e[t]?n=!0:N[t]=0;n||(S=!1)}function a(e,t,n,r,i,a){var u,c,s=[],f=n.type;if(!M[e])return[];for("keyup"==f&&h(e)&&(t=[e]),u=0;u95&&112>e||K.hasOwnProperty(e)&&(C[K[e]]=e)}return C}function v(e,t,n){return n||(n=y()[e]?"keydown":"keypress"),"keypress"==n&&t.length&&(n="keydown"),n}function m(e,t,n,o){function a(t){return function(){S=t,++N[e],d()}}function u(t){f(n,t,e),"keyup"!==o&&(A=r(t)),setTimeout(i,10)}N[e]=0;for(var c=0;c1?void m(e,u,t,n):(i=g(e,n),M[i.key]=M[i.key]||[],a(i.key,i.modifiers,{type:i.action},r,e,o),void M[i.key][r?"unshift":"push"]({callback:t,modifiers:i.modifiers,action:i.action,seq:r,level:o,combo:e}))}function w(e,t,n){for(var r=0;r":".","?":"/","|":"\\"},T={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},M={},L={},N={},A=!1,O=!1,S=!1,j=1;20>j;++j)K[111+j]="f"+j;for(j=0;9>=j;++j)K[j+96]=j;n(t,"keypress",p),n(t,"keydown",p),n(t,"keyup",p);var x={bind:function(e,t,n){return e=e instanceof Array?e:[e],w(e,t,n),this},unbind:function(e,t){return x.bind(e,function(){},t)},trigger:function(e,t){return L[e+":"+t]&&L[e+":"+t]({},e),this},reset:function(){return M={},L={},this},stopCallback:function(e,t){return(" "+t.className+" ").indexOf(" mousetrap ")>-1?!1:"INPUT"==t.tagName||"SELECT"==t.tagName||"TEXTAREA"==t.tagName||t.isContentEditable},handleKey:l};e.Mousetrap=x,"function"==typeof define&&define.amd&&define(x)}(t,document);var n=t.Mousetrap;n=function(e){var t=e,n=t.stopCallback,r=!0;return t.stopCallback=function(e,t,o){return r?n(e,t,o):!0},t.pause=function(){r=!1},t.unpause=function(){r=!0},t}(n),n=function(e){var t,n=e,r=n.bind;return n.bind=function(){if(t=arguments,"string"==typeof t[0]||t[0]instanceof Array)return r(t[0],t[1],t[2]);for(var e in t[0])t[0].hasOwnProperty(e)&&r(e,t[0][e],t[1])},n}(n),e.module("angular-mousetrap",[]).factory("Mousetrap",[function(){return t.Mousetrap}])}(angular); -------------------------------------------------------------------------------- /src/angular-mousetrap-service.js: -------------------------------------------------------------------------------- 1 | angular.module('angular-mousetrap', []) 2 | .factory('Mousetrap', [function(){ 3 | return window.Mousetrap; 4 | }]); 5 | -------------------------------------------------------------------------------- /src/before-plugins.js: -------------------------------------------------------------------------------- 1 | var Mousetrap = window.Mousetrap; 2 | --------------------------------------------------------------------------------