lit 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);