kms 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/kms/application.js +1 -0
  3. data/app/assets/javascripts/kms/application/controllers/assets_controller.coffee.erb +14 -4
  4. data/app/assets/javascripts/kms/application/controllers/pages_controller.coffee.erb +12 -2
  5. data/app/assets/javascripts/kms/application/controllers/snippets_controller.coffee.erb +13 -3
  6. data/app/assets/javascripts/kms/application/controllers/templates_controller.coffee.erb +13 -3
  7. data/app/assets/javascripts/kms/application/controllers/users_controller.coffee +5 -5
  8. data/app/assets/javascripts/kms/application/module.coffee +6 -2
  9. data/app/assets/javascripts/kms/application/routes.coffee.erb +10 -0
  10. data/app/assets/javascripts/templates/assets/edit.html.slim +2 -1
  11. data/app/assets/javascripts/templates/assets/form.html.slim +1 -1
  12. data/app/assets/javascripts/templates/pages/edit.html.slim +1 -0
  13. data/app/assets/javascripts/templates/shared/hotkey_notification.html.slim +6 -0
  14. data/app/assets/javascripts/templates/snippets/edit.html.slim +1 -0
  15. data/app/assets/javascripts/templates/templates/edit.html.slim +1 -0
  16. data/app/assets/javascripts/templates/users/edit.html.slim +5 -0
  17. data/app/assets/javascripts/templates/users/form.html.slim +3 -2
  18. data/app/assets/javascripts/templates/users/index.html.slim +2 -1
  19. data/app/assets/stylesheets/kms/custom.css.scss +10 -0
  20. data/app/controllers/kms/assets_controller.rb +6 -3
  21. data/app/controllers/kms/users_controller.rb +14 -0
  22. data/app/services/kms/resource_service.rb +3 -1
  23. data/app/views/layouts/kms/kms.html.erb +1 -1
  24. data/config/initializers/devise.rb +9 -0
  25. data/config/locales/en.yml +12 -0
  26. data/config/locales/ru.yml +12 -0
  27. data/config/routes.rb +1 -1
  28. data/lib/kms/engine.rb +1 -1
  29. data/lib/kms/version.rb +1 -1
  30. data/spec/controllers/kms/assets_controller_spec.rb +28 -10
  31. data/spec/controllers/kms/users_controller_spec.rb +23 -0
  32. data/spec/internal/config/routes.rb +1 -1
  33. data/spec/internal/log/test.log +0 -105823
  34. data/vendor/assets/bower.json +5 -4
  35. data/vendor/assets/bower_components/angular-cookies/angular-cookies.js +22 -18
  36. data/vendor/assets/bower_components/angular-cookies/angular-cookies.min.js +4 -4
  37. data/vendor/assets/bower_components/angular-cookies/angular-cookies.min.js.map +2 -2
  38. data/vendor/assets/bower_components/angular-cookies/bower.json +2 -2
  39. data/vendor/assets/bower_components/angular-cookies/package.json +1 -1
  40. data/vendor/assets/bower_components/angular-hotkeys/Gruntfile.js +118 -0
  41. data/vendor/assets/bower_components/angular-hotkeys/LICENSE +20 -0
  42. data/vendor/assets/bower_components/angular-hotkeys/README.md +248 -0
  43. data/vendor/assets/bower_components/angular-hotkeys/bower.json +19 -0
  44. data/vendor/assets/bower_components/angular-hotkeys/build/hotkeys.css +110 -0
  45. data/vendor/assets/bower_components/angular-hotkeys/build/hotkeys.js +1661 -0
  46. data/vendor/assets/bower_components/angular-hotkeys/build/hotkeys.min.css +1 -0
  47. data/vendor/assets/bower_components/angular-hotkeys/build/hotkeys.min.js +7 -0
  48. data/vendor/assets/bower_components/angular-hotkeys/package.json +45 -0
  49. data/vendor/assets/bower_components/angular-hotkeys/src/hotkeys.css +104 -0
  50. data/vendor/assets/bower_components/angular-hotkeys/src/hotkeys.js +633 -0
  51. data/vendor/assets/bower_components/angular-loading-bar/CHANGELOG.md +33 -0
  52. data/vendor/assets/bower_components/angular-loading-bar/CONTRIBUTING.md +17 -0
  53. data/vendor/assets/bower_components/angular-loading-bar/Gruntfile.js +9 -1
  54. data/vendor/assets/bower_components/angular-loading-bar/ISSUE_TEMPLATE.md +14 -0
  55. data/vendor/assets/bower_components/angular-loading-bar/PULL_REQUEST_TEMPLATE.md +13 -0
  56. data/vendor/assets/bower_components/angular-loading-bar/README.md +30 -3
  57. data/vendor/assets/bower_components/angular-loading-bar/bower.json +11 -6
  58. data/vendor/assets/bower_components/angular-loading-bar/build/loading-bar.css +5 -5
  59. data/vendor/assets/bower_components/angular-loading-bar/build/loading-bar.js +39 -12
  60. data/vendor/assets/bower_components/angular-loading-bar/build/loading-bar.min.css +1 -8
  61. data/vendor/assets/bower_components/angular-loading-bar/build/loading-bar.min.js +3 -3
  62. data/vendor/assets/bower_components/angular-loading-bar/index.js +2 -0
  63. data/vendor/assets/bower_components/angular-loading-bar/package.json +12 -15
  64. data/vendor/assets/bower_components/angular-loading-bar/src/loading-bar.css +3 -3
  65. data/vendor/assets/bower_components/angular-loading-bar/src/loading-bar.js +37 -10
  66. data/vendor/assets/bower_components/angular-sanitize/angular-sanitize.js +504 -386
  67. data/vendor/assets/bower_components/angular-sanitize/angular-sanitize.min.js +13 -12
  68. data/vendor/assets/bower_components/angular-sanitize/angular-sanitize.min.js.map +3 -3
  69. data/vendor/assets/bower_components/angular-sanitize/bower.json +2 -2
  70. data/vendor/assets/bower_components/angular-sanitize/package.json +1 -1
  71. data/vendor/assets/bower_components/angular-ui-router/CHANGELOG.md +1410 -0
  72. data/vendor/assets/bower_components/angular-ui-router/CONTRIBUTING.md +64 -16
  73. data/vendor/assets/bower_components/angular-ui-router/DOCS.md +48 -0
  74. data/vendor/assets/bower_components/angular-ui-router/ISSUE_TEMPLATE.md +53 -0
  75. data/vendor/assets/bower_components/angular-ui-router/LICENSE +1 -1
  76. data/vendor/assets/bower_components/angular-ui-router/README.md +24 -211
  77. data/vendor/assets/bower_components/angular-ui-router/artifacts.json +8 -0
  78. data/vendor/assets/bower_components/angular-ui-router/bower.json +1 -23
  79. data/vendor/assets/bower_components/angular-ui-router/karma.conf.js +105 -0
  80. data/vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.js +9744 -3901
  81. data/vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.js.map +192 -0
  82. data/vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.min.js +9 -4
  83. data/vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.min.js.map +1679 -0
  84. data/vendor/assets/bower_components/angular-ui-router/release/resolveService.js +83 -0
  85. data/vendor/assets/bower_components/angular-ui-router/release/resolveService.js.map +19 -0
  86. data/vendor/assets/bower_components/angular-ui-router/release/resolveService.min.js +8 -0
  87. data/vendor/assets/bower_components/angular-ui-router/release/resolveService.min.js.map +47 -0
  88. data/vendor/assets/bower_components/angular-ui-router/release/stateEvents.js +294 -0
  89. data/vendor/assets/bower_components/angular-ui-router/release/stateEvents.js.map +17 -0
  90. data/vendor/assets/bower_components/angular-ui-router/release/stateEvents.min.js +8 -0
  91. data/vendor/assets/bower_components/angular-ui-router/release/stateEvents.min.js.map +102 -0
  92. data/vendor/assets/bower_components/angular-ui-router/release/ui-router-angularjs.js +2014 -0
  93. data/vendor/assets/bower_components/angular-ui-router/release/ui-router-angularjs.js.map +70 -0
  94. data/vendor/assets/bower_components/angular-ui-router/release/ui-router-angularjs.min.js +9 -0
  95. data/vendor/assets/bower_components/angular-ui-router/release/ui-router-angularjs.min.js.map +541 -0
  96. data/vendor/assets/bower_components/angular-ui-router/rollup.config.js +116 -0
  97. data/vendor/assets/bower_components/angular-ui-router/tslint.json +60 -0
  98. data/vendor/assets/bower_components/angular-ui-router/yarn.lock +4146 -0
  99. data/vendor/assets/bower_components/angular-ui-tree/yarn.lock +4945 -0
  100. data/vendor/assets/bower_components/angular/angular.js +4019 -2449
  101. data/vendor/assets/bower_components/angular/angular.min.js +331 -319
  102. data/vendor/assets/bower_components/angular/angular.min.js.gzip +0 -0
  103. data/vendor/assets/bower_components/angular/angular.min.js.map +3 -3
  104. data/vendor/assets/bower_components/angular/bower.json +1 -1
  105. data/vendor/assets/bower_components/angular/package.json +1 -1
  106. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/LICENSE +21 -0
  107. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/README.md +14 -14
  108. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/bower.json +25 -12
  109. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/development_index.html +59 -52
  110. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/dist/angularjs-dropdown-multiselect.min.js +1 -1
  111. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/index.html +73 -0
  112. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/package.json +19 -7
  113. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/pages/javascripts/pages/home/ExampleCtrl.js +126 -3
  114. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/pages/javascripts/pages/home/home.html +1262 -852
  115. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/pages/stylesheets/stylesheet.css +10 -5
  116. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/src/angularjs-dropdown-multiselect.js +612 -287
  117. metadata +66 -169
  118. data/spec/internal/config/database.yml +0 -7
  119. data/spec/internal/public/uploads/kms/asset/file/1/avatar.jpg +0 -0
  120. data/spec/internal/public/uploads/kms/asset/file/2/avatar.jpg +0 -0
  121. data/spec/internal/public/uploads/kms/asset/file/2/style.css +0 -1
  122. data/spec/internal/public/uploads/kms/asset/file/3/style.css +0 -1
  123. data/spec/internal/public/uploads/kms/asset/file/4/style.css +0 -1
  124. data/spec/internal/public/uploads/tmp/1500976987-41025-0002-0883/style.css +0 -1
  125. data/spec/internal/public/uploads/tmp/1500977082-41195-0002-6495/style.css +0 -1
  126. data/spec/internal/public/uploads/tmp/1500977109-41364-0002-4518/style.css +0 -1
  127. data/spec/internal/public/uploads/tmp/1500977152-41405-0002-2345/style.css +0 -1
  128. data/spec/internal/public/uploads/tmp/1500977327-41694-0002-5448/style.css +0 -1
  129. data/spec/internal/public/uploads/tmp/1500977376-41732-0002-7916/style.css +0 -1
  130. data/spec/internal/public/uploads/tmp/1500977392-41759-0002-7593/style.css +0 -1
  131. data/spec/internal/public/uploads/tmp/1500977410-42259-0002-7527/style.css +0 -1
  132. data/spec/internal/public/uploads/tmp/1500977429-42306-0002-5937/style.css +0 -1
  133. data/spec/internal/public/uploads/tmp/1500977437-42324-0002-5880/style.css +0 -1
  134. data/spec/internal/public/uploads/tmp/1500983228-53594-0002-4559/style.css +0 -1
  135. data/spec/internal/public/uploads/tmp/1500983284-53632-0002-6590/style.css +0 -1
  136. data/spec/internal/public/uploads/tmp/1500983360-53784-0002-7289/style.css +0 -1
  137. data/spec/internal/public/uploads/tmp/1500983469-54321-0002-0386/avatar.jpg +0 -0
  138. data/spec/internal/public/uploads/tmp/1500983469-54321-0004-5691/style.css +0 -1
  139. data/spec/internal/public/uploads/tmp/1500983511-54352-0002-5720/avatar.jpg +0 -0
  140. data/spec/internal/public/uploads/tmp/1500983511-54352-0004-1399/style.css +0 -1
  141. data/spec/internal/public/uploads/tmp/1500983610-54507-0002-4280/avatar.jpg +0 -0
  142. data/spec/internal/public/uploads/tmp/1500983610-54507-0004-9758/style.css +0 -1
  143. data/spec/internal/public/uploads/tmp/1500984466-57012-0002-4146/avatar.jpg +0 -0
  144. data/spec/internal/public/uploads/tmp/1500984466-57012-0004-5895/style.css +0 -1
  145. data/spec/internal/public/uploads/tmp/1500984509-57158-0002-9657/avatar.jpg +0 -0
  146. data/spec/internal/public/uploads/tmp/1500984509-57158-0004-5003/style.css +0 -1
  147. data/spec/internal/public/uploads/tmp/1500984616-57697-0002-7201/avatar.jpg +0 -0
  148. data/spec/internal/public/uploads/tmp/1500984616-57697-0004-6255/style.css +0 -1
  149. data/spec/internal/public/uploads/tmp/1500985257-58947-0002-3629/avatar.jpg +0 -0
  150. data/spec/internal/public/uploads/tmp/1500985257-58947-0004-5338/style.css +0 -1
  151. data/spec/internal/public/uploads/tmp/1500985407-58947-0006-5929/style.css +0 -1
  152. data/spec/internal/public/uploads/tmp/1500985473-59264-0002-0397/avatar.jpg +0 -0
  153. data/spec/internal/public/uploads/tmp/1500985473-59264-0004-6493/style.css +0 -1
  154. data/spec/internal/public/uploads/tmp/1500985475-59264-0007-8674/style.css +0 -1
  155. data/spec/internal/public/uploads/tmp/1500985538-59468-0002-9206/avatar.jpg +0 -0
  156. data/spec/internal/public/uploads/tmp/1500985538-59468-0004-2586/style.css +0 -1
  157. data/spec/internal/public/uploads/tmp/1500985538-59468-0007-6200/style.css +0 -1
  158. data/spec/internal/public/uploads/tmp/1500988358-65877-0002-4528/avatar.jpg +0 -0
  159. data/spec/internal/public/uploads/tmp/1500988358-65877-0004-5904/style.css +0 -1
  160. data/spec/internal/public/uploads/tmp/1500988358-65877-0007-7320/style.css +0 -1
  161. data/spec/internal/public/uploads/tmp/1500988407-65916-0002-3138/avatar.jpg +0 -0
  162. data/spec/internal/public/uploads/tmp/1500988407-65916-0004-5400/style.css +0 -1
  163. data/spec/internal/public/uploads/tmp/1500988407-65916-0007-1655/style.css +0 -1
  164. data/spec/internal/public/uploads/tmp/1500988421-65950-0002-9415/avatar.jpg +0 -0
  165. data/spec/internal/public/uploads/tmp/1500988421-65950-0004-7130/style.css +0 -1
  166. data/spec/internal/public/uploads/tmp/1500988421-65950-0007-9886/style.css +0 -1
  167. data/spec/internal/public/uploads/tmp/1500988435-65981-0002-3228/avatar.jpg +0 -0
  168. data/spec/internal/public/uploads/tmp/1500988435-65981-0004-3682/style.css +0 -1
  169. data/spec/internal/public/uploads/tmp/1500988435-65981-0007-1582/style.css +0 -1
  170. data/spec/internal/public/uploads/tmp/1500988475-66122-0002-9516/avatar.jpg +0 -0
  171. data/spec/internal/public/uploads/tmp/1500988475-66122-0004-5634/style.css +0 -1
  172. data/spec/internal/public/uploads/tmp/1500988530-66122-0007-2272/style.css +0 -1
  173. data/spec/internal/public/uploads/tmp/1500988554-66315-0002-6262/avatar.jpg +0 -0
  174. data/spec/internal/public/uploads/tmp/1500988554-66315-0004-6099/style.css +0 -1
  175. data/spec/internal/public/uploads/tmp/1500988554-66315-0007-1632/style.css +0 -1
  176. data/spec/internal/public/uploads/tmp/1500991751-73722-0002-9937/avatar.jpg +0 -0
  177. data/spec/internal/public/uploads/tmp/1500991751-73722-0004-8034/style.css +0 -1
  178. data/spec/internal/public/uploads/tmp/1500991751-73722-0007-7763/style.css +0 -1
  179. data/spec/internal/public/uploads/tmp/1501233238-34385-0002-3210/avatar.jpg +0 -0
  180. data/spec/internal/public/uploads/tmp/1501233238-34385-0004-5881/style.css +0 -1
  181. data/spec/internal/public/uploads/tmp/1501233238-34385-0007-6280/style.css +0 -1
  182. data/spec/internal/tmp/cache/assets/test/sprockets/v3.0/1XyAFYlYI0pK7WAgjR4PgXV6BgU6huJSviWmHetdCRs.cache +0 -1
  183. data/vendor/assets/bower_components/angular-ui-router/api/angular-ui-router.d.ts +0 -126
  184. data/vendor/assets/bower_components/angular-ui-router/src/common.js +0 -292
  185. data/vendor/assets/bower_components/angular-ui-router/src/resolve.js +0 -252
  186. data/vendor/assets/bower_components/angular-ui-router/src/state.js +0 -1373
  187. data/vendor/assets/bower_components/angular-ui-router/src/stateDirectives.js +0 -268
  188. data/vendor/assets/bower_components/angular-ui-router/src/stateFilters.js +0 -39
  189. data/vendor/assets/bower_components/angular-ui-router/src/templateFactory.js +0 -110
  190. data/vendor/assets/bower_components/angular-ui-router/src/urlMatcherFactory.js +0 -1036
  191. data/vendor/assets/bower_components/angular-ui-router/src/urlRouter.js +0 -413
  192. data/vendor/assets/bower_components/angular-ui-router/src/view.js +0 -71
  193. data/vendor/assets/bower_components/angular-ui-router/src/viewDirective.js +0 -302
  194. data/vendor/assets/bower_components/angular-ui-router/src/viewScroll.js +0 -52
  195. data/vendor/assets/bower_components/angularjs-dropdown-multiselect/pages/index.html +0 -67
  196. data/vendor/assets/bower_components/bootstrap/Gemfile.lock +0 -43
@@ -0,0 +1,19 @@
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
+ }
@@ -0,0 +1,110 @@
1
+ /*!
2
+ * angular-hotkeys v1.7.0
3
+ * https://chieffancypants.github.io/angular-hotkeys
4
+ * Copyright (c) 2016 Wes Cruver
5
+ * License: MIT
6
+ */
7
+ .cfp-hotkeys-container {
8
+ display: table !important;
9
+ position: fixed;
10
+ width: 100%;
11
+ height: 100%;
12
+ top: 0;
13
+ left: 0;
14
+ color: #333;
15
+ font-size: 1em;
16
+ background-color: rgba(255,255,255,0.9);
17
+ }
18
+
19
+ .cfp-hotkeys-container.fade {
20
+ z-index: -1024;
21
+ visibility: hidden;
22
+ opacity: 0;
23
+ -webkit-transition: opacity 0.15s linear;
24
+ -moz-transition: opacity 0.15s linear;
25
+ -o-transition: opacity 0.15s linear;
26
+ transition: opacity 0.15s linear;
27
+ }
28
+
29
+ .cfp-hotkeys-container.fade.in {
30
+ z-index: 10002;
31
+ visibility: visible;
32
+ opacity: 1;
33
+ }
34
+
35
+ .cfp-hotkeys-title {
36
+ font-weight: bold;
37
+ text-align: center;
38
+ font-size: 1.2em;
39
+ }
40
+
41
+ .cfp-hotkeys {
42
+ width: 100%;
43
+ height: 100%;
44
+ display: table-cell;
45
+ vertical-align: middle;
46
+ }
47
+
48
+ .cfp-hotkeys table {
49
+ margin: auto;
50
+ color: #333;
51
+ }
52
+
53
+ .cfp-content {
54
+ display: table-cell;
55
+ vertical-align: middle;
56
+ }
57
+
58
+ .cfp-hotkeys-keys {
59
+ padding: 5px;
60
+ text-align: right;
61
+ }
62
+
63
+ .cfp-hotkeys-key {
64
+ display: inline-block;
65
+ color: #fff;
66
+ background-color: #333;
67
+ border: 1px solid #333;
68
+ border-radius: 5px;
69
+ text-align: center;
70
+ margin-right: 5px;
71
+ box-shadow: inset 0 1px 0 #666, 0 1px 0 #bbb;
72
+ padding: 5px 9px;
73
+ font-size: 1em;
74
+ }
75
+
76
+ .cfp-hotkeys-text {
77
+ padding-left: 10px;
78
+ font-size: 1em;
79
+ }
80
+
81
+ .cfp-hotkeys-close {
82
+ position: fixed;
83
+ top: 20px;
84
+ right: 20px;
85
+ font-size: 2em;
86
+ font-weight: bold;
87
+ padding: 5px 10px;
88
+ border: 1px solid #ddd;
89
+ border-radius: 5px;
90
+ min-height: 45px;
91
+ min-width: 45px;
92
+ text-align: center;
93
+ }
94
+
95
+ .cfp-hotkeys-close:hover {
96
+ background-color: #fff;
97
+ cursor: pointer;
98
+ }
99
+
100
+ @media all and (max-width: 500px) {
101
+ .cfp-hotkeys {
102
+ font-size: 0.8em;
103
+ }
104
+ }
105
+
106
+ @media all and (min-width: 750px) {
107
+ .cfp-hotkeys {
108
+ font-size: 1.2em;
109
+ }
110
+ }
@@ -0,0 +1,1661 @@
1
+ /*!
2
+ * angular-hotkeys v1.7.0
3
+ * https://chieffancypants.github.io/angular-hotkeys
4
+ * Copyright (c) 2016 Wes Cruver
5
+ * License: MIT
6
+ */
7
+ /*
8
+ * angular-hotkeys
9
+ *
10
+ * Automatic keyboard shortcuts for your angular apps
11
+ *
12
+ * (c) 2016 Wes Cruver
13
+ * License: MIT
14
+ */
15
+
16
+ (function() {
17
+
18
+ 'use strict';
19
+
20
+ angular.module('cfp.hotkeys', []).provider('hotkeys', ['$injector', function($injector) {
21
+
22
+ /**
23
+ * Configurable setting to disable the cheatsheet entirely
24
+ * @type {Boolean}
25
+ */
26
+ this.includeCheatSheet = true;
27
+
28
+ /**
29
+ * Configurable setting to disable ngRoute hooks
30
+ * @type {Boolean}
31
+ */
32
+ this.useNgRoute = $injector.has('ngViewDirective');
33
+
34
+ /**
35
+ * Configurable setting for the cheat sheet title
36
+ * @type {String}
37
+ */
38
+
39
+ this.templateTitle = 'Keyboard Shortcuts:';
40
+
41
+ /**
42
+ * Configurable settings for the cheat sheet header and footer. Both are HTML, and the header
43
+ * overrides the normal title if specified.
44
+ * @type {String}
45
+ */
46
+ this.templateHeader = null;
47
+ this.templateFooter = null;
48
+
49
+ /**
50
+ * Cheat sheet template in the event you want to totally customize it.
51
+ * @type {String}
52
+ */
53
+ this.template = '<div class="cfp-hotkeys-container fade" ng-class="{in: helpVisible}" style="display: none;"><div class="cfp-hotkeys">' +
54
+ '<h4 class="cfp-hotkeys-title" ng-if="!header">{{ title }}</h4>' +
55
+ '<div ng-bind-html="header" ng-if="header"></div>' +
56
+ '<table><tbody>' +
57
+ '<tr ng-repeat="hotkey in hotkeys | filter:{ description: \'!$$undefined$$\' }">' +
58
+ '<td class="cfp-hotkeys-keys">' +
59
+ '<span ng-repeat="key in hotkey.format() track by $index" class="cfp-hotkeys-key">{{ key }}</span>' +
60
+ '</td>' +
61
+ '<td class="cfp-hotkeys-text">{{ hotkey.description }}</td>' +
62
+ '</tr>' +
63
+ '</tbody></table>' +
64
+ '<div ng-bind-html="footer" ng-if="footer"></div>' +
65
+ '<div class="cfp-hotkeys-close" ng-click="toggleCheatSheet()">&#215;</div>' +
66
+ '</div></div>';
67
+
68
+ /**
69
+ * Configurable setting for the cheat sheet hotkey
70
+ * @type {String}
71
+ */
72
+ this.cheatSheetHotkey = '?';
73
+
74
+ /**
75
+ * Configurable setting for the cheat sheet description
76
+ * @type {String}
77
+ */
78
+ this.cheatSheetDescription = 'Show / hide this help menu';
79
+
80
+ this.$get = ['$rootElement', '$rootScope', '$compile', '$window', '$document', function ($rootElement, $rootScope, $compile, $window, $document) {
81
+
82
+ var mouseTrapEnabled = true;
83
+
84
+ function pause() {
85
+ mouseTrapEnabled = false;
86
+ }
87
+
88
+ function unpause() {
89
+ mouseTrapEnabled = true;
90
+ }
91
+
92
+ // monkeypatch Mousetrap's stopCallback() function
93
+ // this version doesn't return true when the element is an INPUT, SELECT, or TEXTAREA
94
+ // (instead we will perform this check per-key in the _add() method)
95
+ Mousetrap.prototype.stopCallback = function(event, element) {
96
+ if (!mouseTrapEnabled) {
97
+ return true;
98
+ }
99
+
100
+ // if the element has the class "mousetrap" then no need to stop
101
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
102
+ return false;
103
+ }
104
+
105
+ return (element.contentEditable && element.contentEditable == 'true');
106
+ };
107
+
108
+ /**
109
+ * Convert strings like cmd into symbols like ⌘
110
+ * @param {String} combo Key combination, e.g. 'mod+f'
111
+ * @return {String} The key combination with symbols
112
+ */
113
+ function symbolize (combo) {
114
+ var map = {
115
+ command : '\u2318', // ⌘
116
+ shift : '\u21E7', // ⇧
117
+ left : '\u2190', // ←
118
+ right : '\u2192', // →
119
+ up : '\u2191', // ↑
120
+ down : '\u2193', // ↓
121
+ 'return' : '\u23CE', // ⏎
122
+ backspace : '\u232B' // ⌫
123
+ };
124
+ combo = combo.split('+');
125
+
126
+ for (var i = 0; i < combo.length; i++) {
127
+ // try to resolve command / ctrl based on OS:
128
+ if (combo[i] === 'mod') {
129
+ if ($window.navigator && $window.navigator.platform.indexOf('Mac') >=0 ) {
130
+ combo[i] = 'command';
131
+ } else {
132
+ combo[i] = 'ctrl';
133
+ }
134
+ }
135
+
136
+ combo[i] = map[combo[i]] || combo[i];
137
+ }
138
+
139
+ return combo.join(' + ');
140
+ }
141
+
142
+ /**
143
+ * Hotkey object used internally for consistency
144
+ *
145
+ * @param {array} combo The keycombo. it's an array to support multiple combos
146
+ * @param {String} description Description for the keycombo
147
+ * @param {Function} callback function to execute when keycombo pressed
148
+ * @param {string} action the type of event to listen for (for mousetrap)
149
+ * @param {array} allowIn an array of tag names to allow this combo in ('INPUT', 'SELECT', and/or 'TEXTAREA')
150
+ * @param {Boolean} persistent Whether the hotkey persists navigation events
151
+ */
152
+ function Hotkey (combo, description, callback, action, allowIn, persistent) {
153
+ // TODO: Check that the values are sane because we could
154
+ // be trying to instantiate a new Hotkey with outside dev's
155
+ // supplied values
156
+
157
+ this.combo = combo instanceof Array ? combo : [combo];
158
+ this.description = description;
159
+ this.callback = callback;
160
+ this.action = action;
161
+ this.allowIn = allowIn;
162
+ this.persistent = persistent;
163
+ this._formated = null;
164
+ }
165
+
166
+ /**
167
+ * Helper method to format (symbolize) the key combo for display
168
+ *
169
+ * @return {[Array]} An array of the key combination sequence
170
+ * for example: "command+g c i" becomes ["⌘ + g", "c", "i"]
171
+ *
172
+ */
173
+ Hotkey.prototype.format = function() {
174
+ if (this._formated === null) {
175
+ // Don't show all the possible key combos, just the first one. Not sure
176
+ // of usecase here, so open a ticket if my assumptions are wrong
177
+ var combo = this.combo[0];
178
+
179
+ var sequence = combo.split(/[\s]/);
180
+ for (var i = 0; i < sequence.length; i++) {
181
+ sequence[i] = symbolize(sequence[i]);
182
+ }
183
+ this._formated = sequence;
184
+ }
185
+
186
+ return this._formated;
187
+ };
188
+
189
+ /**
190
+ * A new scope used internally for the cheatsheet
191
+ * @type {$rootScope.Scope}
192
+ */
193
+ var scope = $rootScope.$new();
194
+
195
+ /**
196
+ * Holds an array of Hotkey objects currently bound
197
+ * @type {Array}
198
+ */
199
+ scope.hotkeys = [];
200
+
201
+ /**
202
+ * Contains the state of the help's visibility
203
+ * @type {Boolean}
204
+ */
205
+ scope.helpVisible = false;
206
+
207
+ /**
208
+ * Holds the title string for the help menu
209
+ * @type {String}
210
+ */
211
+ scope.title = this.templateTitle;
212
+
213
+ /**
214
+ * Holds the header HTML for the help menu
215
+ * @type {String}
216
+ */
217
+ scope.header = this.templateHeader;
218
+
219
+ /**
220
+ * Holds the footer HTML for the help menu
221
+ * @type {String}
222
+ */
223
+ scope.footer = this.templateFooter;
224
+
225
+ /**
226
+ * Expose toggleCheatSheet to hotkeys scope so we can call it using
227
+ * ng-click from the template
228
+ * @type {function}
229
+ */
230
+ scope.toggleCheatSheet = toggleCheatSheet;
231
+
232
+
233
+ /**
234
+ * Holds references to the different scopes that have bound hotkeys
235
+ * attached. This is useful to catch when the scopes are `$destroy`d and
236
+ * then automatically unbind the hotkey.
237
+ *
238
+ * @type {Object}
239
+ */
240
+ var boundScopes = {};
241
+
242
+ if (this.useNgRoute) {
243
+ $rootScope.$on('$routeChangeSuccess', function (event, route) {
244
+ purgeHotkeys();
245
+
246
+ if (route && route.hotkeys) {
247
+ angular.forEach(route.hotkeys, function (hotkey) {
248
+ // a string was given, which implies this is a function that is to be
249
+ // $eval()'d within that controller's scope
250
+ // TODO: hotkey here is super confusing. sometimes a function (that gets turned into an array), sometimes a string
251
+ var callback = hotkey[2];
252
+ if (typeof(callback) === 'string' || callback instanceof String) {
253
+ hotkey[2] = [callback, route];
254
+ }
255
+
256
+ // todo: perform check to make sure not already defined:
257
+ // this came from a route, so it's likely not meant to be persistent
258
+ hotkey[5] = false;
259
+ _add.apply(this, hotkey);
260
+ });
261
+ }
262
+ });
263
+ }
264
+
265
+
266
+
267
+ // Auto-create a help menu:
268
+ if (this.includeCheatSheet) {
269
+ var document = $document[0];
270
+ var element = $rootElement[0];
271
+ var helpMenu = angular.element(this.template);
272
+ _add(this.cheatSheetHotkey, this.cheatSheetDescription, toggleCheatSheet);
273
+
274
+ // If $rootElement is document or documentElement, then body must be used
275
+ if (element === document || element === document.documentElement) {
276
+ element = document.body;
277
+ }
278
+
279
+ angular.element(element).append($compile(helpMenu)(scope));
280
+ }
281
+
282
+
283
+ /**
284
+ * Purges all non-persistent hotkeys (such as those defined in routes)
285
+ *
286
+ * Without this, the same hotkey would get recreated everytime
287
+ * the route is accessed.
288
+ */
289
+ function purgeHotkeys() {
290
+ var i = scope.hotkeys.length;
291
+ while (i--) {
292
+ var hotkey = scope.hotkeys[i];
293
+ if (hotkey && !hotkey.persistent) {
294
+ _del(hotkey);
295
+ }
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Toggles the help menu element's visiblity
301
+ */
302
+ var previousEsc = false;
303
+
304
+ function toggleCheatSheet() {
305
+ scope.helpVisible = !scope.helpVisible;
306
+
307
+ // Bind to esc to remove the cheat sheet. Ideally, this would be done
308
+ // as a directive in the template, but that would create a nasty
309
+ // circular dependency issue that I don't feel like sorting out.
310
+ if (scope.helpVisible) {
311
+ previousEsc = _get('esc');
312
+ _del('esc');
313
+
314
+ // Here's an odd way to do this: we're going to use the original
315
+ // description of the hotkey on the cheat sheet so that it shows up.
316
+ // without it, no entry for esc will ever show up (#22)
317
+ _add('esc', previousEsc.description, toggleCheatSheet, null, ['INPUT', 'SELECT', 'TEXTAREA']);
318
+ } else {
319
+ _del('esc');
320
+
321
+ // restore the previously bound ESC key
322
+ if (previousEsc !== false) {
323
+ _add(previousEsc);
324
+ }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Creates a new Hotkey and creates the Mousetrap binding
330
+ *
331
+ * @param {string} combo mousetrap key binding
332
+ * @param {string} description description for the help menu
333
+ * @param {Function} callback method to call when key is pressed
334
+ * @param {string} action the type of event to listen for (for mousetrap)
335
+ * @param {array} allowIn an array of tag names to allow this combo in ('INPUT', 'SELECT', and/or 'TEXTAREA')
336
+ * @param {boolean} persistent if true, the binding is preserved upon route changes
337
+ */
338
+ function _add (combo, description, callback, action, allowIn, persistent) {
339
+
340
+ // used to save original callback for "allowIn" wrapping:
341
+ var _callback;
342
+
343
+ // these elements are prevented by the default Mousetrap.stopCallback():
344
+ var preventIn = ['INPUT', 'SELECT', 'TEXTAREA'];
345
+
346
+ // Determine if object format was given:
347
+ var objType = Object.prototype.toString.call(combo);
348
+
349
+ if (objType === '[object Object]') {
350
+ description = combo.description;
351
+ callback = combo.callback;
352
+ action = combo.action;
353
+ persistent = combo.persistent;
354
+ allowIn = combo.allowIn;
355
+ combo = combo.combo;
356
+ }
357
+
358
+ // no duplicates please
359
+ _del(combo);
360
+
361
+ // description is optional:
362
+ if (description instanceof Function) {
363
+ action = callback;
364
+ callback = description;
365
+ description = '$$undefined$$';
366
+ } else if (angular.isUndefined(description)) {
367
+ description = '$$undefined$$';
368
+ }
369
+
370
+ // any items added through the public API are for controllers
371
+ // that persist through navigation, and thus undefined should mean
372
+ // true in this case.
373
+ if (persistent === undefined) {
374
+ persistent = true;
375
+ }
376
+ // if callback is defined, then wrap it in a function
377
+ // that checks if the event originated from a form element.
378
+ // the function blocks the callback from executing unless the element is specified
379
+ // in allowIn (emulates Mousetrap.stopCallback() on a per-key level)
380
+ if (typeof callback === 'function') {
381
+
382
+ // save the original callback
383
+ _callback = callback;
384
+
385
+ // make sure allowIn is an array
386
+ if (!(allowIn instanceof Array)) {
387
+ allowIn = [];
388
+ }
389
+
390
+ // remove anything from preventIn that's present in allowIn
391
+ var index;
392
+ for (var i=0; i < allowIn.length; i++) {
393
+ allowIn[i] = allowIn[i].toUpperCase();
394
+ index = preventIn.indexOf(allowIn[i]);
395
+ if (index !== -1) {
396
+ preventIn.splice(index, 1);
397
+ }
398
+ }
399
+
400
+ // create the new wrapper callback
401
+ callback = function(event) {
402
+ var shouldExecute = true;
403
+
404
+ // if the callback is executed directly `hotkey.get('w').callback()`
405
+ // there will be no event, so just execute the callback.
406
+ if (event) {
407
+ var target = event.target || event.srcElement; // srcElement is IE only
408
+ var nodeName = target.nodeName.toUpperCase();
409
+
410
+ // check if the input has a mousetrap class, and skip checking preventIn if so
411
+ if ((' ' + target.className + ' ').indexOf(' mousetrap ') > -1) {
412
+ shouldExecute = true;
413
+ } else {
414
+ // don't execute callback if the event was fired from inside an element listed in preventIn
415
+ for (var i=0; i<preventIn.length; i++) {
416
+ if (preventIn[i] === nodeName) {
417
+ shouldExecute = false;
418
+ break;
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ if (shouldExecute) {
425
+ wrapApply(_callback.apply(this, arguments));
426
+ }
427
+ };
428
+ }
429
+
430
+ if (typeof(action) === 'string') {
431
+ Mousetrap.bind(combo, wrapApply(callback), action);
432
+ } else {
433
+ Mousetrap.bind(combo, wrapApply(callback));
434
+ }
435
+
436
+ var hotkey = new Hotkey(combo, description, callback, action, allowIn, persistent);
437
+ scope.hotkeys.push(hotkey);
438
+ return hotkey;
439
+ }
440
+
441
+ /**
442
+ * delete and unbind a Hotkey
443
+ *
444
+ * @param {mixed} hotkey Either the bound key or an instance of Hotkey
445
+ * @return {boolean} true if successful
446
+ */
447
+ function _del (hotkey) {
448
+ var combo = (hotkey instanceof Hotkey) ? hotkey.combo : hotkey;
449
+
450
+ Mousetrap.unbind(combo);
451
+
452
+ if (angular.isArray(combo)) {
453
+ var retStatus = true;
454
+ var i = combo.length;
455
+ while (i--) {
456
+ retStatus = _del(combo[i]) && retStatus;
457
+ }
458
+ return retStatus;
459
+ } else {
460
+ var index = scope.hotkeys.indexOf(_get(combo));
461
+
462
+ if (index > -1) {
463
+ // if the combo has other combos bound, don't unbind the whole thing, just the one combo:
464
+ if (scope.hotkeys[index].combo.length > 1) {
465
+ scope.hotkeys[index].combo.splice(scope.hotkeys[index].combo.indexOf(combo), 1);
466
+ } else {
467
+
468
+ // remove hotkey from bound scopes
469
+ angular.forEach(boundScopes, function (boundScope) {
470
+ var scopeIndex = boundScope.indexOf(scope.hotkeys[index]);
471
+ if (scopeIndex !== -1) {
472
+ boundScope.splice(scopeIndex, 1);
473
+ }
474
+ });
475
+
476
+ scope.hotkeys.splice(index, 1);
477
+ }
478
+ return true;
479
+ }
480
+ }
481
+
482
+ return false;
483
+
484
+ }
485
+
486
+ /**
487
+ * Get a Hotkey object by key binding
488
+ *
489
+ * @param {[string]} [combo] the key the Hotkey is bound to. Returns all key bindings if no key is passed
490
+ * @return {Hotkey} The Hotkey object
491
+ */
492
+ function _get (combo) {
493
+
494
+ if (!combo) {
495
+ return scope.hotkeys;
496
+ }
497
+
498
+ var hotkey;
499
+
500
+ for (var i = 0; i < scope.hotkeys.length; i++) {
501
+ hotkey = scope.hotkeys[i];
502
+
503
+ if (hotkey.combo.indexOf(combo) > -1) {
504
+ return hotkey;
505
+ }
506
+ }
507
+
508
+ return false;
509
+ }
510
+
511
+ /**
512
+ * Binds the hotkey to a particular scope. Useful if the scope is
513
+ * destroyed, we can automatically destroy the hotkey binding.
514
+ *
515
+ * @param {Object} scope The scope to bind to
516
+ */
517
+ function bindTo (scope) {
518
+ // Only initialize once to allow multiple calls for same scope.
519
+ if (!(scope.$id in boundScopes)) {
520
+
521
+ // Add the scope to the list of bound scopes
522
+ boundScopes[scope.$id] = [];
523
+
524
+ scope.$on('$destroy', function () {
525
+ var i = boundScopes[scope.$id].length;
526
+ while (i--) {
527
+ _del(boundScopes[scope.$id].pop());
528
+ }
529
+ });
530
+ }
531
+ // return an object with an add function so we can keep track of the
532
+ // hotkeys and their scope that we added via this chaining method
533
+ return {
534
+ add: function (args) {
535
+ var hotkey;
536
+
537
+ if (arguments.length > 1) {
538
+ hotkey = _add.apply(this, arguments);
539
+ } else {
540
+ hotkey = _add(args);
541
+ }
542
+
543
+ boundScopes[scope.$id].push(hotkey);
544
+ return this;
545
+ }
546
+ };
547
+ }
548
+
549
+ /**
550
+ * All callbacks sent to Mousetrap are wrapped using this function
551
+ * so that we can force a $scope.$apply()
552
+ *
553
+ * @param {Function} callback [description]
554
+ * @return {[type]} [description]
555
+ */
556
+ function wrapApply (callback) {
557
+ // return mousetrap a function to call
558
+ return function (event, combo) {
559
+
560
+ // if this is an array, it means we provided a route object
561
+ // because the scope wasn't available yet, so rewrap the callback
562
+ // now that the scope is available:
563
+ if (callback instanceof Array) {
564
+ var funcString = callback[0];
565
+ var route = callback[1];
566
+ callback = function (event) {
567
+ route.scope.$eval(funcString);
568
+ };
569
+ }
570
+
571
+ // this takes place outside angular, so we'll have to call
572
+ // $apply() to make sure angular's digest happens
573
+ $rootScope.$apply(function() {
574
+ // call the original hotkey callback with the keyboard event
575
+ callback(event, _get(combo));
576
+ });
577
+ };
578
+ }
579
+
580
+ var publicApi = {
581
+ add : _add,
582
+ del : _del,
583
+ get : _get,
584
+ bindTo : bindTo,
585
+ template : this.template,
586
+ toggleCheatSheet : toggleCheatSheet,
587
+ includeCheatSheet : this.includeCheatSheet,
588
+ cheatSheetHotkey : this.cheatSheetHotkey,
589
+ cheatSheetDescription : this.cheatSheetDescription,
590
+ useNgRoute : this.useNgRoute,
591
+ purgeHotkeys : purgeHotkeys,
592
+ templateTitle : this.templateTitle,
593
+ pause : pause,
594
+ unpause : unpause
595
+ };
596
+
597
+ return publicApi;
598
+
599
+ }];
600
+
601
+
602
+ }])
603
+
604
+ .directive('hotkey', ['hotkeys', function (hotkeys) {
605
+ return {
606
+ restrict: 'A',
607
+ link: function (scope, el, attrs) {
608
+ var keys = [],
609
+ allowIn;
610
+
611
+ angular.forEach(scope.$eval(attrs.hotkey), function (func, hotkey) {
612
+ // split and trim the hotkeys string into array
613
+ allowIn = typeof attrs.hotkeyAllowIn === "string" ? attrs.hotkeyAllowIn.split(/[\s,]+/) : [];
614
+
615
+ keys.push(hotkey);
616
+
617
+ hotkeys.add({
618
+ combo: hotkey,
619
+ description: attrs.hotkeyDescription,
620
+ callback: func,
621
+ action: attrs.hotkeyAction,
622
+ allowIn: allowIn
623
+ });
624
+ });
625
+
626
+ // remove the hotkey if the directive is destroyed:
627
+ el.bind('$destroy', function() {
628
+ angular.forEach(keys, hotkeys.del);
629
+ });
630
+ }
631
+ };
632
+ }])
633
+
634
+ .run(['hotkeys', function(hotkeys) {
635
+ // force hotkeys to run by injecting it. Without this, hotkeys only runs
636
+ // when a controller or something else asks for it via DI.
637
+ }]);
638
+
639
+ })();
640
+
641
+ /*global define:false */
642
+ /**
643
+ * Copyright 2015 Craig Campbell
644
+ *
645
+ * Licensed under the Apache License, Version 2.0 (the "License");
646
+ * you may not use this file except in compliance with the License.
647
+ * You may obtain a copy of the License at
648
+ *
649
+ * http://www.apache.org/licenses/LICENSE-2.0
650
+ *
651
+ * Unless required by applicable law or agreed to in writing, software
652
+ * distributed under the License is distributed on an "AS IS" BASIS,
653
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
654
+ * See the License for the specific language governing permissions and
655
+ * limitations under the License.
656
+ *
657
+ * Mousetrap is a simple keyboard shortcut library for Javascript with
658
+ * no external dependencies
659
+ *
660
+ * @version 1.5.2
661
+ * @url craig.is/killing/mice
662
+ */
663
+ (function(window, document, undefined) {
664
+
665
+ /**
666
+ * mapping of special keycodes to their corresponding keys
667
+ *
668
+ * everything in this dictionary cannot use keypress events
669
+ * so it has to be here to map to the correct keycodes for
670
+ * keyup/keydown events
671
+ *
672
+ * @type {Object}
673
+ */
674
+ var _MAP = {
675
+ 8: 'backspace',
676
+ 9: 'tab',
677
+ 13: 'enter',
678
+ 16: 'shift',
679
+ 17: 'ctrl',
680
+ 18: 'alt',
681
+ 20: 'capslock',
682
+ 27: 'esc',
683
+ 32: 'space',
684
+ 33: 'pageup',
685
+ 34: 'pagedown',
686
+ 35: 'end',
687
+ 36: 'home',
688
+ 37: 'left',
689
+ 38: 'up',
690
+ 39: 'right',
691
+ 40: 'down',
692
+ 45: 'ins',
693
+ 46: 'del',
694
+ 91: 'meta',
695
+ 93: 'meta',
696
+ 224: 'meta'
697
+ };
698
+
699
+ /**
700
+ * mapping for special characters so they can support
701
+ *
702
+ * this dictionary is only used incase you want to bind a
703
+ * keyup or keydown event to one of these keys
704
+ *
705
+ * @type {Object}
706
+ */
707
+ var _KEYCODE_MAP = {
708
+ 106: '*',
709
+ 107: '+',
710
+ 109: '-',
711
+ 110: '.',
712
+ 111 : '/',
713
+ 186: ';',
714
+ 187: '=',
715
+ 188: ',',
716
+ 189: '-',
717
+ 190: '.',
718
+ 191: '/',
719
+ 192: '`',
720
+ 219: '[',
721
+ 220: '\\',
722
+ 221: ']',
723
+ 222: '\''
724
+ };
725
+
726
+ /**
727
+ * this is a mapping of keys that require shift on a US keypad
728
+ * back to the non shift equivelents
729
+ *
730
+ * this is so you can use keyup events with these keys
731
+ *
732
+ * note that this will only work reliably on US keyboards
733
+ *
734
+ * @type {Object}
735
+ */
736
+ var _SHIFT_MAP = {
737
+ '~': '`',
738
+ '!': '1',
739
+ '@': '2',
740
+ '#': '3',
741
+ '$': '4',
742
+ '%': '5',
743
+ '^': '6',
744
+ '&': '7',
745
+ '*': '8',
746
+ '(': '9',
747
+ ')': '0',
748
+ '_': '-',
749
+ '+': '=',
750
+ ':': ';',
751
+ '\"': '\'',
752
+ '<': ',',
753
+ '>': '.',
754
+ '?': '/',
755
+ '|': '\\'
756
+ };
757
+
758
+ /**
759
+ * this is a list of special strings you can use to map
760
+ * to modifier keys when you specify your keyboard shortcuts
761
+ *
762
+ * @type {Object}
763
+ */
764
+ var _SPECIAL_ALIASES = {
765
+ 'option': 'alt',
766
+ 'command': 'meta',
767
+ 'return': 'enter',
768
+ 'escape': 'esc',
769
+ 'plus': '+',
770
+ 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
771
+ };
772
+
773
+ /**
774
+ * variable to store the flipped version of _MAP from above
775
+ * needed to check if we should use keypress or not when no action
776
+ * is specified
777
+ *
778
+ * @type {Object|undefined}
779
+ */
780
+ var _REVERSE_MAP;
781
+
782
+ /**
783
+ * loop through the f keys, f1 to f19 and add them to the map
784
+ * programatically
785
+ */
786
+ for (var i = 1; i < 20; ++i) {
787
+ _MAP[111 + i] = 'f' + i;
788
+ }
789
+
790
+ /**
791
+ * loop through to map numbers on the numeric keypad
792
+ */
793
+ for (i = 0; i <= 9; ++i) {
794
+ _MAP[i + 96] = i;
795
+ }
796
+
797
+ /**
798
+ * cross browser add event method
799
+ *
800
+ * @param {Element|HTMLDocument} object
801
+ * @param {string} type
802
+ * @param {Function} callback
803
+ * @returns void
804
+ */
805
+ function _addEvent(object, type, callback) {
806
+ if (object.addEventListener) {
807
+ object.addEventListener(type, callback, false);
808
+ return;
809
+ }
810
+
811
+ object.attachEvent('on' + type, callback);
812
+ }
813
+
814
+ /**
815
+ * takes the event and returns the key character
816
+ *
817
+ * @param {Event} e
818
+ * @return {string}
819
+ */
820
+ function _characterFromEvent(e) {
821
+
822
+ // for keypress events we should return the character as is
823
+ if (e.type == 'keypress') {
824
+ var character = String.fromCharCode(e.which);
825
+
826
+ // if the shift key is not pressed then it is safe to assume
827
+ // that we want the character to be lowercase. this means if
828
+ // you accidentally have caps lock on then your key bindings
829
+ // will continue to work
830
+ //
831
+ // the only side effect that might not be desired is if you
832
+ // bind something like 'A' cause you want to trigger an
833
+ // event when capital A is pressed caps lock will no longer
834
+ // trigger the event. shift+a will though.
835
+ if (!e.shiftKey) {
836
+ character = character.toLowerCase();
837
+ }
838
+
839
+ return character;
840
+ }
841
+
842
+ // for non keypress events the special maps are needed
843
+ if (_MAP[e.which]) {
844
+ return _MAP[e.which];
845
+ }
846
+
847
+ if (_KEYCODE_MAP[e.which]) {
848
+ return _KEYCODE_MAP[e.which];
849
+ }
850
+
851
+ // if it is not in the special map
852
+
853
+ // with keydown and keyup events the character seems to always
854
+ // come in as an uppercase character whether you are pressing shift
855
+ // or not. we should make sure it is always lowercase for comparisons
856
+ return String.fromCharCode(e.which).toLowerCase();
857
+ }
858
+
859
+ /**
860
+ * checks if two arrays are equal
861
+ *
862
+ * @param {Array} modifiers1
863
+ * @param {Array} modifiers2
864
+ * @returns {boolean}
865
+ */
866
+ function _modifiersMatch(modifiers1, modifiers2) {
867
+ return modifiers1.sort().join(',') === modifiers2.sort().join(',');
868
+ }
869
+
870
+ /**
871
+ * takes a key event and figures out what the modifiers are
872
+ *
873
+ * @param {Event} e
874
+ * @returns {Array}
875
+ */
876
+ function _eventModifiers(e) {
877
+ var modifiers = [];
878
+
879
+ if (e.shiftKey) {
880
+ modifiers.push('shift');
881
+ }
882
+
883
+ if (e.altKey) {
884
+ modifiers.push('alt');
885
+ }
886
+
887
+ if (e.ctrlKey) {
888
+ modifiers.push('ctrl');
889
+ }
890
+
891
+ if (e.metaKey) {
892
+ modifiers.push('meta');
893
+ }
894
+
895
+ return modifiers;
896
+ }
897
+
898
+ /**
899
+ * prevents default for this event
900
+ *
901
+ * @param {Event} e
902
+ * @returns void
903
+ */
904
+ function _preventDefault(e) {
905
+ if (e.preventDefault) {
906
+ e.preventDefault();
907
+ return;
908
+ }
909
+
910
+ e.returnValue = false;
911
+ }
912
+
913
+ /**
914
+ * stops propogation for this event
915
+ *
916
+ * @param {Event} e
917
+ * @returns void
918
+ */
919
+ function _stopPropagation(e) {
920
+ if (e.stopPropagation) {
921
+ e.stopPropagation();
922
+ return;
923
+ }
924
+
925
+ e.cancelBubble = true;
926
+ }
927
+
928
+ /**
929
+ * determines if the keycode specified is a modifier key or not
930
+ *
931
+ * @param {string} key
932
+ * @returns {boolean}
933
+ */
934
+ function _isModifier(key) {
935
+ return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
936
+ }
937
+
938
+ /**
939
+ * reverses the map lookup so that we can look for specific keys
940
+ * to see what can and can't use keypress
941
+ *
942
+ * @return {Object}
943
+ */
944
+ function _getReverseMap() {
945
+ if (!_REVERSE_MAP) {
946
+ _REVERSE_MAP = {};
947
+ for (var key in _MAP) {
948
+
949
+ // pull out the numeric keypad from here cause keypress should
950
+ // be able to detect the keys from the character
951
+ if (key > 95 && key < 112) {
952
+ continue;
953
+ }
954
+
955
+ if (_MAP.hasOwnProperty(key)) {
956
+ _REVERSE_MAP[_MAP[key]] = key;
957
+ }
958
+ }
959
+ }
960
+ return _REVERSE_MAP;
961
+ }
962
+
963
+ /**
964
+ * picks the best action based on the key combination
965
+ *
966
+ * @param {string} key - character for key
967
+ * @param {Array} modifiers
968
+ * @param {string=} action passed in
969
+ */
970
+ function _pickBestAction(key, modifiers, action) {
971
+
972
+ // if no action was picked in we should try to pick the one
973
+ // that we think would work best for this key
974
+ if (!action) {
975
+ action = _getReverseMap()[key] ? 'keydown' : 'keypress';
976
+ }
977
+
978
+ // modifier keys don't work as expected with keypress,
979
+ // switch to keydown
980
+ if (action == 'keypress' && modifiers.length) {
981
+ action = 'keydown';
982
+ }
983
+
984
+ return action;
985
+ }
986
+
987
+ /**
988
+ * Converts from a string key combination to an array
989
+ *
990
+ * @param {string} combination like "command+shift+l"
991
+ * @return {Array}
992
+ */
993
+ function _keysFromString(combination) {
994
+ if (combination === '+') {
995
+ return ['+'];
996
+ }
997
+
998
+ combination = combination.replace(/\+{2}/g, '+plus');
999
+ return combination.split('+');
1000
+ }
1001
+
1002
+ /**
1003
+ * Gets info for a specific key combination
1004
+ *
1005
+ * @param {string} combination key combination ("command+s" or "a" or "*")
1006
+ * @param {string=} action
1007
+ * @returns {Object}
1008
+ */
1009
+ function _getKeyInfo(combination, action) {
1010
+ var keys;
1011
+ var key;
1012
+ var i;
1013
+ var modifiers = [];
1014
+
1015
+ // take the keys from this pattern and figure out what the actual
1016
+ // pattern is all about
1017
+ keys = _keysFromString(combination);
1018
+
1019
+ for (i = 0; i < keys.length; ++i) {
1020
+ key = keys[i];
1021
+
1022
+ // normalize key names
1023
+ if (_SPECIAL_ALIASES[key]) {
1024
+ key = _SPECIAL_ALIASES[key];
1025
+ }
1026
+
1027
+ // if this is not a keypress event then we should
1028
+ // be smart about using shift keys
1029
+ // this will only work for US keyboards however
1030
+ if (action && action != 'keypress' && _SHIFT_MAP[key]) {
1031
+ key = _SHIFT_MAP[key];
1032
+ modifiers.push('shift');
1033
+ }
1034
+
1035
+ // if this key is a modifier then add it to the list of modifiers
1036
+ if (_isModifier(key)) {
1037
+ modifiers.push(key);
1038
+ }
1039
+ }
1040
+
1041
+ // depending on what the key combination is
1042
+ // we will try to pick the best event for it
1043
+ action = _pickBestAction(key, modifiers, action);
1044
+
1045
+ return {
1046
+ key: key,
1047
+ modifiers: modifiers,
1048
+ action: action
1049
+ };
1050
+ }
1051
+
1052
+ function _belongsTo(element, ancestor) {
1053
+ if (element === document) {
1054
+ return false;
1055
+ }
1056
+
1057
+ if (element === ancestor) {
1058
+ return true;
1059
+ }
1060
+
1061
+ return _belongsTo(element.parentNode, ancestor);
1062
+ }
1063
+
1064
+ function Mousetrap(targetElement) {
1065
+ var self = this;
1066
+
1067
+ targetElement = targetElement || document;
1068
+
1069
+ if (!(self instanceof Mousetrap)) {
1070
+ return new Mousetrap(targetElement);
1071
+ }
1072
+
1073
+ /**
1074
+ * element to attach key events to
1075
+ *
1076
+ * @type {Element}
1077
+ */
1078
+ self.target = targetElement;
1079
+
1080
+ /**
1081
+ * a list of all the callbacks setup via Mousetrap.bind()
1082
+ *
1083
+ * @type {Object}
1084
+ */
1085
+ self._callbacks = {};
1086
+
1087
+ /**
1088
+ * direct map of string combinations to callbacks used for trigger()
1089
+ *
1090
+ * @type {Object}
1091
+ */
1092
+ self._directMap = {};
1093
+
1094
+ /**
1095
+ * keeps track of what level each sequence is at since multiple
1096
+ * sequences can start out with the same sequence
1097
+ *
1098
+ * @type {Object}
1099
+ */
1100
+ var _sequenceLevels = {};
1101
+
1102
+ /**
1103
+ * variable to store the setTimeout call
1104
+ *
1105
+ * @type {null|number}
1106
+ */
1107
+ var _resetTimer;
1108
+
1109
+ /**
1110
+ * temporary state where we will ignore the next keyup
1111
+ *
1112
+ * @type {boolean|string}
1113
+ */
1114
+ var _ignoreNextKeyup = false;
1115
+
1116
+ /**
1117
+ * temporary state where we will ignore the next keypress
1118
+ *
1119
+ * @type {boolean}
1120
+ */
1121
+ var _ignoreNextKeypress = false;
1122
+
1123
+ /**
1124
+ * are we currently inside of a sequence?
1125
+ * type of action ("keyup" or "keydown" or "keypress") or false
1126
+ *
1127
+ * @type {boolean|string}
1128
+ */
1129
+ var _nextExpectedAction = false;
1130
+
1131
+ /**
1132
+ * resets all sequence counters except for the ones passed in
1133
+ *
1134
+ * @param {Object} doNotReset
1135
+ * @returns void
1136
+ */
1137
+ function _resetSequences(doNotReset) {
1138
+ doNotReset = doNotReset || {};
1139
+
1140
+ var activeSequences = false,
1141
+ key;
1142
+
1143
+ for (key in _sequenceLevels) {
1144
+ if (doNotReset[key]) {
1145
+ activeSequences = true;
1146
+ continue;
1147
+ }
1148
+ _sequenceLevels[key] = 0;
1149
+ }
1150
+
1151
+ if (!activeSequences) {
1152
+ _nextExpectedAction = false;
1153
+ }
1154
+ }
1155
+
1156
+ /**
1157
+ * finds all callbacks that match based on the keycode, modifiers,
1158
+ * and action
1159
+ *
1160
+ * @param {string} character
1161
+ * @param {Array} modifiers
1162
+ * @param {Event|Object} e
1163
+ * @param {string=} sequenceName - name of the sequence we are looking for
1164
+ * @param {string=} combination
1165
+ * @param {number=} level
1166
+ * @returns {Array}
1167
+ */
1168
+ function _getMatches(character, modifiers, e, sequenceName, combination, level) {
1169
+ var i;
1170
+ var callback;
1171
+ var matches = [];
1172
+ var action = e.type;
1173
+
1174
+ // if there are no events related to this keycode
1175
+ if (!self._callbacks[character]) {
1176
+ return [];
1177
+ }
1178
+
1179
+ // if a modifier key is coming up on its own we should allow it
1180
+ if (action == 'keyup' && _isModifier(character)) {
1181
+ modifiers = [character];
1182
+ }
1183
+
1184
+ // loop through all callbacks for the key that was pressed
1185
+ // and see if any of them match
1186
+ for (i = 0; i < self._callbacks[character].length; ++i) {
1187
+ callback = self._callbacks[character][i];
1188
+
1189
+ // if a sequence name is not specified, but this is a sequence at
1190
+ // the wrong level then move onto the next match
1191
+ if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
1192
+ continue;
1193
+ }
1194
+
1195
+ // if the action we are looking for doesn't match the action we got
1196
+ // then we should keep going
1197
+ if (action != callback.action) {
1198
+ continue;
1199
+ }
1200
+
1201
+ // if this is a keypress event and the meta key and control key
1202
+ // are not pressed that means that we need to only look at the
1203
+ // character, otherwise check the modifiers as well
1204
+ //
1205
+ // chrome will not fire a keypress if meta or control is down
1206
+ // safari will fire a keypress if meta or meta+shift is down
1207
+ // firefox will fire a keypress if meta or control is down
1208
+ if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
1209
+
1210
+ // when you bind a combination or sequence a second time it
1211
+ // should overwrite the first one. if a sequenceName or
1212
+ // combination is specified in this call it does just that
1213
+ //
1214
+ // @todo make deleting its own method?
1215
+ var deleteCombo = !sequenceName && callback.combo == combination;
1216
+ var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
1217
+ if (deleteCombo || deleteSequence) {
1218
+ self._callbacks[character].splice(i, 1);
1219
+ }
1220
+
1221
+ matches.push(callback);
1222
+ }
1223
+ }
1224
+
1225
+ return matches;
1226
+ }
1227
+
1228
+ /**
1229
+ * actually calls the callback function
1230
+ *
1231
+ * if your callback function returns false this will use the jquery
1232
+ * convention - prevent default and stop propogation on the event
1233
+ *
1234
+ * @param {Function} callback
1235
+ * @param {Event} e
1236
+ * @returns void
1237
+ */
1238
+ function _fireCallback(callback, e, combo, sequence) {
1239
+
1240
+ // if this event should not happen stop here
1241
+ if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
1242
+ return;
1243
+ }
1244
+
1245
+ if (callback(e, combo) === false) {
1246
+ _preventDefault(e);
1247
+ _stopPropagation(e);
1248
+ }
1249
+ }
1250
+
1251
+ /**
1252
+ * handles a character key event
1253
+ *
1254
+ * @param {string} character
1255
+ * @param {Array} modifiers
1256
+ * @param {Event} e
1257
+ * @returns void
1258
+ */
1259
+ self._handleKey = function(character, modifiers, e) {
1260
+ var callbacks = _getMatches(character, modifiers, e);
1261
+ var i;
1262
+ var doNotReset = {};
1263
+ var maxLevel = 0;
1264
+ var processedSequenceCallback = false;
1265
+
1266
+ // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
1267
+ for (i = 0; i < callbacks.length; ++i) {
1268
+ if (callbacks[i].seq) {
1269
+ maxLevel = Math.max(maxLevel, callbacks[i].level);
1270
+ }
1271
+ }
1272
+
1273
+ // loop through matching callbacks for this key event
1274
+ for (i = 0; i < callbacks.length; ++i) {
1275
+
1276
+ // fire for all sequence callbacks
1277
+ // this is because if for example you have multiple sequences
1278
+ // bound such as "g i" and "g t" they both need to fire the
1279
+ // callback for matching g cause otherwise you can only ever
1280
+ // match the first one
1281
+ if (callbacks[i].seq) {
1282
+
1283
+ // only fire callbacks for the maxLevel to prevent
1284
+ // subsequences from also firing
1285
+ //
1286
+ // for example 'a option b' should not cause 'option b' to fire
1287
+ // even though 'option b' is part of the other sequence
1288
+ //
1289
+ // any sequences that do not match here will be discarded
1290
+ // below by the _resetSequences call
1291
+ if (callbacks[i].level != maxLevel) {
1292
+ continue;
1293
+ }
1294
+
1295
+ processedSequenceCallback = true;
1296
+
1297
+ // keep a list of which sequences were matches for later
1298
+ doNotReset[callbacks[i].seq] = 1;
1299
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
1300
+ continue;
1301
+ }
1302
+
1303
+ // if there were no sequence matches but we are still here
1304
+ // that means this is a regular match so we should fire that
1305
+ if (!processedSequenceCallback) {
1306
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
1307
+ }
1308
+ }
1309
+
1310
+ // if the key you pressed matches the type of sequence without
1311
+ // being a modifier (ie "keyup" or "keypress") then we should
1312
+ // reset all sequences that were not matched by this event
1313
+ //
1314
+ // this is so, for example, if you have the sequence "h a t" and you
1315
+ // type "h e a r t" it does not match. in this case the "e" will
1316
+ // cause the sequence to reset
1317
+ //
1318
+ // modifier keys are ignored because you can have a sequence
1319
+ // that contains modifiers such as "enter ctrl+space" and in most
1320
+ // cases the modifier key will be pressed before the next key
1321
+ //
1322
+ // also if you have a sequence such as "ctrl+b a" then pressing the
1323
+ // "b" key will trigger a "keypress" and a "keydown"
1324
+ //
1325
+ // the "keydown" is expected when there is a modifier, but the
1326
+ // "keypress" ends up matching the _nextExpectedAction since it occurs
1327
+ // after and that causes the sequence to reset
1328
+ //
1329
+ // we ignore keypresses in a sequence that directly follow a keydown
1330
+ // for the same character
1331
+ var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
1332
+ if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
1333
+ _resetSequences(doNotReset);
1334
+ }
1335
+
1336
+ _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
1337
+ };
1338
+
1339
+ /**
1340
+ * handles a keydown event
1341
+ *
1342
+ * @param {Event} e
1343
+ * @returns void
1344
+ */
1345
+ function _handleKeyEvent(e) {
1346
+
1347
+ // normalize e.which for key events
1348
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
1349
+ if (typeof e.which !== 'number') {
1350
+ e.which = e.keyCode;
1351
+ }
1352
+
1353
+ var character = _characterFromEvent(e);
1354
+
1355
+ // no character found then stop
1356
+ if (!character) {
1357
+ return;
1358
+ }
1359
+
1360
+ // need to use === for the character check because the character can be 0
1361
+ if (e.type == 'keyup' && _ignoreNextKeyup === character) {
1362
+ _ignoreNextKeyup = false;
1363
+ return;
1364
+ }
1365
+
1366
+ self.handleKey(character, _eventModifiers(e), e);
1367
+ }
1368
+
1369
+ /**
1370
+ * called to set a 1 second timeout on the specified sequence
1371
+ *
1372
+ * this is so after each key press in the sequence you have 1 second
1373
+ * to press the next key before you have to start over
1374
+ *
1375
+ * @returns void
1376
+ */
1377
+ function _resetSequenceTimer() {
1378
+ clearTimeout(_resetTimer);
1379
+ _resetTimer = setTimeout(_resetSequences, 1000);
1380
+ }
1381
+
1382
+ /**
1383
+ * binds a key sequence to an event
1384
+ *
1385
+ * @param {string} combo - combo specified in bind call
1386
+ * @param {Array} keys
1387
+ * @param {Function} callback
1388
+ * @param {string=} action
1389
+ * @returns void
1390
+ */
1391
+ function _bindSequence(combo, keys, callback, action) {
1392
+
1393
+ // start off by adding a sequence level record for this combination
1394
+ // and setting the level to 0
1395
+ _sequenceLevels[combo] = 0;
1396
+
1397
+ /**
1398
+ * callback to increase the sequence level for this sequence and reset
1399
+ * all other sequences that were active
1400
+ *
1401
+ * @param {string} nextAction
1402
+ * @returns {Function}
1403
+ */
1404
+ function _increaseSequence(nextAction) {
1405
+ return function() {
1406
+ _nextExpectedAction = nextAction;
1407
+ ++_sequenceLevels[combo];
1408
+ _resetSequenceTimer();
1409
+ };
1410
+ }
1411
+
1412
+ /**
1413
+ * wraps the specified callback inside of another function in order
1414
+ * to reset all sequence counters as soon as this sequence is done
1415
+ *
1416
+ * @param {Event} e
1417
+ * @returns void
1418
+ */
1419
+ function _callbackAndReset(e) {
1420
+ _fireCallback(callback, e, combo);
1421
+
1422
+ // we should ignore the next key up if the action is key down
1423
+ // or keypress. this is so if you finish a sequence and
1424
+ // release the key the final key will not trigger a keyup
1425
+ if (action !== 'keyup') {
1426
+ _ignoreNextKeyup = _characterFromEvent(e);
1427
+ }
1428
+
1429
+ // weird race condition if a sequence ends with the key
1430
+ // another sequence begins with
1431
+ setTimeout(_resetSequences, 10);
1432
+ }
1433
+
1434
+ // loop through keys one at a time and bind the appropriate callback
1435
+ // function. for any key leading up to the final one it should
1436
+ // increase the sequence. after the final, it should reset all sequences
1437
+ //
1438
+ // if an action is specified in the original bind call then that will
1439
+ // be used throughout. otherwise we will pass the action that the
1440
+ // next key in the sequence should match. this allows a sequence
1441
+ // to mix and match keypress and keydown events depending on which
1442
+ // ones are better suited to the key provided
1443
+ for (var i = 0; i < keys.length; ++i) {
1444
+ var isFinal = i + 1 === keys.length;
1445
+ var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
1446
+ _bindSingle(keys[i], wrappedCallback, action, combo, i);
1447
+ }
1448
+ }
1449
+
1450
+ /**
1451
+ * binds a single keyboard combination
1452
+ *
1453
+ * @param {string} combination
1454
+ * @param {Function} callback
1455
+ * @param {string=} action
1456
+ * @param {string=} sequenceName - name of sequence if part of sequence
1457
+ * @param {number=} level - what part of the sequence the command is
1458
+ * @returns void
1459
+ */
1460
+ function _bindSingle(combination, callback, action, sequenceName, level) {
1461
+
1462
+ // store a direct mapped reference for use with Mousetrap.trigger
1463
+ self._directMap[combination + ':' + action] = callback;
1464
+
1465
+ // make sure multiple spaces in a row become a single space
1466
+ combination = combination.replace(/\s+/g, ' ');
1467
+
1468
+ var sequence = combination.split(' ');
1469
+ var info;
1470
+
1471
+ // if this pattern is a sequence of keys then run through this method
1472
+ // to reprocess each pattern one key at a time
1473
+ if (sequence.length > 1) {
1474
+ _bindSequence(combination, sequence, callback, action);
1475
+ return;
1476
+ }
1477
+
1478
+ info = _getKeyInfo(combination, action);
1479
+
1480
+ // make sure to initialize array if this is the first time
1481
+ // a callback is added for this key
1482
+ self._callbacks[info.key] = self._callbacks[info.key] || [];
1483
+
1484
+ // remove an existing match if there is one
1485
+ _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
1486
+
1487
+ // add this call back to the array
1488
+ // if it is a sequence put it at the beginning
1489
+ // if not put it at the end
1490
+ //
1491
+ // this is important because the way these are processed expects
1492
+ // the sequence ones to come first
1493
+ self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
1494
+ callback: callback,
1495
+ modifiers: info.modifiers,
1496
+ action: info.action,
1497
+ seq: sequenceName,
1498
+ level: level,
1499
+ combo: combination
1500
+ });
1501
+ }
1502
+
1503
+ /**
1504
+ * binds multiple combinations to the same callback
1505
+ *
1506
+ * @param {Array} combinations
1507
+ * @param {Function} callback
1508
+ * @param {string|undefined} action
1509
+ * @returns void
1510
+ */
1511
+ self._bindMultiple = function(combinations, callback, action) {
1512
+ for (var i = 0; i < combinations.length; ++i) {
1513
+ _bindSingle(combinations[i], callback, action);
1514
+ }
1515
+ };
1516
+
1517
+ // start!
1518
+ _addEvent(targetElement, 'keypress', _handleKeyEvent);
1519
+ _addEvent(targetElement, 'keydown', _handleKeyEvent);
1520
+ _addEvent(targetElement, 'keyup', _handleKeyEvent);
1521
+ }
1522
+
1523
+ /**
1524
+ * binds an event to mousetrap
1525
+ *
1526
+ * can be a single key, a combination of keys separated with +,
1527
+ * an array of keys, or a sequence of keys separated by spaces
1528
+ *
1529
+ * be sure to list the modifier keys first to make sure that the
1530
+ * correct key ends up getting bound (the last key in the pattern)
1531
+ *
1532
+ * @param {string|Array} keys
1533
+ * @param {Function} callback
1534
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
1535
+ * @returns void
1536
+ */
1537
+ Mousetrap.prototype.bind = function(keys, callback, action) {
1538
+ var self = this;
1539
+ keys = keys instanceof Array ? keys : [keys];
1540
+ self._bindMultiple.call(self, keys, callback, action);
1541
+ return self;
1542
+ };
1543
+
1544
+ /**
1545
+ * unbinds an event to mousetrap
1546
+ *
1547
+ * the unbinding sets the callback function of the specified key combo
1548
+ * to an empty function and deletes the corresponding key in the
1549
+ * _directMap dict.
1550
+ *
1551
+ * TODO: actually remove this from the _callbacks dictionary instead
1552
+ * of binding an empty function
1553
+ *
1554
+ * the keycombo+action has to be exactly the same as
1555
+ * it was defined in the bind method
1556
+ *
1557
+ * @param {string|Array} keys
1558
+ * @param {string} action
1559
+ * @returns void
1560
+ */
1561
+ Mousetrap.prototype.unbind = function(keys, action) {
1562
+ var self = this;
1563
+ return self.bind.call(self, keys, function() {}, action);
1564
+ };
1565
+
1566
+ /**
1567
+ * triggers an event that has already been bound
1568
+ *
1569
+ * @param {string} keys
1570
+ * @param {string=} action
1571
+ * @returns void
1572
+ */
1573
+ Mousetrap.prototype.trigger = function(keys, action) {
1574
+ var self = this;
1575
+ if (self._directMap[keys + ':' + action]) {
1576
+ self._directMap[keys + ':' + action]({}, keys);
1577
+ }
1578
+ return self;
1579
+ };
1580
+
1581
+ /**
1582
+ * resets the library back to its initial state. this is useful
1583
+ * if you want to clear out the current keyboard shortcuts and bind
1584
+ * new ones - for example if you switch to another page
1585
+ *
1586
+ * @returns void
1587
+ */
1588
+ Mousetrap.prototype.reset = function() {
1589
+ var self = this;
1590
+ self._callbacks = {};
1591
+ self._directMap = {};
1592
+ return self;
1593
+ };
1594
+
1595
+ /**
1596
+ * should we stop this event before firing off callbacks
1597
+ *
1598
+ * @param {Event} e
1599
+ * @param {Element} element
1600
+ * @return {boolean}
1601
+ */
1602
+ Mousetrap.prototype.stopCallback = function(e, element) {
1603
+ var self = this;
1604
+
1605
+ // if the element has the class "mousetrap" then no need to stop
1606
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
1607
+ return false;
1608
+ }
1609
+
1610
+ if (_belongsTo(element, self.target)) {
1611
+ return false;
1612
+ }
1613
+
1614
+ // stop for input, select, and textarea
1615
+ return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
1616
+ };
1617
+
1618
+ /**
1619
+ * exposes _handleKey publicly so it can be overwritten by extensions
1620
+ */
1621
+ Mousetrap.prototype.handleKey = function() {
1622
+ var self = this;
1623
+ return self._handleKey.apply(self, arguments);
1624
+ };
1625
+
1626
+ /**
1627
+ * Init the global mousetrap functions
1628
+ *
1629
+ * This method is needed to allow the global mousetrap functions to work
1630
+ * now that mousetrap is a constructor function.
1631
+ */
1632
+ Mousetrap.init = function() {
1633
+ var documentMousetrap = Mousetrap(document);
1634
+ for (var method in documentMousetrap) {
1635
+ if (method.charAt(0) !== '_') {
1636
+ Mousetrap[method] = (function(method) {
1637
+ return function() {
1638
+ return documentMousetrap[method].apply(documentMousetrap, arguments);
1639
+ };
1640
+ } (method));
1641
+ }
1642
+ }
1643
+ };
1644
+
1645
+ Mousetrap.init();
1646
+
1647
+ // expose mousetrap to the global object
1648
+ window.Mousetrap = Mousetrap;
1649
+
1650
+ // expose as a common js module
1651
+ if (typeof module !== 'undefined' && module.exports) {
1652
+ module.exports = Mousetrap;
1653
+ }
1654
+
1655
+ // expose mousetrap as an AMD module
1656
+ if (typeof define === 'function' && define.amd) {
1657
+ define(function() {
1658
+ return Mousetrap;
1659
+ });
1660
+ }
1661
+ }) (window, document);