lit 0.2.6 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +5 -13
  2. data/README.md +93 -12
  3. data/Rakefile +1 -0
  4. data/app/assets/javascripts/lit/application.js +1 -2
  5. data/app/assets/javascripts/lit/{bootstrap.js.coffee → backend/bootstrap.js.coffee} +0 -0
  6. data/app/assets/javascripts/lit/{dashboard.js → backend/dashboard.js} +0 -0
  7. data/app/assets/javascripts/lit/{jquery-te-1.4.0.min.js → backend/jquery-te-1.4.0.min.js} +0 -0
  8. data/app/assets/javascripts/lit/{localizations.js.coffee → backend/localizations.js.coffee} +2 -0
  9. data/app/assets/javascripts/lit/backend/sources.js.coffee +24 -0
  10. data/app/assets/javascripts/lit/lit_frontend.js +103 -0
  11. data/app/assets/javascripts/lit/mousetrap.js +1044 -0
  12. data/app/assets/stylesheets/lit/application.css +7 -1
  13. data/app/assets/stylesheets/lit/{jquery-te-1.4.0.css.scss → backend/jquery-te-1.4.0.css.scss} +0 -0
  14. data/app/assets/stylesheets/lit/lit_frontend.css +86 -0
  15. data/app/controllers/lit/api/v1/base_controller.rb +1 -1
  16. data/app/controllers/lit/application_controller.rb +7 -3
  17. data/app/controllers/lit/concerns/request_info_store.rb +16 -0
  18. data/app/controllers/lit/concerns/request_keys_store.rb +16 -0
  19. data/app/controllers/lit/incomming_localizations_controller.rb +2 -2
  20. data/app/controllers/lit/locales_controller.rb +1 -1
  21. data/app/controllers/lit/localization_keys_controller.rb +22 -9
  22. data/app/controllers/lit/localizations_controller.rb +19 -6
  23. data/app/controllers/lit/sources_controller.rb +11 -1
  24. data/app/helpers/lit/frontend_helper.rb +71 -0
  25. data/app/helpers/lit/localization_keys_helper.rb +5 -0
  26. data/app/helpers/lit/sources_helper.rb +3 -0
  27. data/app/jobs/lit/synchronize_source_job.rb +11 -0
  28. data/app/models/lit/localization.rb +17 -10
  29. data/app/models/lit/localization_key.rb +6 -2
  30. data/app/models/lit/source.rb +4 -3
  31. data/app/views/layouts/lit/application.html.erb +6 -6
  32. data/app/views/lit/incomming_localizations/index.html.erb +22 -17
  33. data/app/views/lit/localization_keys/_localization_row.html.erb +1 -1
  34. data/app/views/lit/localization_keys/index.html.erb +27 -8
  35. data/app/views/lit/localizations/_form.html.erb +5 -5
  36. data/app/views/lit/localizations/_previous_versions_rows.html.erb +1 -1
  37. data/app/views/lit/localizations/edit.js.erb +2 -2
  38. data/app/views/lit/localizations/previous_versions.js.erb +1 -1
  39. data/app/views/lit/localizations/update.js.erb +2 -2
  40. data/config/routes.rb +12 -12
  41. data/db/migrate/20180101010101_lit_create_lit_locales.rb +16 -0
  42. data/db/migrate/20180101010102_lit_create_lit_localization_keys.rb +18 -0
  43. data/db/migrate/20180101010103_lit_create_lit_localizations.rb +24 -0
  44. data/db/migrate/20180101010104_lit_add_is_completed_and_is_starred_to_localization_keys.rb +17 -0
  45. data/db/migrate/20180101010105_lit_add_is_hidden_to_locales.rb +12 -0
  46. data/db/migrate/20180101010106_lit_create_lit_localization_versions.rb +20 -0
  47. data/db/migrate/20180101010107_lit_create_lit_sources.rb +19 -0
  48. data/db/migrate/20180101010108_lit_create_lit_incomming_localizations.rb +34 -0
  49. data/db/migrate/20180101010109_lit_add_sync_complete_to_lit_sources.rb +12 -0
  50. data/lib/generators/lit/install/templates/initializer.rb +17 -6
  51. data/lib/generators/lit/install_generator.rb +0 -7
  52. data/lib/lit.rb +15 -8
  53. data/lib/lit/adapters/hash_storage.rb +4 -0
  54. data/lib/lit/adapters/redis_storage.rb +9 -3
  55. data/lib/lit/cache.rb +126 -70
  56. data/lib/lit/engine.rb +20 -1
  57. data/lib/lit/i18n_backend.rb +66 -26
  58. data/lib/lit/version.rb +1 -1
  59. data/lib/tasks/lit_tasks.rake +21 -3
  60. metadata +67 -43
  61. data/app/assets/stylesheets/lit/dashboard.css +0 -4
  62. data/config/database.yml +0 -8
  63. data/db/migrate/20121103144612_create_lit_locales.rb +0 -9
  64. data/db/migrate/20121103174049_create_lit_localization_keys.rb +0 -10
  65. data/db/migrate/20121103182106_create_lit_localizations.rb +0 -15
  66. data/db/migrate/20121225112100_add_is_completed_and_is_starred_to_localization_keys.rb +0 -6
  67. data/db/migrate/20130511111904_add_is_hidden_to_locales.rb +0 -5
  68. data/db/migrate/20130921071304_create_lit_localization_versions.rb +0 -11
  69. data/db/migrate/20130923162141_create_lit_sources.rb +0 -12
  70. data/db/migrate/20130924151910_create_lit_incomming_localizations.rb +0 -21
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- NTgyNTU3OTU2YmIwNDY3OTBjZTNkMzcyMGE4MWNkYjQyYWE3MjMyZg==
5
- data.tar.gz: !binary |-
6
- YzY5NzAwMmU5MTM3OGIyMzY4ZThhNzBjM2Q3NzYyMjNjMWU2YzhmNw==
2
+ SHA1:
3
+ metadata.gz: 5032b96bd3391669294423dd97e9f313db39035e
4
+ data.tar.gz: '096814810e4705b73f2ec1e35326a6d8e5043149'
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- NzM2ZjJlNjg3OTllNmEyM2I1OWQ1NzBkNTA3ZDBlMjMwODA4NjVkZjk5Mjhh
10
- MmRiZmYxNmNiNWQ4MGZiMDJjZmFhMGFjNDc2YjJjNDg0YjI1MmNiODA2OTg0
11
- M2YxNTBiMTc0Yzc2NjI3Mjk1YzQ2Y2JmZjYyNGZkYjViM2Q2NTA=
12
- data.tar.gz: !binary |-
13
- OWE5MWJmNGYyYWJhNWRhZGYzMjY0MWQ5YWExM2M5NjZiZjRlYzdjM2FhMWI2
14
- NDc0NWEyN2NmZTM4Nzc4OWUxMGZlNzE2YzIxNmU3MDcyMGJiMDZiMWYyNmJj
15
- MGFlMDIxN2VhMzU4ODY3MWJiM2Y3MDhlNDU4MmZhZjUyZjI3MjI=
6
+ metadata.gz: b300749e8a858a2b4bd1208c0967bd44e5193b20f7801c01fc80ea8be773b150e4405542e0aab1c981625afe49d3f85486ff1f65cf934b93824db15d7fa36036
7
+ data.tar.gz: d00ba634b5a2c9eaeb0f86d886812d5a8073628fa483aa0dcfb9e042d51d6372a888c8c7068a284750bea367acb4d8f794846b6b3334aabf2673874e1c3c88f4
data/README.md CHANGED
@@ -18,11 +18,37 @@ Highly inspired by Copycopter by thoughtbot.
18
18
  5. Easy to install - works as an engine, comes with simple generator
19
19
  6. You can always export all translations to plain old YAML file
20
20
  7. Has build in wysiwyg editor ([jQuery TE](http://jqueryte.com/))
21
+ 8. Translating apps directly in frontend (see bellow)
22
+ 9. (On request) stores paths where translation keys were called
23
+ 10. (On request) is able to show all translation keys used to render current page
21
24
 
22
25
  ### Screenshots
23
26
 
24
27
  Check wiki: [Screenshots](https://github.com/prograils/lit/wiki/Screenshots)
25
28
 
29
+ ### Installation
30
+
31
+ 1. Add `lit` gem to your `Gemfile`
32
+ ```ruby
33
+ gem 'lit'
34
+ ```
35
+
36
+ For Ruby < 1.9 use `gem 'lit', '= 0.2.4'`, as next versions introduced new ruby hash syntax.
37
+
38
+ 2. run `bundle install`
39
+
40
+ 3. run installation generator `bundle exec rails g lit:install`
41
+ (for production/staging environment `redis` is suggested as key value engine. `hash` will not work in multi process environment)
42
+
43
+ 4. Add `config.i18n.available_locales = [...]` to `application.rb` - it's required to precompile appropriate language flags in lit backend.
44
+
45
+ 5. After doing above and restarting app, point your browser to `http://app/lit`
46
+
47
+ 6. Profit!
48
+
49
+
50
+ You may want to take a look at generated initializer in `config/initializers/lit.rb` and change some default configuration options.
51
+
26
52
  ### So... again - what is it and how to use it?
27
53
  *Lit* is Rails engine - it runs in it's own namespace, by default it's avaulable under `/lit`. It provides UI for managing translations of your app.
28
54
 
@@ -33,35 +59,88 @@ To optimize translation key lookup, *Lit* can use different cache engines. For p
33
59
  Keys ending with `_html` have auto wysiwyg support.
34
60
 
35
61
  You can also export translations using rake task
62
+
36
63
  ```bash
37
64
  $ rake lit:export
38
65
  ```
66
+
39
67
  You may also give it extra env variables to limit the export results.
68
+
40
69
  ```bash
41
70
  $ LOCALES=en,de rake lit:export
42
71
  ```
43
72
 
44
73
 
45
- ### Installation
74
+ ### 0.2 -> 0.3 upgrade guide
75
+
76
+ 1. Specify exact lit version in your Gemfile: `gem 'lit', '~> 0.3.0'`
77
+ 2. Run `bundle update lit`
78
+ 3. Add `config.i18n.available_locales` to your `application.rb` (see 3rd point from Installation info)
79
+ 4. Add `config.i18n.enforce_available_locales = true` config to your `application.rb`
80
+ 5. Compare your current `lit.rb` initializer with [template](https://github.com/prograils/lit/blob/master/lib/generators/lit/install/templates/initializer.rb).
81
+
82
+ ### On-site live translations
83
+
84
+ 1. Add `Lit::FrontendHelper` to your `ApplicationController`
85
+ 2.
86
+ ```ruby
87
+ helper Lit::FrontendHelper
88
+ ```
89
+
90
+ 2. In you layout file include lit assets
91
+
92
+ ```erb
93
+ <% if admin_user_signed_in? %>
94
+ <%= lit_frontend_assets %>
95
+ <% end %>
96
+ ```
97
+
98
+ 3. You're good to go - now log in to lit (if required) and open your frontend in separate tab (to have session persisted). On the bottom-right of your page you should see "Enable / disable lit highlight" - after enabling it you'll be able to click and translate phrases directly in your frontend
99
+
100
+ 4. Once enabled, all translations called via `t` helper function be rendered inside `<span />` tag, what may break your layout (ie if you're using translated values as button values or
101
+ as placeholders, etc). To avoid that add `skip_lit: true` to `t()` call or use `I18n.t`.
102
+
103
+ ### Storing request info
104
+
105
+ 1. Include `Lit::Concerns::RequestInfoStore` concern in your `ApplicationController`
106
+
107
+ ```ruby
108
+ include Lit::Concerns::RequestInfoStore
109
+ ```
110
+
111
+ 2. In lit initializer (`lit.rb`) set `store_request_info` config to true
46
112
 
47
- 1. Add `lit` gem to your `Gemfile`
48
113
  ```ruby
49
- gem 'lit'
114
+ Lit.store_request_info = true
50
115
  ```
51
116
 
52
- For Ruby < 1.9 use `gem 'lit', '= 0.2.4'`, as next versions introduced new ruby hash syntax.
117
+ 3. Lit authorized user must be signed in for this feature to work!
53
118
 
54
- 2. run `bundle install`
119
+ ### Showing called translations in frontend
55
120
 
56
- 3. run installation generator `bundle exec rails g lit:install`
57
- (for production/staging environment `redis` is suggested as key value engine. `hash` will not work in multi process environment)
58
121
 
59
- 4. After doing above and restarting app, point your browser to `http://app/lit`
122
+ 1. Add `Lit::FrontendHelper` in your `ApplicationController`
60
123
 
61
- 5. Profit!
124
+ ```ruby
125
+ include Lit::FrontendHelper
126
+ ```
62
127
 
128
+ 2. Include `Lit::Concerns::RequestKeysStore` concern in your `ApplicationController`
129
+
130
+ ```ruby
131
+ include Lit::Concerns::RequestKeysStore
132
+ ```
133
+
134
+ 3. On the bottom of you layout file call `lit_translations_info` helper function
135
+
136
+ ```erb
137
+ <%= lit_translations_info %>
138
+ ```
139
+
140
+ 4. From now on you'll be able to see all translation keys that were used to render current page. This feature works great with on-site live translations!
141
+
142
+ 5. Lit authorized user must be signed in for this feature to work!
63
143
 
64
- You may want to take a look at generated initializer in `config/initializers/lit.rb` and change some default configuration options.
65
144
 
66
145
 
67
146
  ### ToDo
@@ -74,14 +153,16 @@ You may want to take a look at generated initializer in `config/initializers/lit
74
153
  * ~~Support for array types (ie. `date.abbr_day_names`)~~
75
154
  * ~~Generator~~
76
155
  * ~~Support for wysiwyg~~
77
- * Better cache
156
+ * ~~Better cache~~
78
157
  * ~~Support for other key value providers (ie. Redis does not support Array types in easy way)~~ (not applicable, as array storage works now with redis).
79
158
  * Integration with ActiveAdmin
80
159
  * Support for Proc defaults (like in `I18n.t('not_exising_keys', default: lambda{|_, options| 'text'})` )
81
160
 
82
161
  ### Testing
83
162
 
84
- For local testing [wwtd](https://github.com/grosser/wwtd) gem comes into play, run tests via: `wwtd --local`. Run migrations using command `RAILS_ENV=test bundle exec rake db:migrate`.
163
+ For local testing [Appraisal](https://github.com/thoughtbot/appraisal) gem comes into play, run tests via: `bundle exec appraisal rails-4.2 rake test`.
164
+
165
+ Please remember to edit `test/dummy/config/database.yml` file
85
166
 
86
167
  ### License
87
168
 
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env rake
2
+ require 'rubygems'
2
3
  begin
3
4
  require 'bundler/setup'
4
5
  rescue LoadError
@@ -12,9 +12,8 @@
12
12
  //
13
13
  //= require jquery
14
14
  //= require jquery_ujs
15
- //= require_tree .
15
+ //= require_directory ./backend/
16
16
 
17
17
  $(document).ready(function(){
18
18
  $('.title').tooltip();
19
-
20
19
  });
@@ -22,3 +22,5 @@ $(document).ready ->
22
22
  $parent.children('td').html('')
23
23
  $('tr.localization_key_row').on 'click', 'input.wysiwyg_switch', (e)->
24
24
  $(this).parents('form').find("textarea").jqte()
25
+ $('tr.localization_key_row').on 'click', '.request_info_link', (e)->
26
+ $(this).parents('tr.localization_key_row').find(".request_info_row").toggleClass('hidden')
@@ -0,0 +1,24 @@
1
+ $ ->
2
+ sourceId = $('#source_id').attr('value')
3
+
4
+ updateFunc = ->
5
+ $.ajax "/lit/sources/" + sourceId + "/sync_complete",
6
+ type: 'GET'
7
+ format: 'json'
8
+ success: (xml, textStatus, xhr) ->
9
+ if xhr.responseJSON.sync_complete
10
+ $('.loading').addClass('loaded').removeClass('loading')
11
+ clearInterval(interval)
12
+ location.reload()
13
+ statusCode:
14
+ 404: ->
15
+ $('.loading').text('Could not update synchronization status, please try refreshing page')
16
+ 401: ->
17
+ $('.loading').text('You are not authorized. Please check if you are properly logged in')
18
+ 500: ->
19
+ $('.loading').text('Something went wrong, please try synchronizing again')
20
+ error: ->
21
+ clearInterval(interval)
22
+
23
+ if $('.loading').length > 0
24
+ interval = window.setInterval(updateFunc, 500)
@@ -0,0 +1,103 @@
1
+ //= require ./mousetrap.js
2
+ "use strict";
3
+
4
+ (function() {
5
+ var $btn, $elem;
6
+ var buildLocalizationForm, getLocalizationPath, getLocalizationDetails,
7
+ replaceWithForm, submitForm, removeLitForm;
8
+
9
+ buildLocalizationForm = function(e){
10
+ var $this = $(this);
11
+ var meta = $('meta[name="lit-url-base"]');
12
+ if(meta.length > 0){
13
+ getLocalizationPath($this, meta);
14
+ //replaceWithForm(e.currentTarget, value, update_path)
15
+ }
16
+ e.stopPropagation();
17
+ return false;
18
+ }
19
+
20
+ getLocalizationPath = function(elem, metaElem) {
21
+ $.getJSON(metaElem.attr('value'),
22
+ { locale: elem.data('locale'),
23
+ key: elem.data('key') },
24
+ function(data){
25
+ getLocalizationDetails(elem, data.path);
26
+ }
27
+ );
28
+ };
29
+
30
+ getLocalizationDetails = function(elem, path){
31
+ $.getJSON(path, {},
32
+ function(data){
33
+ replaceWithForm(elem, data.value, path);
34
+ }
35
+ );
36
+ };
37
+
38
+ replaceWithForm = function(elem, value, update_path){
39
+ removeLitForm();
40
+ var $this = $(elem);
41
+ $this.attr('contentEditable', true);
42
+ $this.html( value );
43
+ $this.focus();
44
+ $this.on('blur', function(){
45
+ submitForm($this, $this.html(), update_path);
46
+ removeLitForm();
47
+ });
48
+ };
49
+
50
+ submitForm = function(elem, val, update_path){
51
+ $.ajax({
52
+ type: 'PATCH',
53
+ dataType: 'json',
54
+ url: update_path,
55
+ data: { 'localization[translated_value]': val },
56
+ success: function(data){
57
+ elem.html( data.html );
58
+ elem.attr('contentEditable', false);
59
+ console.log('saved ' + elem.data('key'));
60
+ },
61
+ error: function(){
62
+ console.log('problem saving ' + elem.data('key'));
63
+ alert('ups, ops, something went wrong');
64
+ }
65
+ });
66
+ };
67
+
68
+ removeLitForm = function(){
69
+ $('#litForm').remove();
70
+ }
71
+
72
+ $(document).ready(function(){
73
+ $('<div id="lit_button_wrapper" />').appendTo('body');
74
+ $btn = $('#lit_button_wrapper').text('Enable / disable lit highlight');
75
+ $btn.on('click', function(){
76
+ removeLitForm();
77
+ if($btn.hasClass('lit-highlight-enabled')){
78
+ $('.lit-key-generic').removeClass('lit-key-highlight').off('click.form');
79
+ $btn.removeClass('lit-highlight-enabled');
80
+ $('.lit-key-generic').each(function(_, elem){
81
+ $elem = $(elem);
82
+ $elem.attr('title', $elem.data('old-title') || '');
83
+ });
84
+ }else{
85
+ $('.lit-key-generic').addClass('lit-key-highlight').on('click.form', buildLocalizationForm);
86
+ $btn.addClass('lit-highlight-enabled');
87
+ $('.lit-key-generic').each(function(_, elem){
88
+ $elem = $(elem);
89
+ $elem.data('old-title', $elem.attr('title'));
90
+ $elem.attr('title', $elem.data('key'));
91
+ });
92
+ }
93
+ });
94
+ $('.lit-translations-info .lit-open-button').click(function(){
95
+ $('.lit-translations-info').toggleClass('collapsed expanded');
96
+ });
97
+ $('.lit-translations-info .lit-close-button').click(function(){
98
+ $('.lit-translations-info').toggleClass('collapsed expanded');
99
+ });
100
+ });
101
+
102
+ }).call(this);
103
+
@@ -0,0 +1,1044 @@
1
+ /*global define:false */
2
+ /**
3
+ * Copyright 2016 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.6.0
21
+ * @url craig.is/killing/mice
22
+ */
23
+ (function(window, document, undefined) {
24
+
25
+ // Check if mousetrap is used inside browser, if not, return
26
+ if (!window) {
27
+ return;
28
+ }
29
+
30
+ /**
31
+ * mapping of special keycodes to their corresponding keys
32
+ *
33
+ * everything in this dictionary cannot use keypress events
34
+ * so it has to be here to map to the correct keycodes for
35
+ * keyup/keydown events
36
+ *
37
+ * @type {Object}
38
+ */
39
+ var _MAP = {
40
+ 8: 'backspace',
41
+ 9: 'tab',
42
+ 13: 'enter',
43
+ 16: 'shift',
44
+ 17: 'ctrl',
45
+ 18: 'alt',
46
+ 20: 'capslock',
47
+ 27: 'esc',
48
+ 32: 'space',
49
+ 33: 'pageup',
50
+ 34: 'pagedown',
51
+ 35: 'end',
52
+ 36: 'home',
53
+ 37: 'left',
54
+ 38: 'up',
55
+ 39: 'right',
56
+ 40: 'down',
57
+ 45: 'ins',
58
+ 46: 'del',
59
+ 91: 'meta',
60
+ 93: 'meta',
61
+ 224: 'meta'
62
+ };
63
+
64
+ /**
65
+ * mapping for special characters so they can support
66
+ *
67
+ * this dictionary is only used incase you want to bind a
68
+ * keyup or keydown event to one of these keys
69
+ *
70
+ * @type {Object}
71
+ */
72
+ var _KEYCODE_MAP = {
73
+ 106: '*',
74
+ 107: '+',
75
+ 109: '-',
76
+ 110: '.',
77
+ 111 : '/',
78
+ 186: ';',
79
+ 187: '=',
80
+ 188: ',',
81
+ 189: '-',
82
+ 190: '.',
83
+ 191: '/',
84
+ 192: '`',
85
+ 219: '[',
86
+ 220: '\\',
87
+ 221: ']',
88
+ 222: '\''
89
+ };
90
+
91
+ /**
92
+ * this is a mapping of keys that require shift on a US keypad
93
+ * back to the non shift equivelents
94
+ *
95
+ * this is so you can use keyup events with these keys
96
+ *
97
+ * note that this will only work reliably on US keyboards
98
+ *
99
+ * @type {Object}
100
+ */
101
+ var _SHIFT_MAP = {
102
+ '~': '`',
103
+ '!': '1',
104
+ '@': '2',
105
+ '#': '3',
106
+ '$': '4',
107
+ '%': '5',
108
+ '^': '6',
109
+ '&': '7',
110
+ '*': '8',
111
+ '(': '9',
112
+ ')': '0',
113
+ '_': '-',
114
+ '+': '=',
115
+ ':': ';',
116
+ '\"': '\'',
117
+ '<': ',',
118
+ '>': '.',
119
+ '?': '/',
120
+ '|': '\\'
121
+ };
122
+
123
+ /**
124
+ * this is a list of special strings you can use to map
125
+ * to modifier keys when you specify your keyboard shortcuts
126
+ *
127
+ * @type {Object}
128
+ */
129
+ var _SPECIAL_ALIASES = {
130
+ 'option': 'alt',
131
+ 'command': 'meta',
132
+ 'return': 'enter',
133
+ 'escape': 'esc',
134
+ 'plus': '+',
135
+ 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
136
+ };
137
+
138
+ /**
139
+ * variable to store the flipped version of _MAP from above
140
+ * needed to check if we should use keypress or not when no action
141
+ * is specified
142
+ *
143
+ * @type {Object|undefined}
144
+ */
145
+ var _REVERSE_MAP;
146
+
147
+ /**
148
+ * loop through the f keys, f1 to f19 and add them to the map
149
+ * programatically
150
+ */
151
+ for (var i = 1; i < 20; ++i) {
152
+ _MAP[111 + i] = 'f' + i;
153
+ }
154
+
155
+ /**
156
+ * loop through to map numbers on the numeric keypad
157
+ */
158
+ for (i = 0; i <= 9; ++i) {
159
+
160
+ // This needs to use a string cause otherwise since 0 is falsey
161
+ // mousetrap will never fire for numpad 0 pressed as part of a keydown
162
+ // event.
163
+ //
164
+ // @see https://github.com/ccampbell/mousetrap/pull/258
165
+ _MAP[i + 96] = i.toString();
166
+ }
167
+
168
+ /**
169
+ * cross browser add event method
170
+ *
171
+ * @param {Element|HTMLDocument} object
172
+ * @param {string} type
173
+ * @param {Function} callback
174
+ * @returns void
175
+ */
176
+ function _addEvent(object, type, callback) {
177
+ if (object.addEventListener) {
178
+ object.addEventListener(type, callback, false);
179
+ return;
180
+ }
181
+
182
+ object.attachEvent('on' + type, callback);
183
+ }
184
+
185
+ /**
186
+ * takes the event and returns the key character
187
+ *
188
+ * @param {Event} e
189
+ * @return {string}
190
+ */
191
+ function _characterFromEvent(e) {
192
+
193
+ // for keypress events we should return the character as is
194
+ if (e.type == 'keypress') {
195
+ var character = String.fromCharCode(e.which);
196
+
197
+ // if the shift key is not pressed then it is safe to assume
198
+ // that we want the character to be lowercase. this means if
199
+ // you accidentally have caps lock on then your key bindings
200
+ // will continue to work
201
+ //
202
+ // the only side effect that might not be desired is if you
203
+ // bind something like 'A' cause you want to trigger an
204
+ // event when capital A is pressed caps lock will no longer
205
+ // trigger the event. shift+a will though.
206
+ if (!e.shiftKey) {
207
+ character = character.toLowerCase();
208
+ }
209
+
210
+ return character;
211
+ }
212
+
213
+ // for non keypress events the special maps are needed
214
+ if (_MAP[e.which]) {
215
+ return _MAP[e.which];
216
+ }
217
+
218
+ if (_KEYCODE_MAP[e.which]) {
219
+ return _KEYCODE_MAP[e.which];
220
+ }
221
+
222
+ // if it is not in the special map
223
+
224
+ // with keydown and keyup events the character seems to always
225
+ // come in as an uppercase character whether you are pressing shift
226
+ // or not. we should make sure it is always lowercase for comparisons
227
+ return String.fromCharCode(e.which).toLowerCase();
228
+ }
229
+
230
+ /**
231
+ * checks if two arrays are equal
232
+ *
233
+ * @param {Array} modifiers1
234
+ * @param {Array} modifiers2
235
+ * @returns {boolean}
236
+ */
237
+ function _modifiersMatch(modifiers1, modifiers2) {
238
+ return modifiers1.sort().join(',') === modifiers2.sort().join(',');
239
+ }
240
+
241
+ /**
242
+ * takes a key event and figures out what the modifiers are
243
+ *
244
+ * @param {Event} e
245
+ * @returns {Array}
246
+ */
247
+ function _eventModifiers(e) {
248
+ var modifiers = [];
249
+
250
+ if (e.shiftKey) {
251
+ modifiers.push('shift');
252
+ }
253
+
254
+ if (e.altKey) {
255
+ modifiers.push('alt');
256
+ }
257
+
258
+ if (e.ctrlKey) {
259
+ modifiers.push('ctrl');
260
+ }
261
+
262
+ if (e.metaKey) {
263
+ modifiers.push('meta');
264
+ }
265
+
266
+ return modifiers;
267
+ }
268
+
269
+ /**
270
+ * prevents default for this event
271
+ *
272
+ * @param {Event} e
273
+ * @returns void
274
+ */
275
+ function _preventDefault(e) {
276
+ if (e.preventDefault) {
277
+ e.preventDefault();
278
+ return;
279
+ }
280
+
281
+ e.returnValue = false;
282
+ }
283
+
284
+ /**
285
+ * stops propogation for this event
286
+ *
287
+ * @param {Event} e
288
+ * @returns void
289
+ */
290
+ function _stopPropagation(e) {
291
+ if (e.stopPropagation) {
292
+ e.stopPropagation();
293
+ return;
294
+ }
295
+
296
+ e.cancelBubble = true;
297
+ }
298
+
299
+ /**
300
+ * determines if the keycode specified is a modifier key or not
301
+ *
302
+ * @param {string} key
303
+ * @returns {boolean}
304
+ */
305
+ function _isModifier(key) {
306
+ return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
307
+ }
308
+
309
+ /**
310
+ * reverses the map lookup so that we can look for specific keys
311
+ * to see what can and can't use keypress
312
+ *
313
+ * @return {Object}
314
+ */
315
+ function _getReverseMap() {
316
+ if (!_REVERSE_MAP) {
317
+ _REVERSE_MAP = {};
318
+ for (var key in _MAP) {
319
+
320
+ // pull out the numeric keypad from here cause keypress should
321
+ // be able to detect the keys from the character
322
+ if (key > 95 && key < 112) {
323
+ continue;
324
+ }
325
+
326
+ if (_MAP.hasOwnProperty(key)) {
327
+ _REVERSE_MAP[_MAP[key]] = key;
328
+ }
329
+ }
330
+ }
331
+ return _REVERSE_MAP;
332
+ }
333
+
334
+ /**
335
+ * picks the best action based on the key combination
336
+ *
337
+ * @param {string} key - character for key
338
+ * @param {Array} modifiers
339
+ * @param {string=} action passed in
340
+ */
341
+ function _pickBestAction(key, modifiers, action) {
342
+
343
+ // if no action was picked in we should try to pick the one
344
+ // that we think would work best for this key
345
+ if (!action) {
346
+ action = _getReverseMap()[key] ? 'keydown' : 'keypress';
347
+ }
348
+
349
+ // modifier keys don't work as expected with keypress,
350
+ // switch to keydown
351
+ if (action == 'keypress' && modifiers.length) {
352
+ action = 'keydown';
353
+ }
354
+
355
+ return action;
356
+ }
357
+
358
+ /**
359
+ * Converts from a string key combination to an array
360
+ *
361
+ * @param {string} combination like "command+shift+l"
362
+ * @return {Array}
363
+ */
364
+ function _keysFromString(combination) {
365
+ if (combination === '+') {
366
+ return ['+'];
367
+ }
368
+
369
+ combination = combination.replace(/\+{2}/g, '+plus');
370
+ return combination.split('+');
371
+ }
372
+
373
+ /**
374
+ * Gets info for a specific key combination
375
+ *
376
+ * @param {string} combination key combination ("command+s" or "a" or "*")
377
+ * @param {string=} action
378
+ * @returns {Object}
379
+ */
380
+ function _getKeyInfo(combination, action) {
381
+ var keys;
382
+ var key;
383
+ var i;
384
+ var modifiers = [];
385
+
386
+ // take the keys from this pattern and figure out what the actual
387
+ // pattern is all about
388
+ keys = _keysFromString(combination);
389
+
390
+ for (i = 0; i < keys.length; ++i) {
391
+ key = keys[i];
392
+
393
+ // normalize key names
394
+ if (_SPECIAL_ALIASES[key]) {
395
+ key = _SPECIAL_ALIASES[key];
396
+ }
397
+
398
+ // if this is not a keypress event then we should
399
+ // be smart about using shift keys
400
+ // this will only work for US keyboards however
401
+ if (action && action != 'keypress' && _SHIFT_MAP[key]) {
402
+ key = _SHIFT_MAP[key];
403
+ modifiers.push('shift');
404
+ }
405
+
406
+ // if this key is a modifier then add it to the list of modifiers
407
+ if (_isModifier(key)) {
408
+ modifiers.push(key);
409
+ }
410
+ }
411
+
412
+ // depending on what the key combination is
413
+ // we will try to pick the best event for it
414
+ action = _pickBestAction(key, modifiers, action);
415
+
416
+ return {
417
+ key: key,
418
+ modifiers: modifiers,
419
+ action: action
420
+ };
421
+ }
422
+
423
+ function _belongsTo(element, ancestor) {
424
+ if (element === null || element === document) {
425
+ return false;
426
+ }
427
+
428
+ if (element === ancestor) {
429
+ return true;
430
+ }
431
+
432
+ return _belongsTo(element.parentNode, ancestor);
433
+ }
434
+
435
+ function Mousetrap(targetElement) {
436
+ var self = this;
437
+
438
+ targetElement = targetElement || document;
439
+
440
+ if (!(self instanceof Mousetrap)) {
441
+ return new Mousetrap(targetElement);
442
+ }
443
+
444
+ /**
445
+ * element to attach key events to
446
+ *
447
+ * @type {Element}
448
+ */
449
+ self.target = targetElement;
450
+
451
+ /**
452
+ * a list of all the callbacks setup via Mousetrap.bind()
453
+ *
454
+ * @type {Object}
455
+ */
456
+ self._callbacks = {};
457
+
458
+ /**
459
+ * direct map of string combinations to callbacks used for trigger()
460
+ *
461
+ * @type {Object}
462
+ */
463
+ self._directMap = {};
464
+
465
+ /**
466
+ * keeps track of what level each sequence is at since multiple
467
+ * sequences can start out with the same sequence
468
+ *
469
+ * @type {Object}
470
+ */
471
+ var _sequenceLevels = {};
472
+
473
+ /**
474
+ * variable to store the setTimeout call
475
+ *
476
+ * @type {null|number}
477
+ */
478
+ var _resetTimer;
479
+
480
+ /**
481
+ * temporary state where we will ignore the next keyup
482
+ *
483
+ * @type {boolean|string}
484
+ */
485
+ var _ignoreNextKeyup = false;
486
+
487
+ /**
488
+ * temporary state where we will ignore the next keypress
489
+ *
490
+ * @type {boolean}
491
+ */
492
+ var _ignoreNextKeypress = false;
493
+
494
+ /**
495
+ * are we currently inside of a sequence?
496
+ * type of action ("keyup" or "keydown" or "keypress") or false
497
+ *
498
+ * @type {boolean|string}
499
+ */
500
+ var _nextExpectedAction = false;
501
+
502
+ /**
503
+ * resets all sequence counters except for the ones passed in
504
+ *
505
+ * @param {Object} doNotReset
506
+ * @returns void
507
+ */
508
+ function _resetSequences(doNotReset) {
509
+ doNotReset = doNotReset || {};
510
+
511
+ var activeSequences = false,
512
+ key;
513
+
514
+ for (key in _sequenceLevels) {
515
+ if (doNotReset[key]) {
516
+ activeSequences = true;
517
+ continue;
518
+ }
519
+ _sequenceLevels[key] = 0;
520
+ }
521
+
522
+ if (!activeSequences) {
523
+ _nextExpectedAction = false;
524
+ }
525
+ }
526
+
527
+ /**
528
+ * finds all callbacks that match based on the keycode, modifiers,
529
+ * and action
530
+ *
531
+ * @param {string} character
532
+ * @param {Array} modifiers
533
+ * @param {Event|Object} e
534
+ * @param {string=} sequenceName - name of the sequence we are looking for
535
+ * @param {string=} combination
536
+ * @param {number=} level
537
+ * @returns {Array}
538
+ */
539
+ function _getMatches(character, modifiers, e, sequenceName, combination, level) {
540
+ var i;
541
+ var callback;
542
+ var matches = [];
543
+ var action = e.type;
544
+
545
+ // if there are no events related to this keycode
546
+ if (!self._callbacks[character]) {
547
+ return [];
548
+ }
549
+
550
+ // if a modifier key is coming up on its own we should allow it
551
+ if (action == 'keyup' && _isModifier(character)) {
552
+ modifiers = [character];
553
+ }
554
+
555
+ // loop through all callbacks for the key that was pressed
556
+ // and see if any of them match
557
+ for (i = 0; i < self._callbacks[character].length; ++i) {
558
+ callback = self._callbacks[character][i];
559
+
560
+ // if a sequence name is not specified, but this is a sequence at
561
+ // the wrong level then move onto the next match
562
+ if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
563
+ continue;
564
+ }
565
+
566
+ // if the action we are looking for doesn't match the action we got
567
+ // then we should keep going
568
+ if (action != callback.action) {
569
+ continue;
570
+ }
571
+
572
+ // if this is a keypress event and the meta key and control key
573
+ // are not pressed that means that we need to only look at the
574
+ // character, otherwise check the modifiers as well
575
+ //
576
+ // chrome will not fire a keypress if meta or control is down
577
+ // safari will fire a keypress if meta or meta+shift is down
578
+ // firefox will fire a keypress if meta or control is down
579
+ if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
580
+
581
+ // when you bind a combination or sequence a second time it
582
+ // should overwrite the first one. if a sequenceName or
583
+ // combination is specified in this call it does just that
584
+ //
585
+ // @todo make deleting its own method?
586
+ var deleteCombo = !sequenceName && callback.combo == combination;
587
+ var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
588
+ if (deleteCombo || deleteSequence) {
589
+ self._callbacks[character].splice(i, 1);
590
+ }
591
+
592
+ matches.push(callback);
593
+ }
594
+ }
595
+
596
+ return matches;
597
+ }
598
+
599
+ /**
600
+ * actually calls the callback function
601
+ *
602
+ * if your callback function returns false this will use the jquery
603
+ * convention - prevent default and stop propogation on the event
604
+ *
605
+ * @param {Function} callback
606
+ * @param {Event} e
607
+ * @returns void
608
+ */
609
+ function _fireCallback(callback, e, combo, sequence) {
610
+
611
+ // if this event should not happen stop here
612
+ if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
613
+ return;
614
+ }
615
+
616
+ if (callback(e, combo) === false) {
617
+ _preventDefault(e);
618
+ _stopPropagation(e);
619
+ }
620
+ }
621
+
622
+ /**
623
+ * handles a character key event
624
+ *
625
+ * @param {string} character
626
+ * @param {Array} modifiers
627
+ * @param {Event} e
628
+ * @returns void
629
+ */
630
+ self._handleKey = function(character, modifiers, e) {
631
+ var callbacks = _getMatches(character, modifiers, e);
632
+ var i;
633
+ var doNotReset = {};
634
+ var maxLevel = 0;
635
+ var processedSequenceCallback = false;
636
+
637
+ // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
638
+ for (i = 0; i < callbacks.length; ++i) {
639
+ if (callbacks[i].seq) {
640
+ maxLevel = Math.max(maxLevel, callbacks[i].level);
641
+ }
642
+ }
643
+
644
+ // loop through matching callbacks for this key event
645
+ for (i = 0; i < callbacks.length; ++i) {
646
+
647
+ // fire for all sequence callbacks
648
+ // this is because if for example you have multiple sequences
649
+ // bound such as "g i" and "g t" they both need to fire the
650
+ // callback for matching g cause otherwise you can only ever
651
+ // match the first one
652
+ if (callbacks[i].seq) {
653
+
654
+ // only fire callbacks for the maxLevel to prevent
655
+ // subsequences from also firing
656
+ //
657
+ // for example 'a option b' should not cause 'option b' to fire
658
+ // even though 'option b' is part of the other sequence
659
+ //
660
+ // any sequences that do not match here will be discarded
661
+ // below by the _resetSequences call
662
+ if (callbacks[i].level != maxLevel) {
663
+ continue;
664
+ }
665
+
666
+ processedSequenceCallback = true;
667
+
668
+ // keep a list of which sequences were matches for later
669
+ doNotReset[callbacks[i].seq] = 1;
670
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
671
+ continue;
672
+ }
673
+
674
+ // if there were no sequence matches but we are still here
675
+ // that means this is a regular match so we should fire that
676
+ if (!processedSequenceCallback) {
677
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
678
+ }
679
+ }
680
+
681
+ // if the key you pressed matches the type of sequence without
682
+ // being a modifier (ie "keyup" or "keypress") then we should
683
+ // reset all sequences that were not matched by this event
684
+ //
685
+ // this is so, for example, if you have the sequence "h a t" and you
686
+ // type "h e a r t" it does not match. in this case the "e" will
687
+ // cause the sequence to reset
688
+ //
689
+ // modifier keys are ignored because you can have a sequence
690
+ // that contains modifiers such as "enter ctrl+space" and in most
691
+ // cases the modifier key will be pressed before the next key
692
+ //
693
+ // also if you have a sequence such as "ctrl+b a" then pressing the
694
+ // "b" key will trigger a "keypress" and a "keydown"
695
+ //
696
+ // the "keydown" is expected when there is a modifier, but the
697
+ // "keypress" ends up matching the _nextExpectedAction since it occurs
698
+ // after and that causes the sequence to reset
699
+ //
700
+ // we ignore keypresses in a sequence that directly follow a keydown
701
+ // for the same character
702
+ var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
703
+ if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
704
+ _resetSequences(doNotReset);
705
+ }
706
+
707
+ _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
708
+ };
709
+
710
+ /**
711
+ * handles a keydown event
712
+ *
713
+ * @param {Event} e
714
+ * @returns void
715
+ */
716
+ function _handleKeyEvent(e) {
717
+
718
+ // normalize e.which for key events
719
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
720
+ if (typeof e.which !== 'number') {
721
+ e.which = e.keyCode;
722
+ }
723
+
724
+ var character = _characterFromEvent(e);
725
+
726
+ // no character found then stop
727
+ if (!character) {
728
+ return;
729
+ }
730
+
731
+ // need to use === for the character check because the character can be 0
732
+ if (e.type == 'keyup' && _ignoreNextKeyup === character) {
733
+ _ignoreNextKeyup = false;
734
+ return;
735
+ }
736
+
737
+ self.handleKey(character, _eventModifiers(e), e);
738
+ }
739
+
740
+ /**
741
+ * called to set a 1 second timeout on the specified sequence
742
+ *
743
+ * this is so after each key press in the sequence you have 1 second
744
+ * to press the next key before you have to start over
745
+ *
746
+ * @returns void
747
+ */
748
+ function _resetSequenceTimer() {
749
+ clearTimeout(_resetTimer);
750
+ _resetTimer = setTimeout(_resetSequences, 1000);
751
+ }
752
+
753
+ /**
754
+ * binds a key sequence to an event
755
+ *
756
+ * @param {string} combo - combo specified in bind call
757
+ * @param {Array} keys
758
+ * @param {Function} callback
759
+ * @param {string=} action
760
+ * @returns void
761
+ */
762
+ function _bindSequence(combo, keys, callback, action) {
763
+
764
+ // start off by adding a sequence level record for this combination
765
+ // and setting the level to 0
766
+ _sequenceLevels[combo] = 0;
767
+
768
+ /**
769
+ * callback to increase the sequence level for this sequence and reset
770
+ * all other sequences that were active
771
+ *
772
+ * @param {string} nextAction
773
+ * @returns {Function}
774
+ */
775
+ function _increaseSequence(nextAction) {
776
+ return function() {
777
+ _nextExpectedAction = nextAction;
778
+ ++_sequenceLevels[combo];
779
+ _resetSequenceTimer();
780
+ };
781
+ }
782
+
783
+ /**
784
+ * wraps the specified callback inside of another function in order
785
+ * to reset all sequence counters as soon as this sequence is done
786
+ *
787
+ * @param {Event} e
788
+ * @returns void
789
+ */
790
+ function _callbackAndReset(e) {
791
+ _fireCallback(callback, e, combo);
792
+
793
+ // we should ignore the next key up if the action is key down
794
+ // or keypress. this is so if you finish a sequence and
795
+ // release the key the final key will not trigger a keyup
796
+ if (action !== 'keyup') {
797
+ _ignoreNextKeyup = _characterFromEvent(e);
798
+ }
799
+
800
+ // weird race condition if a sequence ends with the key
801
+ // another sequence begins with
802
+ setTimeout(_resetSequences, 10);
803
+ }
804
+
805
+ // loop through keys one at a time and bind the appropriate callback
806
+ // function. for any key leading up to the final one it should
807
+ // increase the sequence. after the final, it should reset all sequences
808
+ //
809
+ // if an action is specified in the original bind call then that will
810
+ // be used throughout. otherwise we will pass the action that the
811
+ // next key in the sequence should match. this allows a sequence
812
+ // to mix and match keypress and keydown events depending on which
813
+ // ones are better suited to the key provided
814
+ for (var i = 0; i < keys.length; ++i) {
815
+ var isFinal = i + 1 === keys.length;
816
+ var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
817
+ _bindSingle(keys[i], wrappedCallback, action, combo, i);
818
+ }
819
+ }
820
+
821
+ /**
822
+ * binds a single keyboard combination
823
+ *
824
+ * @param {string} combination
825
+ * @param {Function} callback
826
+ * @param {string=} action
827
+ * @param {string=} sequenceName - name of sequence if part of sequence
828
+ * @param {number=} level - what part of the sequence the command is
829
+ * @returns void
830
+ */
831
+ function _bindSingle(combination, callback, action, sequenceName, level) {
832
+
833
+ // store a direct mapped reference for use with Mousetrap.trigger
834
+ self._directMap[combination + ':' + action] = callback;
835
+
836
+ // make sure multiple spaces in a row become a single space
837
+ combination = combination.replace(/\s+/g, ' ');
838
+
839
+ var sequence = combination.split(' ');
840
+ var info;
841
+
842
+ // if this pattern is a sequence of keys then run through this method
843
+ // to reprocess each pattern one key at a time
844
+ if (sequence.length > 1) {
845
+ _bindSequence(combination, sequence, callback, action);
846
+ return;
847
+ }
848
+
849
+ info = _getKeyInfo(combination, action);
850
+
851
+ // make sure to initialize array if this is the first time
852
+ // a callback is added for this key
853
+ self._callbacks[info.key] = self._callbacks[info.key] || [];
854
+
855
+ // remove an existing match if there is one
856
+ _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
857
+
858
+ // add this call back to the array
859
+ // if it is a sequence put it at the beginning
860
+ // if not put it at the end
861
+ //
862
+ // this is important because the way these are processed expects
863
+ // the sequence ones to come first
864
+ self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
865
+ callback: callback,
866
+ modifiers: info.modifiers,
867
+ action: info.action,
868
+ seq: sequenceName,
869
+ level: level,
870
+ combo: combination
871
+ });
872
+ }
873
+
874
+ /**
875
+ * binds multiple combinations to the same callback
876
+ *
877
+ * @param {Array} combinations
878
+ * @param {Function} callback
879
+ * @param {string|undefined} action
880
+ * @returns void
881
+ */
882
+ self._bindMultiple = function(combinations, callback, action) {
883
+ for (var i = 0; i < combinations.length; ++i) {
884
+ _bindSingle(combinations[i], callback, action);
885
+ }
886
+ };
887
+
888
+ // start!
889
+ _addEvent(targetElement, 'keypress', _handleKeyEvent);
890
+ _addEvent(targetElement, 'keydown', _handleKeyEvent);
891
+ _addEvent(targetElement, 'keyup', _handleKeyEvent);
892
+ }
893
+
894
+ /**
895
+ * binds an event to mousetrap
896
+ *
897
+ * can be a single key, a combination of keys separated with +,
898
+ * an array of keys, or a sequence of keys separated by spaces
899
+ *
900
+ * be sure to list the modifier keys first to make sure that the
901
+ * correct key ends up getting bound (the last key in the pattern)
902
+ *
903
+ * @param {string|Array} keys
904
+ * @param {Function} callback
905
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
906
+ * @returns void
907
+ */
908
+ Mousetrap.prototype.bind = function(keys, callback, action) {
909
+ var self = this;
910
+ keys = keys instanceof Array ? keys : [keys];
911
+ self._bindMultiple.call(self, keys, callback, action);
912
+ return self;
913
+ };
914
+
915
+ /**
916
+ * unbinds an event to mousetrap
917
+ *
918
+ * the unbinding sets the callback function of the specified key combo
919
+ * to an empty function and deletes the corresponding key in the
920
+ * _directMap dict.
921
+ *
922
+ * TODO: actually remove this from the _callbacks dictionary instead
923
+ * of binding an empty function
924
+ *
925
+ * the keycombo+action has to be exactly the same as
926
+ * it was defined in the bind method
927
+ *
928
+ * @param {string|Array} keys
929
+ * @param {string} action
930
+ * @returns void
931
+ */
932
+ Mousetrap.prototype.unbind = function(keys, action) {
933
+ var self = this;
934
+ return self.bind.call(self, keys, function() {}, action);
935
+ };
936
+
937
+ /**
938
+ * triggers an event that has already been bound
939
+ *
940
+ * @param {string} keys
941
+ * @param {string=} action
942
+ * @returns void
943
+ */
944
+ Mousetrap.prototype.trigger = function(keys, action) {
945
+ var self = this;
946
+ if (self._directMap[keys + ':' + action]) {
947
+ self._directMap[keys + ':' + action]({}, keys);
948
+ }
949
+ return self;
950
+ };
951
+
952
+ /**
953
+ * resets the library back to its initial state. this is useful
954
+ * if you want to clear out the current keyboard shortcuts and bind
955
+ * new ones - for example if you switch to another page
956
+ *
957
+ * @returns void
958
+ */
959
+ Mousetrap.prototype.reset = function() {
960
+ var self = this;
961
+ self._callbacks = {};
962
+ self._directMap = {};
963
+ return self;
964
+ };
965
+
966
+ /**
967
+ * should we stop this event before firing off callbacks
968
+ *
969
+ * @param {Event} e
970
+ * @param {Element} element
971
+ * @return {boolean}
972
+ */
973
+ Mousetrap.prototype.stopCallback = function(e, element) {
974
+ var self = this;
975
+
976
+ // if the element has the class "mousetrap" then no need to stop
977
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
978
+ return false;
979
+ }
980
+
981
+ if (_belongsTo(element, self.target)) {
982
+ return false;
983
+ }
984
+
985
+ // stop for input, select, and textarea
986
+ return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
987
+ };
988
+
989
+ /**
990
+ * exposes _handleKey publicly so it can be overwritten by extensions
991
+ */
992
+ Mousetrap.prototype.handleKey = function() {
993
+ var self = this;
994
+ return self._handleKey.apply(self, arguments);
995
+ };
996
+
997
+ /**
998
+ * allow custom key mappings
999
+ */
1000
+ Mousetrap.addKeycodes = function(object) {
1001
+ for (var key in object) {
1002
+ if (object.hasOwnProperty(key)) {
1003
+ _MAP[key] = object[key];
1004
+ }
1005
+ }
1006
+ _REVERSE_MAP = null;
1007
+ };
1008
+
1009
+ /**
1010
+ * Init the global mousetrap functions
1011
+ *
1012
+ * This method is needed to allow the global mousetrap functions to work
1013
+ * now that mousetrap is a constructor function.
1014
+ */
1015
+ Mousetrap.init = function() {
1016
+ var documentMousetrap = Mousetrap(document);
1017
+ for (var method in documentMousetrap) {
1018
+ if (method.charAt(0) !== '_') {
1019
+ Mousetrap[method] = (function(method) {
1020
+ return function() {
1021
+ return documentMousetrap[method].apply(documentMousetrap, arguments);
1022
+ };
1023
+ } (method));
1024
+ }
1025
+ }
1026
+ };
1027
+
1028
+ Mousetrap.init();
1029
+
1030
+ // expose mousetrap to the global object
1031
+ window.Mousetrap = Mousetrap;
1032
+
1033
+ // expose as a common js module
1034
+ if (typeof module !== 'undefined' && module.exports) {
1035
+ module.exports = Mousetrap;
1036
+ }
1037
+
1038
+ // expose mousetrap as an AMD module
1039
+ if (typeof define === 'function' && define.amd) {
1040
+ define(function() {
1041
+ return Mousetrap;
1042
+ });
1043
+ }
1044
+ }) (typeof window !== 'undefined' ? window : null, typeof window !== 'undefined' ? document : null);