shipit-engine 0.8.9 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/shipit_bs.js.coffee +2 -0
  3. data/app/assets/javascripts/task.js.coffee +14 -5
  4. data/app/assets/javascripts/task/search_bar.js.coffee +52 -0
  5. data/app/assets/javascripts/task/stream.js.coffee +9 -2
  6. data/app/assets/javascripts/task/tty.js.coffee +75 -28
  7. data/app/assets/stylesheets/_base/_forms.scss +5 -0
  8. data/app/assets/stylesheets/_pages/_commits.scss +1 -1
  9. data/app/assets/stylesheets/_pages/_deploy.scss +56 -24
  10. data/app/assets/stylesheets/_pages/_settings.scss +5 -0
  11. data/app/assets/stylesheets/_structure/_layout.scss +1 -0
  12. data/app/assets/stylesheets/_structure/_main.scss +4 -0
  13. data/app/assets/stylesheets/shipit.scss +2 -0
  14. data/app/assets/stylesheets/shipit_bs.scss +22 -0
  15. data/app/controllers/shipit/deploys_controller.rb +5 -1
  16. data/app/controllers/shipit/shipit_controller.rb +10 -3
  17. data/app/controllers/shipit/stacks_controller.rb +12 -3
  18. data/app/controllers/shipit/tasks_controller.rb +4 -0
  19. data/app/helpers/shipit/shipit_helper.rb +18 -0
  20. data/app/helpers/shipit/stacks_helper.rb +1 -1
  21. data/app/jobs/shipit/cache_deploy_spec_job.rb +2 -0
  22. data/app/jobs/shipit/fetch_deployed_revision_job.rb +1 -0
  23. data/app/jobs/shipit/git_mirror_update_job.rb +2 -0
  24. data/app/jobs/shipit/perform_task_job.rb +1 -0
  25. data/app/models/shipit/commit.rb +2 -2
  26. data/app/models/shipit/deploy.rb +1 -1
  27. data/app/models/shipit/deploy_spec/bundler_discovery.rb +1 -1
  28. data/app/models/shipit/duration.rb +28 -0
  29. data/app/models/shipit/stack.rb +33 -11
  30. data/app/models/shipit/task.rb +26 -3
  31. data/app/serializers/shipit/task_serializer.rb +14 -1
  32. data/app/views/bootstrap/shipit/missing_settings.html.erb +97 -0
  33. data/app/views/bootstrap/shipit/stacks/new.html.erb +44 -0
  34. data/app/views/layouts/shipit.html.erb +1 -1
  35. data/app/views/layouts/shipit_bootstrap.html.erb +44 -0
  36. data/app/views/shipit/commits/_commit.html.erb +3 -2
  37. data/app/views/shipit/deploys/_deploy.html.erb +11 -2
  38. data/app/views/shipit/deploys/show.html.erb +1 -1
  39. data/app/views/shipit/stacks/new.html.erb +12 -12
  40. data/app/views/shipit/stacks/settings.html.erb +5 -0
  41. data/app/views/shipit/stacks/show.html.erb +1 -1
  42. data/app/views/shipit/tasks/_task.html.erb +10 -2
  43. data/app/views/shipit/tasks/_task_output.html.erb +11 -1
  44. data/app/views/shipit/tasks/show.html.erb +1 -1
  45. data/config/routes.rb +1 -0
  46. data/db/migrate/20160324155046_add_started_at_and_ended_at_on_tasks.rb +25 -0
  47. data/lib/shipit.rb +13 -0
  48. data/lib/shipit/command.rb +13 -9
  49. data/lib/shipit/engine.rb +8 -0
  50. data/lib/shipit/template_renderer_extension.rb +16 -0
  51. data/lib/shipit/version.rb +1 -1
  52. data/test/controllers/deploys_controller_test.rb +11 -0
  53. data/test/controllers/stacks_controller_test.rb +5 -0
  54. data/test/controllers/tasks_controller_test.rb +6 -0
  55. data/test/dummy/config/secrets.example.yml +4 -0
  56. data/test/dummy/config/secrets.yml +2 -0
  57. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/bar.txt +2 -0
  58. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/dkfdsf +0 -0
  59. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/dskjfsd +0 -0
  60. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/dslkjfjsdf +0 -0
  61. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/plopfizz +0 -0
  62. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/sd +0 -0
  63. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/sdkfjsdf +1 -0
  64. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/sdlfjsdfdsfj +0 -0
  65. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/sdlkfjsdlkfjsdlkfjdsfsdfksdfjsldkfjsdlkfjsdf +0 -0
  66. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/shipit.yml +32 -0
  67. data/test/dummy/data/stacks/byroot/junk/production/deploys/83/toto.txt +2 -0
  68. data/test/dummy/data/stacks/byroot/junk/production/git/bar.txt +1 -0
  69. data/test/dummy/data/stacks/byroot/junk/production/git/shipit.yml +6 -1
  70. data/test/dummy/data/stacks/byroot/test/production/git/README.md +1 -0
  71. data/test/dummy/db/development.sqlite3 +0 -0
  72. data/test/dummy/db/schema.rb +3 -1
  73. data/test/dummy/db/seeds.rb +6 -0
  74. data/test/dummy/db/test.sqlite3 +0 -0
  75. data/test/dummy/db/test.sqlite3-journal +0 -0
  76. data/test/fixtures/shipit/tasks.yml +11 -0
  77. data/test/models/commits_test.rb +1 -1
  78. data/test/models/deploys_test.rb +40 -0
  79. data/test/models/duration_test.rb +13 -0
  80. data/test/models/stacks_test.rb +3 -4
  81. data/test/unit/command_test.rb +14 -0
  82. data/vendor/assets/javascripts/clusterize.js +327 -0
  83. data/vendor/assets/javascripts/mousetrap-global-bind.js +43 -0
  84. data/vendor/assets/javascripts/mousetrap.js +1021 -0
  85. data/vendor/assets/javascripts/string_includes.js +14 -0
  86. data/vendor/assets/stylesheets/clusterize.css +27 -0
  87. metadata +100 -3
  88. data/app/assets/javascripts/task/sticky_element.js.coffee +0 -16
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ module Shipit
4
+ class DurationTest < ActiveSupport::TestCase
5
+ test "#to_s is precise and readable for humans" do
6
+ assert_equal '1m01s', Duration.new(61).to_s
7
+ assert_equal '1m00s', Duration.new(60).to_s
8
+ assert_equal '59s', Duration.new(59).to_s
9
+ assert_equal '2d00h00m00s', Duration.new(2.days).to_s
10
+ assert_equal '0s', Duration.new(0).to_s
11
+ end
12
+ end
13
+ end
@@ -11,7 +11,7 @@ module Shipit
11
11
  test "repo_owner, repo_name and environment uniqueness is enforced" do
12
12
  clone = Stack.new(@stack.attributes.except('id'))
13
13
  refute clone.save
14
- assert_equal ["has already been taken"], clone.errors[:repo_name]
14
+ assert_equal ["cannot be used more than once with this environment"], clone.errors[:repo_name]
15
15
  end
16
16
 
17
17
  test "repo_owner, repo_name, and environment can only be ASCII" do
@@ -28,7 +28,7 @@ module Shipit
28
28
  environment: @stack.environment,
29
29
  )
30
30
  end
31
- assert_equal 'Validation failed: Repo name has already been taken', error.message
31
+ assert_equal 'Validation failed: Repo name cannot be used more than once with this environment', error.message
32
32
  end
33
33
 
34
34
  new_stack = Stack.create!(repo_owner: 'FOO', repo_name: 'BAR')
@@ -133,6 +133,7 @@ module Shipit
133
133
  end
134
134
 
135
135
  test "#update_deployed_revision bail out if there is an active deploy" do
136
+ @stack.deploys_and_rollbacks.last.update_columns(status: 'running')
136
137
  assert_no_difference 'Deploy.count' do
137
138
  @stack.update_deployed_revision(shipit_commits(:fifth).sha)
138
139
  end
@@ -145,8 +146,6 @@ module Shipit
145
146
  end
146
147
 
147
148
  test "#update_deployed_revision create a new completed deploy" do
148
- Deploy.active.update_all(status: 'error')
149
-
150
149
  assert_equal shipit_commits(:fourth), @stack.last_deployed_commit
151
150
  assert_difference 'Deploy.count', 1 do
152
151
  deploy = @stack.update_deployed_revision(shipit_commits(:fifth).sha)
@@ -43,5 +43,19 @@ module Shipit
43
43
  command = Command.new({'cap $LANG deploy' => {'timeout' => 10}}, default_timeout: 5, env: {}, chdir: '.')
44
44
  assert_equal 10, command.timeout
45
45
  end
46
+
47
+ test "command not found" do
48
+ error = assert_raises Command::NotFound do
49
+ Command.new('does-not-exist foo bar', env: {}, chdir: '.').run
50
+ end
51
+ assert_equal 'does-not-exist: command not found', error.message
52
+ end
53
+
54
+ test "permission denied" do
55
+ error = assert_raises Command::Denied do
56
+ Command.new('/etc/passwd foo bar', env: {}, chdir: '.').run
57
+ end
58
+ assert_equal '/etc/passwd: Permission denied', error.message
59
+ end
46
60
  end
47
61
  end
@@ -0,0 +1,327 @@
1
+ /*! Clusterize.js - v0.16.0 - 2016-03-12
2
+ * http://NeXTs.github.com/Clusterize.js/
3
+ * Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */
4
+
5
+ ;(function(name, definition) {
6
+ if (typeof module != 'undefined') module.exports = definition();
7
+ else if (typeof define == 'function' && typeof define.amd == 'object') define(definition);
8
+ else this[name] = definition();
9
+ }('Clusterize', function() {
10
+ "use strict"
11
+
12
+ // detect ie9 and lower
13
+ // https://gist.github.com/padolsey/527683#comment-786682
14
+ var ie = (function(){
15
+ for( var v = 3,
16
+ el = document.createElement('b'),
17
+ all = el.all || [];
18
+ el.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><![endif]-->',
19
+ all[0];
20
+ ){}
21
+ return v > 4 ? v : document.documentMode;
22
+ }()),
23
+ is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1;
24
+ var Clusterize = function(data) {
25
+ if( ! (this instanceof Clusterize))
26
+ return new Clusterize(data);
27
+ var self = this;
28
+
29
+ var defaults = {
30
+ item_height: 0,
31
+ block_height: 0,
32
+ rows_in_block: 50,
33
+ rows_in_cluster: 0,
34
+ cluster_height: 0,
35
+ blocks_in_cluster: 4,
36
+ tag: null,
37
+ content_tag: null,
38
+ show_no_data_row: true,
39
+ no_data_class: 'clusterize-no-data',
40
+ no_data_text: 'No data',
41
+ keep_parity: true,
42
+ callbacks: {},
43
+ scroll_top: 0
44
+ }
45
+
46
+ // public parameters
47
+ self.options = {};
48
+ var options = ['rows_in_block', 'blocks_in_cluster', 'show_no_data_row', 'no_data_class', 'no_data_text', 'keep_parity', 'tag', 'callbacks'];
49
+ for(var i = 0, option; option = options[i]; i++) {
50
+ self.options[option] = typeof data[option] != 'undefined' && data[option] != null
51
+ ? data[option]
52
+ : defaults[option];
53
+ }
54
+
55
+ var elems = ['scroll', 'content'];
56
+ for(var i = 0, elem; elem = elems[i]; i++) {
57
+ self[elem + '_elem'] = data[elem + 'Id']
58
+ ? document.getElementById(data[elem + 'Id'])
59
+ : data[elem + 'Elem'];
60
+ if( ! self[elem + '_elem'])
61
+ throw new Error("Error! Could not find " + elem + " element");
62
+ }
63
+
64
+ // tabindex forces the browser to keep focus on the scrolling list, fixes #11
65
+ if( ! self.content_elem.hasAttribute('tabindex'))
66
+ self.content_elem.setAttribute('tabindex', 0);
67
+
68
+ // private parameters
69
+ var rows = isArray(data.rows)
70
+ ? data.rows
71
+ : self.fetchMarkup(),
72
+ cache = {data: '', bottom: 0},
73
+ scroll_top = self.scroll_elem.scrollTop;
74
+
75
+ // get row height
76
+ self.exploreEnvironment(rows);
77
+
78
+ // append initial data
79
+ self.insertToDOM(rows, cache);
80
+
81
+ // restore the scroll position
82
+ self.scroll_elem.scrollTop = scroll_top;
83
+
84
+ // adding scroll handler
85
+ var last_cluster = false,
86
+ scroll_debounce = 0,
87
+ pointer_events_set = false,
88
+ scrollEv = function() {
89
+ // fixes scrolling issue on Mac #3
90
+ if (is_mac) {
91
+ if( ! pointer_events_set) self.content_elem.style.pointerEvents = 'none';
92
+ pointer_events_set = true;
93
+ clearTimeout(scroll_debounce);
94
+ scroll_debounce = setTimeout(function () {
95
+ self.content_elem.style.pointerEvents = 'auto';
96
+ pointer_events_set = false;
97
+ }, 50);
98
+ }
99
+ if (last_cluster != (last_cluster = self.getClusterNum()))
100
+ self.insertToDOM(rows, cache);
101
+ if (self.options.callbacks.scrollingProgress)
102
+ self.options.callbacks.scrollingProgress(self.getScrollProgress());
103
+ },
104
+ resize_debounce = 0,
105
+ resizeEv = function() {
106
+ clearTimeout(resize_debounce);
107
+ resize_debounce = setTimeout(self.refresh, 100);
108
+ }
109
+ on('scroll', self.scroll_elem, scrollEv);
110
+ on('resize', window, resizeEv);
111
+
112
+ // public methods
113
+ self.destroy = function(clean) {
114
+ off('scroll', self.scroll_elem, scrollEv);
115
+ off('resize', window, resizeEv);
116
+ self.html((clean ? self.generateEmptyRow() : rows).join(''));
117
+ }
118
+ self.refresh = function() {
119
+ self.getRowsHeight(rows) && self.update(rows);
120
+ }
121
+ self.update = function(new_rows) {
122
+ rows = isArray(new_rows)
123
+ ? new_rows
124
+ : [];
125
+ var scroll_top = self.scroll_elem.scrollTop;
126
+ // fixes #39
127
+ if(rows.length * self.options.item_height < scroll_top) {
128
+ self.scroll_elem.scrollTop = 0;
129
+ last_cluster = 0;
130
+ }
131
+ self.insertToDOM(rows, cache);
132
+ self.scroll_elem.scrollTop = scroll_top;
133
+ }
134
+ self.clear = function() {
135
+ self.update([]);
136
+ }
137
+ self.getRowsAmount = function() {
138
+ return rows.length;
139
+ }
140
+ self.getScrollProgress = function() {
141
+ return this.options.scroll_top / (rows.length * this.options.item_height) * 100 || 0;
142
+ }
143
+
144
+ var add = function(where, _new_rows) {
145
+ var new_rows = isArray(_new_rows)
146
+ ? _new_rows
147
+ : [];
148
+ if( ! new_rows.length) return;
149
+ rows = where == 'append'
150
+ ? rows.concat(new_rows)
151
+ : new_rows.concat(rows);
152
+ self.insertToDOM(rows, cache);
153
+ }
154
+ self.append = function(rows) {
155
+ add('append', rows);
156
+ }
157
+ self.prepend = function(rows) {
158
+ add('prepend', rows);
159
+ }
160
+ }
161
+
162
+ Clusterize.prototype = {
163
+ constructor: Clusterize,
164
+ // fetch existing markup
165
+ fetchMarkup: function() {
166
+ var rows = [], rows_nodes = this.getChildNodes(this.content_elem);
167
+ while (rows_nodes.length) {
168
+ rows.push(rows_nodes.shift().outerHTML);
169
+ }
170
+ return rows;
171
+ },
172
+ // get tag name, content tag name, tag height, calc cluster height
173
+ exploreEnvironment: function(rows) {
174
+ var opts = this.options;
175
+ opts.content_tag = this.content_elem.tagName.toLowerCase();
176
+ if( ! rows.length) return;
177
+ if(ie && ie <= 9 && ! opts.tag) opts.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase();
178
+ if(this.content_elem.children.length <= 1) this.html(rows[0] + rows[0] + rows[0]);
179
+ if( ! opts.tag) opts.tag = this.content_elem.children[0].tagName.toLowerCase();
180
+ this.getRowsHeight(rows);
181
+ },
182
+ getRowsHeight: function(rows) {
183
+ var opts = this.options,
184
+ prev_item_height = opts.item_height;
185
+ opts.cluster_height = 0
186
+ if( ! rows.length) return;
187
+ var nodes = this.content_elem.children;
188
+ opts.item_height = nodes[Math.floor(nodes.length / 2)].offsetHeight;
189
+ // consider table's border-spacing
190
+ if(opts.tag == 'tr' && getStyle('borderCollapse', this.content_elem) != 'collapse')
191
+ opts.item_height += parseInt(getStyle('borderSpacing', this.content_elem)) || 0;
192
+ opts.block_height = opts.item_height * opts.rows_in_block;
193
+ opts.rows_in_cluster = opts.blocks_in_cluster * opts.rows_in_block;
194
+ opts.cluster_height = opts.blocks_in_cluster * opts.block_height;
195
+ return prev_item_height != opts.item_height;
196
+ },
197
+ // get current cluster number
198
+ getClusterNum: function () {
199
+ this.options.scroll_top = this.scroll_elem.scrollTop;
200
+ return Math.floor(this.options.scroll_top / (this.options.cluster_height - this.options.block_height)) || 0;
201
+ },
202
+ // generate empty row if no data provided
203
+ generateEmptyRow: function() {
204
+ var opts = this.options;
205
+ if( ! opts.tag || ! opts.show_no_data_row) return [];
206
+ var empty_row = document.createElement(opts.tag),
207
+ no_data_content = document.createTextNode(opts.no_data_text), td;
208
+ empty_row.className = opts.no_data_class;
209
+ if(opts.tag == 'tr') {
210
+ td = document.createElement('td');
211
+ td.appendChild(no_data_content);
212
+ }
213
+ empty_row.appendChild(td || no_data_content);
214
+ return [empty_row.outerHTML];
215
+ },
216
+ // generate cluster for current scroll position
217
+ generate: function (rows, cluster_num) {
218
+ var opts = this.options,
219
+ rows_len = rows.length;
220
+ if (rows_len < opts.rows_in_block) {
221
+ return {
222
+ top_offset: 0,
223
+ bottom_offset: 0,
224
+ rows_above: 0,
225
+ rows: rows_len ? rows : this.generateEmptyRow()
226
+ }
227
+ }
228
+ if( ! opts.cluster_height) {
229
+ this.exploreEnvironment(rows);
230
+ }
231
+ var items_start = Math.max((opts.rows_in_cluster - opts.rows_in_block) * cluster_num, 0),
232
+ items_end = items_start + opts.rows_in_cluster,
233
+ top_offset = Math.max(items_start * opts.item_height, 0),
234
+ bottom_offset = Math.max((rows_len - items_end) * opts.item_height, 0),
235
+ this_cluster_rows = [],
236
+ rows_above = items_start;
237
+ if(top_offset < 1) {
238
+ rows_above++;
239
+ }
240
+ for (var i = items_start; i < items_end; i++) {
241
+ rows[i] && this_cluster_rows.push(rows[i]);
242
+ }
243
+ return {
244
+ top_offset: top_offset,
245
+ bottom_offset: bottom_offset,
246
+ rows_above: rows_above,
247
+ rows: this_cluster_rows
248
+ }
249
+ },
250
+ renderExtraTag: function(class_name, height) {
251
+ var tag = document.createElement(this.options.tag),
252
+ clusterize_prefix = 'clusterize-';
253
+ tag.className = [clusterize_prefix + 'extra-row', clusterize_prefix + class_name].join(' ');
254
+ height && (tag.style.height = height + 'px');
255
+ return tag.outerHTML;
256
+ },
257
+ // if necessary verify data changed and insert to DOM
258
+ insertToDOM: function(rows, cache) {
259
+ var data = this.generate(rows, this.getClusterNum()),
260
+ this_cluster_rows = data.rows.join(''),
261
+ this_cluster_content_changed = this.checkChanges('data', this_cluster_rows, cache),
262
+ only_bottom_offset_changed = this.checkChanges('bottom', data.bottom_offset, cache),
263
+ callbacks = this.options.callbacks,
264
+ layout = [];
265
+
266
+ if(this_cluster_content_changed) {
267
+ if(data.top_offset) {
268
+ this.options.keep_parity && layout.push(this.renderExtraTag('keep-parity'));
269
+ layout.push(this.renderExtraTag('top-space', data.top_offset));
270
+ }
271
+ layout.push(this_cluster_rows);
272
+ data.bottom_offset && layout.push(this.renderExtraTag('bottom-space', data.bottom_offset));
273
+ callbacks.clusterWillChange && callbacks.clusterWillChange();
274
+ this.html(layout.join(''));
275
+ this.options.content_tag == 'ol' && this.content_elem.setAttribute('start', data.rows_above);
276
+ callbacks.clusterChanged && callbacks.clusterChanged();
277
+ } else if(only_bottom_offset_changed) {
278
+ this.content_elem.lastChild.style.height = data.bottom_offset + 'px';
279
+ }
280
+ },
281
+ // unfortunately ie <= 9 does not allow to use innerHTML for table elements, so make a workaround
282
+ html: function(data) {
283
+ var content_elem = this.content_elem;
284
+ if(ie && ie <= 9 && this.options.tag == 'tr') {
285
+ var div = document.createElement('div'), last;
286
+ div.innerHTML = '<table><tbody>' + data + '</tbody></table>';
287
+ while((last = content_elem.lastChild)) {
288
+ content_elem.removeChild(last);
289
+ }
290
+ var rows_nodes = this.getChildNodes(div.firstChild.firstChild);
291
+ while (rows_nodes.length) {
292
+ content_elem.appendChild(rows_nodes.shift());
293
+ }
294
+ } else {
295
+ content_elem.innerHTML = data;
296
+ }
297
+ },
298
+ getChildNodes: function(tag) {
299
+ var child_nodes = tag.children, nodes = [];
300
+ for (var i = 0, ii = child_nodes.length; i < ii; i++) {
301
+ nodes.push(child_nodes[i]);
302
+ }
303
+ return nodes;
304
+ },
305
+ checkChanges: function(type, value, cache) {
306
+ var changed = value != cache[type];
307
+ cache[type] = value;
308
+ return changed;
309
+ }
310
+ }
311
+
312
+ // support functions
313
+ function on(evt, element, fnc) {
314
+ return element.addEventListener ? element.addEventListener(evt, fnc, false) : element.attachEvent("on" + evt, fnc);
315
+ }
316
+ function off(evt, element, fnc) {
317
+ return element.removeEventListener ? element.removeEventListener(evt, fnc, false) : element.detachEvent("on" + evt, fnc);
318
+ }
319
+ function isArray(arr) {
320
+ return Object.prototype.toString.call(arr) === '[object Array]';
321
+ }
322
+ function getStyle(prop, elem) {
323
+ return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];
324
+ }
325
+
326
+ return Clusterize;
327
+ }));
@@ -0,0 +1,43 @@
1
+ /**
2
+ * adds a bindGlobal method to Mousetrap that allows you to
3
+ * bind specific keyboard shortcuts that will still work
4
+ * inside a text input field
5
+ *
6
+ * usage:
7
+ * Mousetrap.bindGlobal('ctrl+s', _saveChanges);
8
+ */
9
+ /* global Mousetrap:true */
10
+ (function(Mousetrap) {
11
+ var _globalCallbacks = {};
12
+ var _originalStopCallback = Mousetrap.prototype.stopCallback;
13
+
14
+ Mousetrap.prototype.stopCallback = function(e, element, combo, sequence) {
15
+ var self = this;
16
+
17
+ if (self.paused) {
18
+ return true;
19
+ }
20
+
21
+ if (_globalCallbacks[combo] || _globalCallbacks[sequence]) {
22
+ return false;
23
+ }
24
+
25
+ return _originalStopCallback.call(self, e, element, combo);
26
+ };
27
+
28
+ Mousetrap.prototype.bindGlobal = function(keys, callback, action) {
29
+ var self = this;
30
+ self.bind(keys, callback, action);
31
+
32
+ if (keys instanceof Array) {
33
+ for (var i = 0; i < keys.length; i++) {
34
+ _globalCallbacks[keys[i]] = true;
35
+ }
36
+ return;
37
+ }
38
+
39
+ _globalCallbacks[keys] = true;
40
+ };
41
+
42
+ Mousetrap.init();
43
+ }) (Mousetrap);
@@ -0,0 +1,1021 @@
1
+ /*global define:false */
2
+ /**
3
+ * Copyright 2015 Craig Campbell
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ *
17
+ * Mousetrap is a simple keyboard shortcut library for Javascript with
18
+ * no external dependencies
19
+ *
20
+ * @version 1.5.3
21
+ * @url craig.is/killing/mice
22
+ */
23
+ (function(window, document, undefined) {
24
+
25
+ /**
26
+ * mapping of special keycodes to their corresponding keys
27
+ *
28
+ * everything in this dictionary cannot use keypress events
29
+ * so it has to be here to map to the correct keycodes for
30
+ * keyup/keydown events
31
+ *
32
+ * @type {Object}
33
+ */
34
+ var _MAP = {
35
+ 8: 'backspace',
36
+ 9: 'tab',
37
+ 13: 'enter',
38
+ 16: 'shift',
39
+ 17: 'ctrl',
40
+ 18: 'alt',
41
+ 20: 'capslock',
42
+ 27: 'esc',
43
+ 32: 'space',
44
+ 33: 'pageup',
45
+ 34: 'pagedown',
46
+ 35: 'end',
47
+ 36: 'home',
48
+ 37: 'left',
49
+ 38: 'up',
50
+ 39: 'right',
51
+ 40: 'down',
52
+ 45: 'ins',
53
+ 46: 'del',
54
+ 91: 'meta',
55
+ 93: 'meta',
56
+ 224: 'meta'
57
+ };
58
+
59
+ /**
60
+ * mapping for special characters so they can support
61
+ *
62
+ * this dictionary is only used incase you want to bind a
63
+ * keyup or keydown event to one of these keys
64
+ *
65
+ * @type {Object}
66
+ */
67
+ var _KEYCODE_MAP = {
68
+ 106: '*',
69
+ 107: '+',
70
+ 109: '-',
71
+ 110: '.',
72
+ 111 : '/',
73
+ 186: ';',
74
+ 187: '=',
75
+ 188: ',',
76
+ 189: '-',
77
+ 190: '.',
78
+ 191: '/',
79
+ 192: '`',
80
+ 219: '[',
81
+ 220: '\\',
82
+ 221: ']',
83
+ 222: '\''
84
+ };
85
+
86
+ /**
87
+ * this is a mapping of keys that require shift on a US keypad
88
+ * back to the non shift equivelents
89
+ *
90
+ * this is so you can use keyup events with these keys
91
+ *
92
+ * note that this will only work reliably on US keyboards
93
+ *
94
+ * @type {Object}
95
+ */
96
+ var _SHIFT_MAP = {
97
+ '~': '`',
98
+ '!': '1',
99
+ '@': '2',
100
+ '#': '3',
101
+ '$': '4',
102
+ '%': '5',
103
+ '^': '6',
104
+ '&': '7',
105
+ '*': '8',
106
+ '(': '9',
107
+ ')': '0',
108
+ '_': '-',
109
+ '+': '=',
110
+ ':': ';',
111
+ '\"': '\'',
112
+ '<': ',',
113
+ '>': '.',
114
+ '?': '/',
115
+ '|': '\\'
116
+ };
117
+
118
+ /**
119
+ * this is a list of special strings you can use to map
120
+ * to modifier keys when you specify your keyboard shortcuts
121
+ *
122
+ * @type {Object}
123
+ */
124
+ var _SPECIAL_ALIASES = {
125
+ 'option': 'alt',
126
+ 'command': 'meta',
127
+ 'return': 'enter',
128
+ 'escape': 'esc',
129
+ 'plus': '+',
130
+ 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
131
+ };
132
+
133
+ /**
134
+ * variable to store the flipped version of _MAP from above
135
+ * needed to check if we should use keypress or not when no action
136
+ * is specified
137
+ *
138
+ * @type {Object|undefined}
139
+ */
140
+ var _REVERSE_MAP;
141
+
142
+ /**
143
+ * loop through the f keys, f1 to f19 and add them to the map
144
+ * programatically
145
+ */
146
+ for (var i = 1; i < 20; ++i) {
147
+ _MAP[111 + i] = 'f' + i;
148
+ }
149
+
150
+ /**
151
+ * loop through to map numbers on the numeric keypad
152
+ */
153
+ for (i = 0; i <= 9; ++i) {
154
+ _MAP[i + 96] = i;
155
+ }
156
+
157
+ /**
158
+ * cross browser add event method
159
+ *
160
+ * @param {Element|HTMLDocument} object
161
+ * @param {string} type
162
+ * @param {Function} callback
163
+ * @returns void
164
+ */
165
+ function _addEvent(object, type, callback) {
166
+ if (object.addEventListener) {
167
+ object.addEventListener(type, callback, false);
168
+ return;
169
+ }
170
+
171
+ object.attachEvent('on' + type, callback);
172
+ }
173
+
174
+ /**
175
+ * takes the event and returns the key character
176
+ *
177
+ * @param {Event} e
178
+ * @return {string}
179
+ */
180
+ function _characterFromEvent(e) {
181
+
182
+ // for keypress events we should return the character as is
183
+ if (e.type == 'keypress') {
184
+ var character = String.fromCharCode(e.which);
185
+
186
+ // if the shift key is not pressed then it is safe to assume
187
+ // that we want the character to be lowercase. this means if
188
+ // you accidentally have caps lock on then your key bindings
189
+ // will continue to work
190
+ //
191
+ // the only side effect that might not be desired is if you
192
+ // bind something like 'A' cause you want to trigger an
193
+ // event when capital A is pressed caps lock will no longer
194
+ // trigger the event. shift+a will though.
195
+ if (!e.shiftKey) {
196
+ character = character.toLowerCase();
197
+ }
198
+
199
+ return character;
200
+ }
201
+
202
+ // for non keypress events the special maps are needed
203
+ if (_MAP[e.which]) {
204
+ return _MAP[e.which];
205
+ }
206
+
207
+ if (_KEYCODE_MAP[e.which]) {
208
+ return _KEYCODE_MAP[e.which];
209
+ }
210
+
211
+ // if it is not in the special map
212
+
213
+ // with keydown and keyup events the character seems to always
214
+ // come in as an uppercase character whether you are pressing shift
215
+ // or not. we should make sure it is always lowercase for comparisons
216
+ return String.fromCharCode(e.which).toLowerCase();
217
+ }
218
+
219
+ /**
220
+ * checks if two arrays are equal
221
+ *
222
+ * @param {Array} modifiers1
223
+ * @param {Array} modifiers2
224
+ * @returns {boolean}
225
+ */
226
+ function _modifiersMatch(modifiers1, modifiers2) {
227
+ return modifiers1.sort().join(',') === modifiers2.sort().join(',');
228
+ }
229
+
230
+ /**
231
+ * takes a key event and figures out what the modifiers are
232
+ *
233
+ * @param {Event} e
234
+ * @returns {Array}
235
+ */
236
+ function _eventModifiers(e) {
237
+ var modifiers = [];
238
+
239
+ if (e.shiftKey) {
240
+ modifiers.push('shift');
241
+ }
242
+
243
+ if (e.altKey) {
244
+ modifiers.push('alt');
245
+ }
246
+
247
+ if (e.ctrlKey) {
248
+ modifiers.push('ctrl');
249
+ }
250
+
251
+ if (e.metaKey) {
252
+ modifiers.push('meta');
253
+ }
254
+
255
+ return modifiers;
256
+ }
257
+
258
+ /**
259
+ * prevents default for this event
260
+ *
261
+ * @param {Event} e
262
+ * @returns void
263
+ */
264
+ function _preventDefault(e) {
265
+ if (e.preventDefault) {
266
+ e.preventDefault();
267
+ return;
268
+ }
269
+
270
+ e.returnValue = false;
271
+ }
272
+
273
+ /**
274
+ * stops propogation for this event
275
+ *
276
+ * @param {Event} e
277
+ * @returns void
278
+ */
279
+ function _stopPropagation(e) {
280
+ if (e.stopPropagation) {
281
+ e.stopPropagation();
282
+ return;
283
+ }
284
+
285
+ e.cancelBubble = true;
286
+ }
287
+
288
+ /**
289
+ * determines if the keycode specified is a modifier key or not
290
+ *
291
+ * @param {string} key
292
+ * @returns {boolean}
293
+ */
294
+ function _isModifier(key) {
295
+ return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
296
+ }
297
+
298
+ /**
299
+ * reverses the map lookup so that we can look for specific keys
300
+ * to see what can and can't use keypress
301
+ *
302
+ * @return {Object}
303
+ */
304
+ function _getReverseMap() {
305
+ if (!_REVERSE_MAP) {
306
+ _REVERSE_MAP = {};
307
+ for (var key in _MAP) {
308
+
309
+ // pull out the numeric keypad from here cause keypress should
310
+ // be able to detect the keys from the character
311
+ if (key > 95 && key < 112) {
312
+ continue;
313
+ }
314
+
315
+ if (_MAP.hasOwnProperty(key)) {
316
+ _REVERSE_MAP[_MAP[key]] = key;
317
+ }
318
+ }
319
+ }
320
+ return _REVERSE_MAP;
321
+ }
322
+
323
+ /**
324
+ * picks the best action based on the key combination
325
+ *
326
+ * @param {string} key - character for key
327
+ * @param {Array} modifiers
328
+ * @param {string=} action passed in
329
+ */
330
+ function _pickBestAction(key, modifiers, action) {
331
+
332
+ // if no action was picked in we should try to pick the one
333
+ // that we think would work best for this key
334
+ if (!action) {
335
+ action = _getReverseMap()[key] ? 'keydown' : 'keypress';
336
+ }
337
+
338
+ // modifier keys don't work as expected with keypress,
339
+ // switch to keydown
340
+ if (action == 'keypress' && modifiers.length) {
341
+ action = 'keydown';
342
+ }
343
+
344
+ return action;
345
+ }
346
+
347
+ /**
348
+ * Converts from a string key combination to an array
349
+ *
350
+ * @param {string} combination like "command+shift+l"
351
+ * @return {Array}
352
+ */
353
+ function _keysFromString(combination) {
354
+ if (combination === '+') {
355
+ return ['+'];
356
+ }
357
+
358
+ combination = combination.replace(/\+{2}/g, '+plus');
359
+ return combination.split('+');
360
+ }
361
+
362
+ /**
363
+ * Gets info for a specific key combination
364
+ *
365
+ * @param {string} combination key combination ("command+s" or "a" or "*")
366
+ * @param {string=} action
367
+ * @returns {Object}
368
+ */
369
+ function _getKeyInfo(combination, action) {
370
+ var keys;
371
+ var key;
372
+ var i;
373
+ var modifiers = [];
374
+
375
+ // take the keys from this pattern and figure out what the actual
376
+ // pattern is all about
377
+ keys = _keysFromString(combination);
378
+
379
+ for (i = 0; i < keys.length; ++i) {
380
+ key = keys[i];
381
+
382
+ // normalize key names
383
+ if (_SPECIAL_ALIASES[key]) {
384
+ key = _SPECIAL_ALIASES[key];
385
+ }
386
+
387
+ // if this is not a keypress event then we should
388
+ // be smart about using shift keys
389
+ // this will only work for US keyboards however
390
+ if (action && action != 'keypress' && _SHIFT_MAP[key]) {
391
+ key = _SHIFT_MAP[key];
392
+ modifiers.push('shift');
393
+ }
394
+
395
+ // if this key is a modifier then add it to the list of modifiers
396
+ if (_isModifier(key)) {
397
+ modifiers.push(key);
398
+ }
399
+ }
400
+
401
+ // depending on what the key combination is
402
+ // we will try to pick the best event for it
403
+ action = _pickBestAction(key, modifiers, action);
404
+
405
+ return {
406
+ key: key,
407
+ modifiers: modifiers,
408
+ action: action
409
+ };
410
+ }
411
+
412
+ function _belongsTo(element, ancestor) {
413
+ if (element === null || element === document) {
414
+ return false;
415
+ }
416
+
417
+ if (element === ancestor) {
418
+ return true;
419
+ }
420
+
421
+ return _belongsTo(element.parentNode, ancestor);
422
+ }
423
+
424
+ function Mousetrap(targetElement) {
425
+ var self = this;
426
+
427
+ targetElement = targetElement || document;
428
+
429
+ if (!(self instanceof Mousetrap)) {
430
+ return new Mousetrap(targetElement);
431
+ }
432
+
433
+ /**
434
+ * element to attach key events to
435
+ *
436
+ * @type {Element}
437
+ */
438
+ self.target = targetElement;
439
+
440
+ /**
441
+ * a list of all the callbacks setup via Mousetrap.bind()
442
+ *
443
+ * @type {Object}
444
+ */
445
+ self._callbacks = {};
446
+
447
+ /**
448
+ * direct map of string combinations to callbacks used for trigger()
449
+ *
450
+ * @type {Object}
451
+ */
452
+ self._directMap = {};
453
+
454
+ /**
455
+ * keeps track of what level each sequence is at since multiple
456
+ * sequences can start out with the same sequence
457
+ *
458
+ * @type {Object}
459
+ */
460
+ var _sequenceLevels = {};
461
+
462
+ /**
463
+ * variable to store the setTimeout call
464
+ *
465
+ * @type {null|number}
466
+ */
467
+ var _resetTimer;
468
+
469
+ /**
470
+ * temporary state where we will ignore the next keyup
471
+ *
472
+ * @type {boolean|string}
473
+ */
474
+ var _ignoreNextKeyup = false;
475
+
476
+ /**
477
+ * temporary state where we will ignore the next keypress
478
+ *
479
+ * @type {boolean}
480
+ */
481
+ var _ignoreNextKeypress = false;
482
+
483
+ /**
484
+ * are we currently inside of a sequence?
485
+ * type of action ("keyup" or "keydown" or "keypress") or false
486
+ *
487
+ * @type {boolean|string}
488
+ */
489
+ var _nextExpectedAction = false;
490
+
491
+ /**
492
+ * resets all sequence counters except for the ones passed in
493
+ *
494
+ * @param {Object} doNotReset
495
+ * @returns void
496
+ */
497
+ function _resetSequences(doNotReset) {
498
+ doNotReset = doNotReset || {};
499
+
500
+ var activeSequences = false,
501
+ key;
502
+
503
+ for (key in _sequenceLevels) {
504
+ if (doNotReset[key]) {
505
+ activeSequences = true;
506
+ continue;
507
+ }
508
+ _sequenceLevels[key] = 0;
509
+ }
510
+
511
+ if (!activeSequences) {
512
+ _nextExpectedAction = false;
513
+ }
514
+ }
515
+
516
+ /**
517
+ * finds all callbacks that match based on the keycode, modifiers,
518
+ * and action
519
+ *
520
+ * @param {string} character
521
+ * @param {Array} modifiers
522
+ * @param {Event|Object} e
523
+ * @param {string=} sequenceName - name of the sequence we are looking for
524
+ * @param {string=} combination
525
+ * @param {number=} level
526
+ * @returns {Array}
527
+ */
528
+ function _getMatches(character, modifiers, e, sequenceName, combination, level) {
529
+ var i;
530
+ var callback;
531
+ var matches = [];
532
+ var action = e.type;
533
+
534
+ // if there are no events related to this keycode
535
+ if (!self._callbacks[character]) {
536
+ return [];
537
+ }
538
+
539
+ // if a modifier key is coming up on its own we should allow it
540
+ if (action == 'keyup' && _isModifier(character)) {
541
+ modifiers = [character];
542
+ }
543
+
544
+ // loop through all callbacks for the key that was pressed
545
+ // and see if any of them match
546
+ for (i = 0; i < self._callbacks[character].length; ++i) {
547
+ callback = self._callbacks[character][i];
548
+
549
+ // if a sequence name is not specified, but this is a sequence at
550
+ // the wrong level then move onto the next match
551
+ if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
552
+ continue;
553
+ }
554
+
555
+ // if the action we are looking for doesn't match the action we got
556
+ // then we should keep going
557
+ if (action != callback.action) {
558
+ continue;
559
+ }
560
+
561
+ // if this is a keypress event and the meta key and control key
562
+ // are not pressed that means that we need to only look at the
563
+ // character, otherwise check the modifiers as well
564
+ //
565
+ // chrome will not fire a keypress if meta or control is down
566
+ // safari will fire a keypress if meta or meta+shift is down
567
+ // firefox will fire a keypress if meta or control is down
568
+ if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
569
+
570
+ // when you bind a combination or sequence a second time it
571
+ // should overwrite the first one. if a sequenceName or
572
+ // combination is specified in this call it does just that
573
+ //
574
+ // @todo make deleting its own method?
575
+ var deleteCombo = !sequenceName && callback.combo == combination;
576
+ var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
577
+ if (deleteCombo || deleteSequence) {
578
+ self._callbacks[character].splice(i, 1);
579
+ }
580
+
581
+ matches.push(callback);
582
+ }
583
+ }
584
+
585
+ return matches;
586
+ }
587
+
588
+ /**
589
+ * actually calls the callback function
590
+ *
591
+ * if your callback function returns false this will use the jquery
592
+ * convention - prevent default and stop propogation on the event
593
+ *
594
+ * @param {Function} callback
595
+ * @param {Event} e
596
+ * @returns void
597
+ */
598
+ function _fireCallback(callback, e, combo, sequence) {
599
+
600
+ // if this event should not happen stop here
601
+ if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
602
+ return;
603
+ }
604
+
605
+ if (callback(e, combo) === false) {
606
+ _preventDefault(e);
607
+ _stopPropagation(e);
608
+ }
609
+ }
610
+
611
+ /**
612
+ * handles a character key event
613
+ *
614
+ * @param {string} character
615
+ * @param {Array} modifiers
616
+ * @param {Event} e
617
+ * @returns void
618
+ */
619
+ self._handleKey = function(character, modifiers, e) {
620
+ var callbacks = _getMatches(character, modifiers, e);
621
+ var i;
622
+ var doNotReset = {};
623
+ var maxLevel = 0;
624
+ var processedSequenceCallback = false;
625
+
626
+ // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
627
+ for (i = 0; i < callbacks.length; ++i) {
628
+ if (callbacks[i].seq) {
629
+ maxLevel = Math.max(maxLevel, callbacks[i].level);
630
+ }
631
+ }
632
+
633
+ // loop through matching callbacks for this key event
634
+ for (i = 0; i < callbacks.length; ++i) {
635
+
636
+ // fire for all sequence callbacks
637
+ // this is because if for example you have multiple sequences
638
+ // bound such as "g i" and "g t" they both need to fire the
639
+ // callback for matching g cause otherwise you can only ever
640
+ // match the first one
641
+ if (callbacks[i].seq) {
642
+
643
+ // only fire callbacks for the maxLevel to prevent
644
+ // subsequences from also firing
645
+ //
646
+ // for example 'a option b' should not cause 'option b' to fire
647
+ // even though 'option b' is part of the other sequence
648
+ //
649
+ // any sequences that do not match here will be discarded
650
+ // below by the _resetSequences call
651
+ if (callbacks[i].level != maxLevel) {
652
+ continue;
653
+ }
654
+
655
+ processedSequenceCallback = true;
656
+
657
+ // keep a list of which sequences were matches for later
658
+ doNotReset[callbacks[i].seq] = 1;
659
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
660
+ continue;
661
+ }
662
+
663
+ // if there were no sequence matches but we are still here
664
+ // that means this is a regular match so we should fire that
665
+ if (!processedSequenceCallback) {
666
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
667
+ }
668
+ }
669
+
670
+ // if the key you pressed matches the type of sequence without
671
+ // being a modifier (ie "keyup" or "keypress") then we should
672
+ // reset all sequences that were not matched by this event
673
+ //
674
+ // this is so, for example, if you have the sequence "h a t" and you
675
+ // type "h e a r t" it does not match. in this case the "e" will
676
+ // cause the sequence to reset
677
+ //
678
+ // modifier keys are ignored because you can have a sequence
679
+ // that contains modifiers such as "enter ctrl+space" and in most
680
+ // cases the modifier key will be pressed before the next key
681
+ //
682
+ // also if you have a sequence such as "ctrl+b a" then pressing the
683
+ // "b" key will trigger a "keypress" and a "keydown"
684
+ //
685
+ // the "keydown" is expected when there is a modifier, but the
686
+ // "keypress" ends up matching the _nextExpectedAction since it occurs
687
+ // after and that causes the sequence to reset
688
+ //
689
+ // we ignore keypresses in a sequence that directly follow a keydown
690
+ // for the same character
691
+ var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
692
+ if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
693
+ _resetSequences(doNotReset);
694
+ }
695
+
696
+ _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
697
+ };
698
+
699
+ /**
700
+ * handles a keydown event
701
+ *
702
+ * @param {Event} e
703
+ * @returns void
704
+ */
705
+ function _handleKeyEvent(e) {
706
+
707
+ // normalize e.which for key events
708
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
709
+ if (typeof e.which !== 'number') {
710
+ e.which = e.keyCode;
711
+ }
712
+
713
+ var character = _characterFromEvent(e);
714
+
715
+ // no character found then stop
716
+ if (!character) {
717
+ return;
718
+ }
719
+
720
+ // need to use === for the character check because the character can be 0
721
+ if (e.type == 'keyup' && _ignoreNextKeyup === character) {
722
+ _ignoreNextKeyup = false;
723
+ return;
724
+ }
725
+
726
+ self.handleKey(character, _eventModifiers(e), e);
727
+ }
728
+
729
+ /**
730
+ * called to set a 1 second timeout on the specified sequence
731
+ *
732
+ * this is so after each key press in the sequence you have 1 second
733
+ * to press the next key before you have to start over
734
+ *
735
+ * @returns void
736
+ */
737
+ function _resetSequenceTimer() {
738
+ clearTimeout(_resetTimer);
739
+ _resetTimer = setTimeout(_resetSequences, 1000);
740
+ }
741
+
742
+ /**
743
+ * binds a key sequence to an event
744
+ *
745
+ * @param {string} combo - combo specified in bind call
746
+ * @param {Array} keys
747
+ * @param {Function} callback
748
+ * @param {string=} action
749
+ * @returns void
750
+ */
751
+ function _bindSequence(combo, keys, callback, action) {
752
+
753
+ // start off by adding a sequence level record for this combination
754
+ // and setting the level to 0
755
+ _sequenceLevels[combo] = 0;
756
+
757
+ /**
758
+ * callback to increase the sequence level for this sequence and reset
759
+ * all other sequences that were active
760
+ *
761
+ * @param {string} nextAction
762
+ * @returns {Function}
763
+ */
764
+ function _increaseSequence(nextAction) {
765
+ return function() {
766
+ _nextExpectedAction = nextAction;
767
+ ++_sequenceLevels[combo];
768
+ _resetSequenceTimer();
769
+ };
770
+ }
771
+
772
+ /**
773
+ * wraps the specified callback inside of another function in order
774
+ * to reset all sequence counters as soon as this sequence is done
775
+ *
776
+ * @param {Event} e
777
+ * @returns void
778
+ */
779
+ function _callbackAndReset(e) {
780
+ _fireCallback(callback, e, combo);
781
+
782
+ // we should ignore the next key up if the action is key down
783
+ // or keypress. this is so if you finish a sequence and
784
+ // release the key the final key will not trigger a keyup
785
+ if (action !== 'keyup') {
786
+ _ignoreNextKeyup = _characterFromEvent(e);
787
+ }
788
+
789
+ // weird race condition if a sequence ends with the key
790
+ // another sequence begins with
791
+ setTimeout(_resetSequences, 10);
792
+ }
793
+
794
+ // loop through keys one at a time and bind the appropriate callback
795
+ // function. for any key leading up to the final one it should
796
+ // increase the sequence. after the final, it should reset all sequences
797
+ //
798
+ // if an action is specified in the original bind call then that will
799
+ // be used throughout. otherwise we will pass the action that the
800
+ // next key in the sequence should match. this allows a sequence
801
+ // to mix and match keypress and keydown events depending on which
802
+ // ones are better suited to the key provided
803
+ for (var i = 0; i < keys.length; ++i) {
804
+ var isFinal = i + 1 === keys.length;
805
+ var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
806
+ _bindSingle(keys[i], wrappedCallback, action, combo, i);
807
+ }
808
+ }
809
+
810
+ /**
811
+ * binds a single keyboard combination
812
+ *
813
+ * @param {string} combination
814
+ * @param {Function} callback
815
+ * @param {string=} action
816
+ * @param {string=} sequenceName - name of sequence if part of sequence
817
+ * @param {number=} level - what part of the sequence the command is
818
+ * @returns void
819
+ */
820
+ function _bindSingle(combination, callback, action, sequenceName, level) {
821
+
822
+ // store a direct mapped reference for use with Mousetrap.trigger
823
+ self._directMap[combination + ':' + action] = callback;
824
+
825
+ // make sure multiple spaces in a row become a single space
826
+ combination = combination.replace(/\s+/g, ' ');
827
+
828
+ var sequence = combination.split(' ');
829
+ var info;
830
+
831
+ // if this pattern is a sequence of keys then run through this method
832
+ // to reprocess each pattern one key at a time
833
+ if (sequence.length > 1) {
834
+ _bindSequence(combination, sequence, callback, action);
835
+ return;
836
+ }
837
+
838
+ info = _getKeyInfo(combination, action);
839
+
840
+ // make sure to initialize array if this is the first time
841
+ // a callback is added for this key
842
+ self._callbacks[info.key] = self._callbacks[info.key] || [];
843
+
844
+ // remove an existing match if there is one
845
+ _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
846
+
847
+ // add this call back to the array
848
+ // if it is a sequence put it at the beginning
849
+ // if not put it at the end
850
+ //
851
+ // this is important because the way these are processed expects
852
+ // the sequence ones to come first
853
+ self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
854
+ callback: callback,
855
+ modifiers: info.modifiers,
856
+ action: info.action,
857
+ seq: sequenceName,
858
+ level: level,
859
+ combo: combination
860
+ });
861
+ }
862
+
863
+ /**
864
+ * binds multiple combinations to the same callback
865
+ *
866
+ * @param {Array} combinations
867
+ * @param {Function} callback
868
+ * @param {string|undefined} action
869
+ * @returns void
870
+ */
871
+ self._bindMultiple = function(combinations, callback, action) {
872
+ for (var i = 0; i < combinations.length; ++i) {
873
+ _bindSingle(combinations[i], callback, action);
874
+ }
875
+ };
876
+
877
+ // start!
878
+ _addEvent(targetElement, 'keypress', _handleKeyEvent);
879
+ _addEvent(targetElement, 'keydown', _handleKeyEvent);
880
+ _addEvent(targetElement, 'keyup', _handleKeyEvent);
881
+ }
882
+
883
+ /**
884
+ * binds an event to mousetrap
885
+ *
886
+ * can be a single key, a combination of keys separated with +,
887
+ * an array of keys, or a sequence of keys separated by spaces
888
+ *
889
+ * be sure to list the modifier keys first to make sure that the
890
+ * correct key ends up getting bound (the last key in the pattern)
891
+ *
892
+ * @param {string|Array} keys
893
+ * @param {Function} callback
894
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
895
+ * @returns void
896
+ */
897
+ Mousetrap.prototype.bind = function(keys, callback, action) {
898
+ var self = this;
899
+ keys = keys instanceof Array ? keys : [keys];
900
+ self._bindMultiple.call(self, keys, callback, action);
901
+ return self;
902
+ };
903
+
904
+ /**
905
+ * unbinds an event to mousetrap
906
+ *
907
+ * the unbinding sets the callback function of the specified key combo
908
+ * to an empty function and deletes the corresponding key in the
909
+ * _directMap dict.
910
+ *
911
+ * TODO: actually remove this from the _callbacks dictionary instead
912
+ * of binding an empty function
913
+ *
914
+ * the keycombo+action has to be exactly the same as
915
+ * it was defined in the bind method
916
+ *
917
+ * @param {string|Array} keys
918
+ * @param {string} action
919
+ * @returns void
920
+ */
921
+ Mousetrap.prototype.unbind = function(keys, action) {
922
+ var self = this;
923
+ return self.bind.call(self, keys, function() {}, action);
924
+ };
925
+
926
+ /**
927
+ * triggers an event that has already been bound
928
+ *
929
+ * @param {string} keys
930
+ * @param {string=} action
931
+ * @returns void
932
+ */
933
+ Mousetrap.prototype.trigger = function(keys, action) {
934
+ var self = this;
935
+ if (self._directMap[keys + ':' + action]) {
936
+ self._directMap[keys + ':' + action]({}, keys);
937
+ }
938
+ return self;
939
+ };
940
+
941
+ /**
942
+ * resets the library back to its initial state. this is useful
943
+ * if you want to clear out the current keyboard shortcuts and bind
944
+ * new ones - for example if you switch to another page
945
+ *
946
+ * @returns void
947
+ */
948
+ Mousetrap.prototype.reset = function() {
949
+ var self = this;
950
+ self._callbacks = {};
951
+ self._directMap = {};
952
+ return self;
953
+ };
954
+
955
+ /**
956
+ * should we stop this event before firing off callbacks
957
+ *
958
+ * @param {Event} e
959
+ * @param {Element} element
960
+ * @return {boolean}
961
+ */
962
+ Mousetrap.prototype.stopCallback = function(e, element) {
963
+ var self = this;
964
+
965
+ // if the element has the class "mousetrap" then no need to stop
966
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
967
+ return false;
968
+ }
969
+
970
+ if (_belongsTo(element, self.target)) {
971
+ return false;
972
+ }
973
+
974
+ // stop for input, select, and textarea
975
+ return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
976
+ };
977
+
978
+ /**
979
+ * exposes _handleKey publicly so it can be overwritten by extensions
980
+ */
981
+ Mousetrap.prototype.handleKey = function() {
982
+ var self = this;
983
+ return self._handleKey.apply(self, arguments);
984
+ };
985
+
986
+ /**
987
+ * Init the global mousetrap functions
988
+ *
989
+ * This method is needed to allow the global mousetrap functions to work
990
+ * now that mousetrap is a constructor function.
991
+ */
992
+ Mousetrap.init = function() {
993
+ var documentMousetrap = Mousetrap(document);
994
+ for (var method in documentMousetrap) {
995
+ if (method.charAt(0) !== '_') {
996
+ Mousetrap[method] = (function(method) {
997
+ return function() {
998
+ return documentMousetrap[method].apply(documentMousetrap, arguments);
999
+ };
1000
+ } (method));
1001
+ }
1002
+ }
1003
+ };
1004
+
1005
+ Mousetrap.init();
1006
+
1007
+ // expose mousetrap to the global object
1008
+ window.Mousetrap = Mousetrap;
1009
+
1010
+ // expose as a common js module
1011
+ if (typeof module !== 'undefined' && module.exports) {
1012
+ module.exports = Mousetrap;
1013
+ }
1014
+
1015
+ // expose mousetrap as an AMD module
1016
+ if (typeof define === 'function' && define.amd) {
1017
+ define(function() {
1018
+ return Mousetrap;
1019
+ });
1020
+ }
1021
+ }) (window, document);