diff --git a/app/assets/javascripts/discourse-shims.js b/app/assets/javascripts/discourse-shims.js
index e93da4e080a..006a98ed696 100644
--- a/app/assets/javascripts/discourse-shims.js
+++ b/app/assets/javascripts/discourse-shims.js
@@ -2,11 +2,6 @@ define("message-bus-client", ["exports"], function (__exports__) {
__exports__.default = window.MessageBus;
});
-define("mousetrap-global-bind", ["exports"], function (__exports__) {
- // In the Rails app it's applied from the vendored file
- __exports__.default = {};
-});
-
define("ember-buffered-proxy/proxy", ["exports"], function (__exports__) {
__exports__.default = window.BufferedProxy;
});
@@ -19,8 +14,8 @@ define("xss", ["exports"], function (__exports__) {
__exports__.default = window.filterXSS;
});
-define("mousetrap", ["exports"], function (__exports__) {
- __exports__.default = window.Mousetrap;
+define("@discourse/itsatrap", ["exports"], function (__exports__) {
+ __exports__.default = window.ItsATrap;
});
define("@popperjs/core", ["exports"], function (__exports__) {
diff --git a/app/assets/javascripts/discourse/app/app.js b/app/assets/javascripts/discourse/app/app.js
index ddcc134df98..8d0aa0b14f3 100644
--- a/app/assets/javascripts/discourse/app/app.js
+++ b/app/assets/javascripts/discourse/app/app.js
@@ -1,5 +1,4 @@
import Application from "@ember/application";
-import Mousetrap from "mousetrap";
import { buildResolver } from "discourse-common/resolver";
import { isTesting } from "discourse-common/config/environment";
@@ -13,11 +12,6 @@ const Discourse = Application.extend({
paste: "paste",
},
- reset() {
- this._super(...arguments);
- Mousetrap.reset();
- },
-
Resolver: buildResolver("discourse"),
_prepareInitializer(moduleName) {
diff --git a/app/assets/javascripts/discourse/app/components/bookmark.js b/app/assets/javascripts/discourse/app/components/bookmark.js
index d51cb7baa98..25157f2eaca 100644
--- a/app/assets/javascripts/discourse/app/components/bookmark.js
+++ b/app/assets/javascripts/discourse/app/components/bookmark.js
@@ -11,7 +11,7 @@ import { AUTO_DELETE_PREFERENCES } from "discourse/models/bookmark";
import Component from "@ember/component";
import I18n from "I18n";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
-import Mousetrap from "mousetrap";
+import ItsATrap from "@discourse/itsatrap";
import { Promise } from "rsvp";
import { TIME_SHORTCUT_TYPES } from "discourse/lib/time-shortcut";
import { action } from "@ember/object";
@@ -37,6 +37,7 @@ export default Component.extend({
_savingBookmarkManually: null,
_saving: null,
_deleting: null,
+ _itsatrap: null,
postDetectedLocalDate: null,
postDetectedLocalTime: null,
postDetectedLocalTimezone: null,
@@ -44,7 +45,6 @@ export default Component.extend({
userTimezone: null,
showOptions: null,
model: null,
-
afterSave: null,
@on("init")
@@ -62,6 +62,7 @@ export default Component.extend({
prefilledDatetime: null,
userTimezone: this.currentUser.resolvedTimezone(this.currentUser),
showOptions: false,
+ _itsatrap: new ItsATrap(),
});
this.registerOnCloseHandler(this._onModalClose.bind(this));
@@ -123,9 +124,8 @@ export default Component.extend({
_bindKeyboardShortcuts() {
KeyboardShortcuts.pause();
- this._mousetrap = new Mousetrap();
Object.keys(BOOKMARK_BINDINGS).forEach((shortcut) => {
- this._mousetrap.bind(shortcut, () => {
+ this._itsatrap.bind(shortcut, () => {
let binding = BOOKMARK_BINDINGS[shortcut];
this.send(binding.handler);
return false;
@@ -266,7 +266,9 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
- this._mousetrap.reset();
+
+ this._itsatrap?.destroy();
+ this.set("_itsatrap", null);
KeyboardShortcuts.unpause();
},
diff --git a/app/assets/javascripts/discourse/app/components/d-editor.js b/app/assets/javascripts/discourse/app/components/d-editor.js
index 1d337cb0ba9..09b90e5246e 100644
--- a/app/assets/javascripts/discourse/app/components/d-editor.js
+++ b/app/assets/javascripts/discourse/app/components/d-editor.js
@@ -9,7 +9,7 @@ import { emojiUrlFor, generateCookFunction } from "discourse/lib/text";
import { later, schedule, scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import I18n from "I18n";
-import Mousetrap from "mousetrap";
+import ItsATrap from "@discourse/itsatrap";
import { Promise } from "rsvp";
import { SKIP } from "discourse/lib/autocomplete";
import { categoryHashtagTriggerRule } from "discourse/lib/category-hashtags";
@@ -238,7 +238,7 @@ export default Component.extend(TextareaTextManipulation, {
classNames: ["d-editor"],
ready: false,
lastSel: null,
- _mouseTrap: null,
+ _itsatrap: null,
showLink: true,
emojiPickerIsActive: false,
emojiStore: service("emoji-store"),
@@ -278,12 +278,12 @@ export default Component.extend(TextareaTextManipulation, {
scheduleOnce("afterRender", this, this._readyNow);
- this._mouseTrap = new Mousetrap(this._textarea);
+ this._itsatrap = new ItsATrap(this._textarea);
const shortcuts = this.get("toolbar.shortcuts");
Object.keys(shortcuts).forEach((sc) => {
const button = shortcuts[sc];
- this._mouseTrap.bind(sc, () => {
+ this._itsatrap.bind(sc, () => {
button.action(button);
return false;
});
@@ -335,7 +335,9 @@ export default Component.extend(TextareaTextManipulation, {
this.appEvents.off("composer:replace-text", this, "_replaceText");
}
- this._mouseTrap.reset();
+ this._itsatrap?.destroy();
+ this._itsatrap = null;
+
$(this.element.querySelector(".d-editor-preview")).off("click.preview");
if (isTesting()) {
diff --git a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js
index 65ab8b6b92c..1c07aceb5c1 100644
--- a/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js
+++ b/app/assets/javascripts/discourse/app/components/edit-topic-timer-form.js
@@ -21,7 +21,7 @@ import {
thisWeekend,
} from "discourse/lib/time-utils";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
-import Mousetrap from "mousetrap";
+import ItsATrap from "@discourse/itsatrap";
export default Component.extend({
statusType: readOnly("topicTimer.status_type"),
@@ -43,12 +43,13 @@ export default Component.extend({
"autoCloseAfterLastPost"
),
duration: null,
+ _itsatrap: null,
init() {
this._super(...arguments);
KeyboardShortcuts.pause();
- this._mousetrap = new Mousetrap();
+ this.set("_itsatrap", new ItsATrap());
this.set("duration", this.initialDuration);
},
@@ -65,7 +66,9 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
- this._mousetrap.reset();
+
+ this._itsatrap.destroy();
+ this.set("_itsatrap", null);
KeyboardShortcuts.unpause();
},
diff --git a/app/assets/javascripts/discourse/app/components/site-header.js b/app/assets/javascripts/discourse/app/components/site-header.js
index 9ac49c9cfec..d78e9c7d49b 100644
--- a/app/assets/javascripts/discourse/app/components/site-header.js
+++ b/app/assets/javascripts/discourse/app/components/site-header.js
@@ -5,7 +5,7 @@ import PanEvents, {
import { cancel, later, schedule } from "@ember/runloop";
import Docking from "discourse/mixins/docking";
import MountWidget from "discourse/components/mount-widget";
-import Mousetrap from "mousetrap";
+import ItsATrap from "@discourse/itsatrap";
import RerenderOnDoNotDisturbChange from "discourse/mixins/rerender-on-do-not-disturb-change";
import { observes } from "discourse-common/utils/decorators";
import { topicTitleDecorators } from "discourse/components/topic-title";
@@ -24,7 +24,7 @@ const SiteHeaderComponent = MountWidget.extend(
_panMenuOffset: 0,
_scheduledRemoveAnimate: null,
_topic: null,
- _mousetrap: null,
+ _itsatrap: null,
@observes(
"currentUser.unread_notifications",
@@ -258,8 +258,8 @@ const SiteHeaderComponent = MountWidget.extend(
}
const header = document.querySelector("header.d-header");
- this._mousetrap = new Mousetrap(header);
- this._mousetrap.bind(["right", "left"], (e) => {
+ this._itsatrap = new ItsATrap(header);
+ this._itsatrap.bind(["right", "left"], (e) => {
const activeTab = document.querySelector(".glyphs .menu-link.active");
if (activeTab) {
@@ -294,7 +294,8 @@ const SiteHeaderComponent = MountWidget.extend(
cancel(this._scheduledRemoveAnimate);
- this._mousetrap.reset();
+ this._itsatrap?.destroy();
+ this._itsatrap = null;
document.removeEventListener("click", this._dismissFirstNotification);
},
diff --git a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js
index 7733cbab85c..62869a39dd6 100644
--- a/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js
+++ b/app/assets/javascripts/discourse/app/components/time-shortcut-picker.js
@@ -67,6 +67,8 @@ export default Component.extend({
customDate: null,
customTime: null,
+ _itsatrap: null,
+
defaultCustomReminderTime: `0${START_OF_DAY_HOUR}:00`,
@on("init")
@@ -101,7 +103,8 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
- this.mousetrap.unbind(Object.keys(BINDINGS));
+
+ this._itsatrap.unbind(Object.keys(BINDINGS));
},
parsePrefilledDatetime() {
@@ -143,7 +146,7 @@ export default Component.extend({
_bindKeyboardShortcuts() {
Object.keys(BINDINGS).forEach((shortcut) => {
- this.mousetrap.bind(shortcut, () => {
+ this._itsatrap.bind(shortcut, () => {
let binding = BINDINGS[shortcut];
this.send(binding.handler, ...binding.args);
return false;
diff --git a/app/assets/javascripts/discourse/app/initializers/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/initializers/keyboard-shortcuts.js
index b5dda8a2378..b484497e320 100644
--- a/app/assets/javascripts/discourse/app/initializers/keyboard-shortcuts.js
+++ b/app/assets/javascripts/discourse/app/initializers/keyboard-shortcuts.js
@@ -1,15 +1,11 @@
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
-import Mousetrap from "mousetrap";
-import bindGlobal from "mousetrap-global-bind";
+import ItsATrap from "@discourse/itsatrap";
export default {
name: "keyboard-shortcuts",
initialize(container) {
- // Ensure mousetrap-global-bind is executed
- void bindGlobal;
-
- KeyboardShortcuts.init(Mousetrap, container);
+ KeyboardShortcuts.init(ItsATrap, container);
KeyboardShortcuts.bindEvents();
},
diff --git a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js
index 4bc5223fdfb..8355a935ca7 100644
--- a/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js
+++ b/app/assets/javascripts/discourse/app/lib/keyboard-shortcuts.js
@@ -122,10 +122,8 @@ export default {
},
teardown() {
- if (this.keyTrapper) {
- this.keyTrapper.reset();
- this.keyTrapper = null;
- }
+ this.keyTrapper?.destroy();
+ this.keyTrapper = null;
this.container = null;
},
@@ -207,7 +205,7 @@ export default {
**/
addShortcut(shortcut, callback, opts = {}) {
// we trim but leave whitespace between characters, as shortcuts
- // like `z z` are valid for Mousetrap
+ // like `z z` are valid for ItsATrap
shortcut = shortcut.trim();
let newBinding = Object.assign({ handler: callback }, opts);
this.bindKey(shortcut, newBinding);
diff --git a/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs
index 9882daa5b35..00119efd9dc 100644
--- a/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/bookmark.hbs
@@ -40,7 +40,7 @@
onTimeSelected=(action "onTimeSelected")
customOptions=customTimeShortcutOptions
additionalOptionsToShow=additionalTimeShortcutOptions
- mousetrap=_mousetrap
+ _itsatrap=_itsatrap
}}
{{else}}
{{html-safe (i18n "bookmarks.no_timezone" basePath=(base-path))}}
diff --git a/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs
index f2b4aca37cd..c960e1a3a1f 100644
--- a/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/edit-topic-timer-form.hbs
@@ -24,7 +24,7 @@
onTimeSelected=onTimeSelected
customOptions=customTimeShortcutOptions
hiddenOptions=hiddenTimeShortcutOptions
- mousetrap=_mousetrap
+ _itsatrap=_itsatrap
}}
{{/if}}
{{#if useDuration}}
diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json
index 5d8808047f4..d6128cac445 100644
--- a/app/assets/javascripts/discourse/package.json
+++ b/app/assets/javascripts/discourse/package.json
@@ -53,8 +53,6 @@
"loader.js": "^4.7.0",
"message-bus-client": "^3.3.0",
"messageformat": "0.1.5",
- "mousetrap": "^1.6.5",
- "mousetrap-global-bind": "^1.1.0",
"pretender": "^3.4.7",
"pretty-text": "^1.0.0",
"qunit": "^2.14.0",
@@ -62,7 +60,8 @@
"sass": "^1.32.8",
"select-kit": "^1.0.0",
"sinon": "^9.2.0",
- "virtual-dom": "^2.1.1"
+ "virtual-dom": "^2.1.1",
+ "@discourse/itsatrap": "^2.0.10"
},
"engines": {
"node": ">= 12.*",
diff --git a/app/assets/javascripts/discourse/tests/theme_qunit_vendor.js b/app/assets/javascripts/discourse/tests/theme_qunit_vendor.js
index 08f31dee8e2..58e2674ef5e 100644
--- a/app/assets/javascripts/discourse/tests/theme_qunit_vendor.js
+++ b/app/assets/javascripts/discourse/tests/theme_qunit_vendor.js
@@ -25,8 +25,7 @@
//= require jquery.tagsinput.js
//= require jquery.sortable.js
//= require lodash.js
-//= require mousetrap.js
-//= require mousetrap-global-bind.js
+//= require itsatrap.js
//= require rsvp.js
//= require show-html.js
//= require uppy.js
diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js
index 1e0b0c6d1cf..f6ce6c790bd 100644
--- a/app/assets/javascripts/vendor.js
+++ b/app/assets/javascripts/vendor.js
@@ -18,8 +18,7 @@
//= require jquery.tagsinput.js
//= require jquery.sortable.js
//= require lodash.js
-//= require mousetrap.js
-//= require mousetrap-global-bind.js
+//= require itsatrap.js
//= require rsvp.js
//= require show-html.js
//= require uppy.js
diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock
index ffb5e3c1e14..e18fca6d24f 100644
--- a/app/assets/javascripts/yarn.lock
+++ b/app/assets/javascripts/yarn.lock
@@ -928,6 +928,11 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
+"@discourse/itsatrap@^2.0.10":
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/@discourse/itsatrap/-/itsatrap-2.0.10.tgz#c7e750eeb32b54e769e952c4ecc472213eb1385a"
+ integrity sha512-Jn1gdiyHMGUsmUfLFf4Q7VnTAv0l7NePbegU6pKhKHEmbzV3FosGxq30fTOYgVyTS1bxqGjlA6LvQttJpv3ROw==
+
"@ember-data/rfc395-data@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@ember-data/rfc395-data/-/rfc395-data-0.0.4.tgz#ecb86efdf5d7733a76ff14ea651a1b0ed1f8a843"
@@ -8586,16 +8591,6 @@ morgan@^1.10.0:
on-finished "~2.3.0"
on-headers "~1.0.2"
-mousetrap-global-bind@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz#cd7de9222bd0646fa2e010d54c84a74c26a88edd"
- integrity sha1-zX3pIivQZG+i4BDVTISnTCaojt0=
-
-mousetrap@^1.6.5:
- version "1.6.5"
- resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
- integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==
-
mout@^1.0.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/mout/-/mout-1.2.2.tgz#c9b718a499806a0632cede178e80f436259e777d"
diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake
index 2914940a788..c876449e692 100644
--- a/lib/tasks/javascript.rake
+++ b/lib/tasks/javascript.rake
@@ -123,7 +123,7 @@ def dependencies
}, {
source: 'markdown-it/dist/markdown-it.js'
}, {
- source: 'mousetrap/mousetrap.js'
+ source: '@discourse/itsatrap/itsatrap.js'
}, {
source: 'moment/moment.js'
}, {
@@ -138,8 +138,6 @@ def dependencies
}, {
source: 'moment-timezone-names-translations/locales/.',
destination: 'moment-timezone-names-locale'
- }, {
- source: 'mousetrap/plugins/global-bind/mousetrap-global-bind.js'
}, {
source: 'resumablejs/resumable.js'
}, {
diff --git a/package.json b/package.json
index 64fb5f33264..18021b9d5b9 100644
--- a/package.json
+++ b/package.json
@@ -10,9 +10,9 @@
"@highlightjs/cdn-assets": "^10.6.0",
"@json-editor/json-editor": "^2.5.2",
"@popperjs/core": "v2.9.3",
- "@uppy/core": "^2.0.1",
"@uppy/aws-s3": "^2.0.1",
"@uppy/aws-s3-multipart": "^2.0.2",
+ "@uppy/core": "^2.0.1",
"@uppy/drop-target": "^1.0.1",
"@uppy/xhr-upload": "^2.0.1",
"ace-builds": "1.4.12",
@@ -33,7 +33,6 @@
"moment": "2.29.1",
"moment-timezone": "0.5.31",
"moment-timezone-names-translations": "https://github.com/discourse/moment-timezone-names-translations",
- "mousetrap": "https://github.com/discourse/mousetrap#firefox-alt-key",
"pikaday": "1.8.0",
"resumablejs": "1.1.0",
"spectrum-colorpicker": "1.8.0",
@@ -42,7 +41,8 @@
"workbox-expiration": "^4.3.1",
"workbox-routing": "^4.3.1",
"workbox-strategies": "^4.3.1",
- "workbox-sw": "^4.3.1"
+ "workbox-sw": "^4.3.1",
+ "@discourse/itsatrap": "^2.0.10"
},
"devDependencies": {
"@arkweid/lefthook": "^0.7.2",
diff --git a/vendor/assets/javascripts/itsatrap.js b/vendor/assets/javascripts/itsatrap.js
new file mode 100644
index 00000000000..1616fb29e27
--- /dev/null
+++ b/vendor/assets/javascripts/itsatrap.js
@@ -0,0 +1,1159 @@
+/*global define:false */
+/**
+ * Copyright 2012-2017 Craig Campbell
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * ItsATrap is a simple keyboard shortcut library for Javascript with
+ * no external dependencies
+ *
+ * @version 2.0.1
+ * @url github.com/discourse/itsatrap
+ */
+(function(window, document, undefined) {
+ // Check if itsatrap is used inside browser, if not, return
+ if (!window) {
+ return;
+ }
+
+ /**
+ * mapping of special keycodes to their corresponding keys
+ *
+ * everything in this dictionary cannot use keypress events
+ * so it has to be here to map to the correct keycodes for
+ * keyup/keydown events
+ *
+ * @type {Object}
+ */
+ var _MAP = {
+ 8: "backspace",
+ 9: "tab",
+ 13: "enter",
+ 16: "shift",
+ 17: "ctrl",
+ 18: "alt",
+ 20: "capslock",
+ 27: "esc",
+ 32: "space",
+ 33: "pageup",
+ 34: "pagedown",
+ 35: "end",
+ 36: "home",
+ 37: "left",
+ 38: "up",
+ 39: "right",
+ 40: "down",
+ 45: "ins",
+ 46: "del",
+ 91: "meta",
+ 93: "meta",
+ 224: "meta"
+ };
+
+ /**
+ * mapping for special characters so they can support
+ *
+ * this dictionary is only used incase you want to bind a
+ * keyup or keydown event to one of these keys
+ *
+ * @type {Object}
+ */
+ var _KEYCODE_MAP = {
+ 106: "*",
+ 107: "+",
+ 109: "-",
+ 110: ".",
+ 111: "/",
+ 186: ";",
+ 187: "=",
+ 188: ",",
+ 189: "-",
+ 190: ".",
+ 191: "/",
+ 192: "`",
+ 219: "[",
+ 220: "\\",
+ 221: "]",
+ 222: "'"
+ };
+
+ /**
+ * this is a mapping of keys that require shift on a US keypad
+ * back to the non shift equivelents
+ *
+ * this is so you can use keyup events with these keys
+ *
+ * note that this will only work reliably on US keyboards
+ *
+ * @type {Object}
+ */
+ var _SHIFT_MAP = {
+ "~": "`",
+ "!": "1",
+ "@": "2",
+ "#": "3",
+ $: "4",
+ "%": "5",
+ "^": "6",
+ "&": "7",
+ "*": "8",
+ "(": "9",
+ ")": "0",
+ _: "-",
+ "+": "=",
+ ":": ";",
+ '"': "'",
+ "<": ",",
+ ">": ".",
+ "?": "/",
+ "|": "\\"
+ };
+
+ /**
+ * this is a list of special strings you can use to map
+ * to modifier keys when you specify your keyboard shortcuts
+ *
+ * @type {Object}
+ */
+ var _SPECIAL_ALIASES = {
+ option: "alt",
+ command: "meta",
+ return: "enter",
+ escape: "esc",
+ plus: "+",
+ mod: /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? "meta" : "ctrl"
+ };
+
+ /**
+ * variable to store the flipped version of _MAP from above
+ * needed to check if we should use keypress or not when no action
+ * is specified
+ *
+ * @type {Object|undefined}
+ */
+ var _REVERSE_MAP;
+
+ /**
+ * holds a reference to global bindings
+ *
+ * @type {Object|undefined}
+ */
+ var _globalCallbacks = {};
+
+ /**
+ * loop through the f keys, f1 to f19 and add them to the map
+ * programatically
+ */
+ for (var i = 1; i < 20; ++i) {
+ _MAP[111 + i] = "f" + i;
+ }
+
+ /**
+ * loop through to map numbers on the numeric keypad
+ */
+ for (i = 0; i <= 9; ++i) {
+ // This needs to use a string cause otherwise since 0 is falsey
+ // itsatrap will never fire for numpad 0 pressed as part of a keydown
+ // event.
+ //
+ // @see https://github.com/ccampbell/itsatrap/pull/258
+ _MAP[i + 96] = i.toString();
+ }
+
+ /**
+ * cross browser add event method
+ *
+ * @param {Element|HTMLDocument} object
+ * @param {string} type
+ * @param {EventListenerOrEventListenerObject} callback
+ * @returns void
+ */
+ function _addEvent(object, type, callback) {
+ if (object.addEventListener) {
+ object.addEventListener(type, callback, false);
+ return;
+ }
+
+ object.attachEvent("on" + type, callback);
+ }
+
+ /**
+ * cross browser remove event method
+ *
+ * @param {Element|HTMLDocument} object
+ * @param {string} type
+ * @param {EventListenerOrEventListenerObject} callback
+ * @returns void
+ */
+ function _removeEvent(object, type, callback) {
+ if (object.removeEventListener) {
+ object.removeEventListener(type, callback, false);
+ return;
+ }
+ object.detachEvent("on" + type, callback);
+ }
+
+ /**
+ * takes the event and returns the key character
+ *
+ * @param {Event} e
+ * @return {string}
+ */
+ function _characterFromEvent(e) {
+ // for keypress events we should return the character as is
+ if (e.type == "keypress") {
+ var character = String.fromCharCode(e.which);
+
+ // if the shift key is not pressed then it is safe to assume
+ // that we want the character to be lowercase. this means if
+ // you accidentally have caps lock on then your key bindings
+ // will continue to work
+ //
+ // the only side effect that might not be desired is if you
+ // bind something like 'A' cause you want to trigger an
+ // event when capital A is pressed caps lock will no longer
+ // trigger the event. shift+a will though.
+ if (!e.shiftKey) {
+ character = character.toLowerCase();
+ }
+
+ return character;
+ }
+
+ // for non keypress events the special maps are needed
+ if (_MAP[e.which]) {
+ return _MAP[e.which];
+ }
+
+ if (_KEYCODE_MAP[e.which]) {
+ return _KEYCODE_MAP[e.which];
+ }
+
+ // if it is not in the special map
+
+ // with keydown and keyup events the character seems to always
+ // come in as an uppercase character whether you are pressing shift
+ // or not. we should make sure it is always lowercase for comparisons
+ return String.fromCharCode(e.which).toLowerCase();
+ }
+
+ /**
+ * checks if two arrays are equal
+ *
+ * @param {Array} modifiers1
+ * @param {Array} modifiers2
+ * @returns {boolean}
+ */
+ function _modifiersMatch(modifiers1, modifiers2) {
+ return modifiers1.sort().join(",") === modifiers2.sort().join(",");
+ }
+
+ /**
+ * takes a key event and figures out what the modifiers are
+ *
+ * @param {Event} e
+ * @returns {Array}
+ */
+ function _eventModifiers(e) {
+ var modifiers = [];
+
+ if (e.shiftKey) {
+ modifiers.push("shift");
+ }
+
+ if (e.altKey) {
+ modifiers.push("alt");
+ }
+
+ if (e.ctrlKey) {
+ modifiers.push("ctrl");
+ }
+
+ if (e.metaKey) {
+ modifiers.push("meta");
+ }
+
+ return modifiers;
+ }
+
+ /**
+ * prevents default for this event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _preventDefault(e) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ return;
+ }
+
+ e.returnValue = false;
+ }
+
+ /**
+ * stops propogation for this event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _stopPropagation(e) {
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ return;
+ }
+
+ e.cancelBubble = true;
+ }
+
+ /**
+ * determines if the keycode specified is a modifier key or not
+ *
+ * @param {string} key
+ * @returns {boolean}
+ */
+ function _isModifier(key) {
+ return key == "shift" || key == "ctrl" || key == "alt" || key == "meta";
+ }
+
+ /**
+ * reverses the map lookup so that we can look for specific keys
+ * to see what can and can't use keypress
+ *
+ * @return {Object}
+ */
+ function _getReverseMap() {
+ if (!_REVERSE_MAP) {
+ _REVERSE_MAP = {};
+ for (var key in _MAP) {
+ // pull out the numeric keypad from here cause keypress should
+ // be able to detect the keys from the character
+ if (key > 95 && key < 112) {
+ continue;
+ }
+
+ if (_MAP.hasOwnProperty(key)) {
+ _REVERSE_MAP[_MAP[key]] = key;
+ }
+ }
+ }
+ return _REVERSE_MAP;
+ }
+
+ /**
+ * picks the best action based on the key combination
+ *
+ * @param {string} key - character for key
+ * @param {Array} modifiers
+ * @param {string=} action passed in
+ */
+ function _pickBestAction(key, modifiers, action) {
+ // if no action was picked in we should try to pick the one
+ // that we think would work best for this key
+ if (!action) {
+ action = _getReverseMap()[key] ? "keydown" : "keypress";
+ }
+
+ // modifier keys don't work as expected with keypress,
+ // switch to keydown
+ if (action == "keypress" && modifiers.length) {
+ action = "keydown";
+ }
+
+ return action;
+ }
+
+ /**
+ * Converts from a string key combination to an array
+ *
+ * @param {string} combination like "command+shift+l"
+ * @return {Array}
+ */
+ function _keysFromString(combination) {
+ if (combination === "+") {
+ return ["+"];
+ }
+
+ combination = combination.replace(/\+{2}/g, "+plus");
+ return combination.split("+");
+ }
+
+ /**
+ * Gets info for a specific key combination
+ *
+ * @param {string} combination key combination ("command+s" or "a" or "*")
+ * @param {string=} action
+ * @returns {Object}
+ */
+ function _getKeyInfo(combination, action) {
+ var keys;
+ var key;
+ var i;
+ var modifiers = [];
+
+ // take the keys from this pattern and figure out what the actual
+ // pattern is all about
+ keys = _keysFromString(combination);
+
+ for (i = 0; i < keys.length; ++i) {
+ key = keys[i];
+
+ // normalize key names
+ if (_SPECIAL_ALIASES[key]) {
+ key = _SPECIAL_ALIASES[key];
+ }
+
+ // if this is not a keypress event then we should
+ // be smart about using shift keys
+ // this will only work for US keyboards however
+ if (action && action != "keypress" && _SHIFT_MAP[key]) {
+ key = _SHIFT_MAP[key];
+ modifiers.push("shift");
+ }
+
+ // if this key is a modifier then add it to the list of modifiers
+ if (_isModifier(key)) {
+ modifiers.push(key);
+ }
+ }
+
+ // depending on what the key combination is
+ // we will try to pick the best event for it
+ action = _pickBestAction(key, modifiers, action);
+
+ return {
+ key: key,
+ modifiers: modifiers,
+ action: action
+ };
+ }
+
+ function _belongsTo(element, ancestor) {
+ if (element === null || element === document) {
+ return false;
+ }
+
+ if (element === ancestor) {
+ return true;
+ }
+
+ return _belongsTo(element.parentNode, ancestor);
+ }
+
+ function ItsATrap(targetElement) {
+ var self = this;
+
+ targetElement = targetElement || document;
+
+ if (!(self instanceof ItsATrap)) {
+ return new ItsATrap(targetElement);
+ }
+
+ /**
+ * element to attach key events to
+ *
+ * @type {Element}
+ */
+ self.target = targetElement;
+
+ /**
+ * a list of all the callbacks setup via ItsATrap.bind()
+ *
+ * @type {Object}
+ */
+ self._callbacks = {};
+
+ /**
+ * direct map of string combinations to callbacks used for trigger()
+ *
+ * @type {Object}
+ */
+ self._directMap = {};
+
+ /**
+ * keeps track of what level each sequence is at since multiple
+ * sequences can start out with the same sequence
+ *
+ * @type {Object}
+ */
+ var _sequenceLevels = {};
+
+ /**
+ * variable to store the setTimeout call
+ *
+ * @type {null|number}
+ */
+ var _resetTimer;
+
+ /**
+ * temporary state where we will ignore the next keyup
+ *
+ * @type {boolean|string}
+ */
+ var _ignoreNextKeyup = false;
+
+ /**
+ * temporary state where we will ignore the next keypress
+ *
+ * @type {boolean}
+ */
+ var _ignoreNextKeypress = false;
+
+ /**
+ * are we currently inside of a sequence?
+ * type of action ("keyup" or "keydown" or "keypress") or false
+ *
+ * @type {boolean|string}
+ */
+ var _nextExpectedAction = false;
+
+ /**
+ * resets all sequence counters except for the ones passed in
+ *
+ * @param {Object} doNotReset
+ * @returns void
+ */
+ function _resetSequences(doNotReset) {
+ doNotReset = doNotReset || {};
+
+ var activeSequences = false,
+ key;
+
+ for (key in _sequenceLevels) {
+ if (doNotReset[key]) {
+ activeSequences = true;
+ continue;
+ }
+ _sequenceLevels[key] = 0;
+ }
+
+ if (!activeSequences) {
+ _nextExpectedAction = false;
+ }
+ }
+
+ /**
+ * finds all callbacks that match based on the keycode, modifiers,
+ * and action
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event|Object} e
+ * @param {string=} sequenceName - name of the sequence we are looking for
+ * @param {string=} combination
+ * @param {number=} level
+ * @returns {Array}
+ */
+ function _getMatches(
+ character,
+ modifiers,
+ e,
+ sequenceName,
+ combination,
+ level
+ ) {
+ var i;
+ var callback;
+ var matches = [];
+ var action = e.type;
+
+ // if there are no events related to this keycode
+ if (!self._callbacks[character]) {
+ return [];
+ }
+
+ // if a modifier key is coming up on its own we should allow it
+ if (action == "keyup" && _isModifier(character)) {
+ modifiers = [character];
+ }
+
+ // loop through all callbacks for the key that was pressed
+ // and see if any of them match
+ for (i = 0; i < self._callbacks[character].length; ++i) {
+ callback = self._callbacks[character][i];
+
+ // if a sequence name is not specified, but this is a sequence at
+ // the wrong level then move onto the next match
+ if (
+ !sequenceName &&
+ callback.seq &&
+ _sequenceLevels[callback.seq] != callback.level
+ ) {
+ continue;
+ }
+
+ // if the action we are looking for doesn't match the action we got
+ // then we should keep going
+ if (action != callback.action) {
+ continue;
+ }
+
+ // if this is a keypress event and the meta key and control key
+ // are not pressed that means that we need to only look at the
+ // character, otherwise check the modifiers as well
+ //
+ // chrome will not fire a keypress if meta or control is down
+ // safari will fire a keypress if meta or meta+shift is down
+ // firefox will fire a keypress if meta or control is down
+ if (
+ (action == "keypress" && !e.metaKey && !e.ctrlKey) ||
+ _modifiersMatch(modifiers, callback.modifiers)
+ ) {
+ // when you bind a combination or sequence a second time it
+ // should overwrite the first one. if a sequenceName or
+ // combination is specified in this call it does just that
+ //
+ // @todo make deleting its own method?
+ var deleteCombo = !sequenceName && callback.combo == combination;
+ var deleteSequence =
+ sequenceName &&
+ callback.seq == sequenceName &&
+ callback.level == level;
+ if (deleteCombo || deleteSequence) {
+ self._callbacks[character].splice(i, 1);
+ }
+
+ matches.push(callback);
+ }
+ }
+
+ return matches;
+ }
+
+ /**
+ * actually calls the callback function
+ *
+ * if your callback function returns false this will use the jquery
+ * convention - prevent default and stop propogation on the event
+ *
+ * @param {Function} callback
+ * @param {Event} e
+ * @returns void
+ */
+ function _fireCallback(callback, e, combo, sequence) {
+ // if this event should not happen stop here
+ if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
+ return;
+ }
+
+ if (callback(e, combo) === false) {
+ _preventDefault(e);
+ _stopPropagation(e);
+ }
+ }
+
+ /**
+ * handles a character key event
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event} e
+ * @returns void
+ */
+ self._handleKey = function(character, modifiers, e) {
+ var callbacks = _getMatches(character, modifiers, e);
+ var i;
+ var doNotReset = {};
+ var maxLevel = 0;
+ var processedSequenceCallback = false;
+
+ // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
+ for (i = 0; i < callbacks.length; ++i) {
+ if (callbacks[i].seq) {
+ maxLevel = Math.max(maxLevel, callbacks[i].level);
+ }
+ }
+
+ // loop through matching callbacks for this key event
+ for (i = 0; i < callbacks.length; ++i) {
+ // fire for all sequence callbacks
+ // this is because if for example you have multiple sequences
+ // bound such as "g i" and "g t" they both need to fire the
+ // callback for matching g cause otherwise you can only ever
+ // match the first one
+ if (callbacks[i].seq) {
+ // only fire callbacks for the maxLevel to prevent
+ // subsequences from also firing
+ //
+ // for example 'a option b' should not cause 'option b' to fire
+ // even though 'option b' is part of the other sequence
+ //
+ // any sequences that do not match here will be discarded
+ // below by the _resetSequences call
+ if (callbacks[i].level != maxLevel) {
+ continue;
+ }
+
+ processedSequenceCallback = true;
+
+ // keep a list of which sequences were matches for later
+ doNotReset[callbacks[i].seq] = 1;
+ _fireCallback(
+ callbacks[i].callback,
+ e,
+ callbacks[i].combo,
+ callbacks[i].seq
+ );
+ continue;
+ }
+
+ // if there were no sequence matches but we are still here
+ // that means this is a regular match so we should fire that
+ if (!processedSequenceCallback) {
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
+ }
+ }
+
+ // if the key you pressed matches the type of sequence without
+ // being a modifier (ie "keyup" or "keypress") then we should
+ // reset all sequences that were not matched by this event
+ //
+ // this is so, for example, if you have the sequence "h a t" and you
+ // type "h e a r t" it does not match. in this case the "e" will
+ // cause the sequence to reset
+ //
+ // modifier keys are ignored because you can have a sequence
+ // that contains modifiers such as "enter ctrl+space" and in most
+ // cases the modifier key will be pressed before the next key
+ //
+ // also if you have a sequence such as "ctrl+b a" then pressing the
+ // "b" key will trigger a "keypress" and a "keydown"
+ //
+ // the "keydown" is expected when there is a modifier, but the
+ // "keypress" ends up matching the _nextExpectedAction since it occurs
+ // after and that causes the sequence to reset
+ //
+ // we ignore keypresses in a sequence that directly follow a keydown
+ // for the same character
+ var ignoreThisKeypress = e.type == "keypress" && _ignoreNextKeypress;
+ if (
+ e.type == _nextExpectedAction &&
+ !_isModifier(character) &&
+ !ignoreThisKeypress
+ ) {
+ _resetSequences(doNotReset);
+ }
+
+ _ignoreNextKeypress = processedSequenceCallback && e.type == "keydown";
+ };
+
+ /**
+ * handles a keydown event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ self._handleKeyEvent = function(e) {
+ // normalize e.which for key events
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
+ if (typeof e.which !== "number") {
+ e.which = e.keyCode;
+ }
+
+ var character = _characterFromEvent(e);
+
+ // no character found then stop
+ if (!character) {
+ return;
+ }
+
+ // need to use === for the character check because the character can be 0
+ if (e.type == "keyup" && _ignoreNextKeyup === character) {
+ _ignoreNextKeyup = false;
+ return;
+ }
+
+ self.handleKey(character, _eventModifiers(e), e);
+ };
+
+ /**
+ * called to set a 1 second timeout on the specified sequence
+ *
+ * this is so after each key press in the sequence you have 1 second
+ * to press the next key before you have to start over
+ *
+ * @returns void
+ */
+ function _resetSequenceTimer() {
+ clearTimeout(_resetTimer);
+ _resetTimer = setTimeout(_resetSequences, 1000);
+ }
+
+ /**
+ * binds a key sequence to an event
+ *
+ * @param {string} combo - combo specified in bind call
+ * @param {Array} keys
+ * @param {Function} callback
+ * @param {string=} action
+ * @returns void
+ */
+ function _bindSequence(combo, keys, callback, action) {
+ // start off by adding a sequence level record for this combination
+ // and setting the level to 0
+ _sequenceLevels[combo] = 0;
+
+ /**
+ * callback to increase the sequence level for this sequence and reset
+ * all other sequences that were active
+ *
+ * @param {string} nextAction
+ * @returns {Function}
+ */
+ function _increaseSequence(nextAction) {
+ return function() {
+ _nextExpectedAction = nextAction;
+ ++_sequenceLevels[combo];
+ _resetSequenceTimer();
+ };
+ }
+
+ /**
+ * wraps the specified callback inside of another function in order
+ * to reset all sequence counters as soon as this sequence is done
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _callbackAndReset(e) {
+ _fireCallback(callback, e, combo);
+
+ // we should ignore the next key up if the action is key down
+ // or keypress. this is so if you finish a sequence and
+ // release the key the final key will not trigger a keyup
+ if (action !== "keyup") {
+ _ignoreNextKeyup = _characterFromEvent(e);
+ }
+
+ // weird race condition if a sequence ends with the key
+ // another sequence begins with
+ setTimeout(_resetSequences, 10);
+ }
+
+ // loop through keys one at a time and bind the appropriate callback
+ // function. for any key leading up to the final one it should
+ // increase the sequence. after the final, it should reset all sequences
+ //
+ // if an action is specified in the original bind call then that will
+ // be used throughout. otherwise we will pass the action that the
+ // next key in the sequence should match. this allows a sequence
+ // to mix and match keypress and keydown events depending on which
+ // ones are better suited to the key provided
+ for (var i = 0; i < keys.length; ++i) {
+ var isFinal = i + 1 === keys.length;
+ var wrappedCallback = isFinal
+ ? _callbackAndReset
+ : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
+ _bindSingle(keys[i], wrappedCallback, action, combo, i);
+ }
+ }
+
+ /**
+ * binds a single keyboard combination
+ *
+ * @param {string} combination
+ * @param {Function} callback
+ * @param {string=} action
+ * @param {string=} sequenceName - name of sequence if part of sequence
+ * @param {number=} level - what part of the sequence the command is
+ * @returns void
+ */
+ function _bindSingle(combination, callback, action, sequenceName, level) {
+ // store a direct mapped reference for use with ItsATrap.trigger
+ self._directMap[combination + ":" + action] = callback;
+
+ // make sure multiple spaces in a row become a single space
+ combination = combination.replace(/\s+/g, " ");
+
+ var sequence = combination.split(" ");
+ var info;
+
+ // if this pattern is a sequence of keys then run through this method
+ // to reprocess each pattern one key at a time
+ if (sequence.length > 1) {
+ _bindSequence(combination, sequence, callback, action);
+ return;
+ }
+
+ info = _getKeyInfo(combination, action);
+
+ // make sure to initialize array if this is the first time
+ // a callback is added for this key
+ self._callbacks[info.key] = self._callbacks[info.key] || [];
+
+ // remove an existing match if there is one
+ _getMatches(
+ info.key,
+ info.modifiers,
+ { type: info.action },
+ sequenceName,
+ combination,
+ level
+ );
+
+ // add this call back to the array
+ // if it is a sequence put it at the beginning
+ // if not put it at the end
+ //
+ // this is important because the way these are processed expects
+ // the sequence ones to come first
+ self._callbacks[info.key][sequenceName ? "unshift" : "push"]({
+ callback: callback,
+ modifiers: info.modifiers,
+ action: info.action,
+ seq: sequenceName,
+ level: level,
+ combo: combination
+ });
+ }
+
+ /**
+ * binds multiple combinations to the same callback
+ *
+ * @param {Array} combinations
+ * @param {Function} callback
+ * @param {string|undefined} action
+ * @returns void
+ */
+ self._bindMultiple = function(combinations, callback, action) {
+ for (var i = 0; i < combinations.length; ++i) {
+ _bindSingle(combinations[i], callback, action);
+ }
+ };
+
+ // start!
+ _addEvent(targetElement, "keypress", self._handleKeyEvent);
+ _addEvent(targetElement, "keydown", self._handleKeyEvent);
+ _addEvent(targetElement, "keyup", self._handleKeyEvent);
+ }
+
+ /**
+ * binds an event to itsatrap
+ *
+ * can be a single key, a combination of keys separated with +,
+ * an array of keys, or a sequence of keys separated by spaces
+ *
+ * be sure to list the modifier keys first to make sure that the
+ * correct key ends up getting bound (the last key in the pattern)
+ *
+ * @param {string|Array} keys
+ * @param {Function} callback
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @returns void
+ */
+ ItsATrap.prototype.bind = function(keys, callback, action) {
+ var self = this;
+ keys = keys instanceof Array ? keys : [keys];
+ self._bindMultiple.call(self, keys, callback, action);
+ return self;
+ };
+
+ /**
+ * unbinds an event to itsatrap
+ *
+ * the unbinding sets the callback function of the specified key combo
+ * to an empty function and deletes the corresponding key in the
+ * _directMap dict.
+ *
+ * TODO: actually remove this from the _callbacks dictionary instead
+ * of binding an empty function
+ *
+ * the keycombo+action has to be exactly the same as
+ * it was defined in the bind method
+ *
+ * @param {string|Array} keys
+ * @param {string} action
+ * @returns void
+ */
+ ItsATrap.prototype.unbind = function(keys, action) {
+ var self = this;
+ return self.bind.call(self, keys, function() {}, action);
+ };
+
+ /**
+ * triggers an event that has already been bound
+ *
+ * @param {string} keys
+ * @param {string=} action
+ * @returns void
+ */
+ ItsATrap.prototype.trigger = function(keys, action) {
+ var self = this;
+ if (self._directMap[keys + ":" + action]) {
+ self._directMap[keys + ":" + action]({}, keys);
+ }
+ return self;
+ };
+
+ /**
+ * resets the library back to its initial state. this is useful
+ * if you want to clear out the current keyboard shortcuts and bind
+ * new ones - for example if you switch to another page
+ *
+ * @returns void
+ */
+ ItsATrap.prototype.reset = function() {
+ var self = this;
+ self._callbacks = {};
+ self._directMap = {};
+ return self;
+ };
+
+ /**
+ * destroy itsatrap object
+ *
+ * - call reset on the itsatrap object ( removing all binding )
+ * - remove all javascript event listener from target element or document
+ * - remove all reference to target
+ *
+ * @return void
+ */
+
+ ItsATrap.prototype.destroy = function() {
+ var self = this;
+
+ self.reset();
+
+ _removeEvent(self.target, "keypress", self._handleKeyEvent);
+ _removeEvent(self.target, "keydown", self._handleKeyEvent);
+ _removeEvent(self.target, "keyup", self._handleKeyEvent);
+
+ self.target = undefined;
+ self._handleKeyEvent = undefined;
+ };
+
+ /**
+ * should we stop this event before firing off callbacks
+ *
+ * @param {Event} e
+ * @param {Element} element
+ * @return {boolean}
+ */
+ ItsATrap.prototype.stopCallback = function(e, element, combo, sequence) {
+ var self = this;
+
+ if (self.paused) {
+ return true;
+ }
+
+ if (_globalCallbacks[combo] || _globalCallbacks[sequence]) {
+ return false;
+ }
+
+ // if the element has the class "itsatrap" then no need to stop
+ if ((" " + element.className + " ").indexOf(" itsatrap ") > -1) {
+ return false;
+ }
+
+ if (_belongsTo(element, self.target)) {
+ return false;
+ }
+
+ // Events originating from a shadow DOM are re-targetted and `e.target` is the shadow host,
+ // not the initial event target in the shadow tree. Note that not all events cross the
+ // shadow boundary.
+ // For shadow trees with `mode: 'open'`, the initial event target is the first element in
+ // the event’s composed path. For shadow trees with `mode: 'closed'`, the initial event
+ // target cannot be obtained.
+ if ("composedPath" in e && typeof e.composedPath === "function") {
+ // For open shadow trees, update `element` so that the following check works.
+ var initialEventTarget = e.composedPath()[0];
+ if (initialEventTarget !== e.target) {
+ element = initialEventTarget;
+ }
+ }
+
+ // stop for input, select, and textarea
+ return (
+ element.tagName == "INPUT" ||
+ element.tagName == "SELECT" ||
+ element.tagName == "TEXTAREA" ||
+ element.isContentEditable
+ );
+ };
+
+ /**
+ * exposes _handleKey publicly so it can be overwritten by extensions
+ */
+ ItsATrap.prototype.handleKey = function() {
+ var self = this;
+ return self._handleKey.apply(self, arguments);
+ };
+
+ /**
+ * allow custom key mappings
+ */
+ ItsATrap.addKeycodes = function(object) {
+ for (var key in object) {
+ if (object.hasOwnProperty(key)) {
+ _MAP[key] = object[key];
+ }
+ }
+ _REVERSE_MAP = null;
+ };
+
+ /**
+ * adds a pause and unpause method to ItsATrap
+ * this allows you to enable or disable keyboard shortcuts
+ * without having to reset ItsATrap and rebind everything
+ */
+ ItsATrap.prototype.pause = function() {
+ var self = this;
+ self.paused = true;
+ };
+
+ ItsATrap.prototype.unpause = function() {
+ var self = this;
+ self.paused = false;
+ };
+
+ /**
+ * adds a bindGlobal method to ItsATrap that allows you to
+ * bind specific keyboard shortcuts that will still work
+ * inside a text input field
+ *
+ * usage:
+ * ItsATrap.bindGlobal('ctrl+s', _saveChanges);
+ */
+ ItsATrap.prototype.bindGlobal = function(keys, callback, action) {
+ var self = this;
+ self.bind(keys, callback, action);
+
+ if (keys instanceof Array) {
+ for (var i = 0; i < keys.length; i++) {
+ _globalCallbacks[keys[i]] = true;
+ }
+ return;
+ }
+
+ _globalCallbacks[keys] = true;
+ };
+
+ // expose itsatrap to the global object
+ window.ItsATrap = ItsATrap;
+
+ // expose as a common js module
+ if (typeof module !== "undefined" && module.exports) {
+ module.exports = ItsATrap;
+ }
+
+ // expose itsatrap as an AMD module
+ if (typeof define === "function" && define.amd) {
+ define(function() {
+ return ItsATrap;
+ });
+ }
+})(
+ typeof window !== "undefined" ? window : null,
+ typeof window !== "undefined" ? document : null
+);
diff --git a/vendor/assets/javascripts/mousetrap-global-bind.js b/vendor/assets/javascripts/mousetrap-global-bind.js
deleted file mode 100644
index 6b86a99fbfa..00000000000
--- a/vendor/assets/javascripts/mousetrap-global-bind.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * adds a bindGlobal method to Mousetrap that allows you to
- * bind specific keyboard shortcuts that will still work
- * inside a text input field
- *
- * usage:
- * Mousetrap.bindGlobal('ctrl+s', _saveChanges);
- */
-/* global Mousetrap:true */
-(function(Mousetrap) {
- if (! Mousetrap) {
- return;
- }
- var _globalCallbacks = {};
- var _originalStopCallback = Mousetrap.prototype.stopCallback;
-
- Mousetrap.prototype.stopCallback = function(e, element, combo, sequence) {
- var self = this;
-
- if (self.paused) {
- return true;
- }
-
- if (_globalCallbacks[combo] || _globalCallbacks[sequence]) {
- return false;
- }
-
- return _originalStopCallback.call(self, e, element, combo);
- };
-
- Mousetrap.prototype.bindGlobal = function(keys, callback, action) {
- var self = this;
- self.bind(keys, callback, action);
-
- if (keys instanceof Array) {
- for (var i = 0; i < keys.length; i++) {
- _globalCallbacks[keys[i]] = true;
- }
- return;
- }
-
- _globalCallbacks[keys] = true;
- };
-
- Mousetrap.init();
-}) (typeof Mousetrap !== "undefined" ? Mousetrap : undefined);
diff --git a/vendor/assets/javascripts/mousetrap.js b/vendor/assets/javascripts/mousetrap.js
deleted file mode 100644
index bd469fcea87..00000000000
--- a/vendor/assets/javascripts/mousetrap.js
+++ /dev/null
@@ -1,1058 +0,0 @@
-/*global define:false */
-/**
- * Copyright 2012-2017 Craig Campbell
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * Mousetrap is a simple keyboard shortcut library for Javascript with
- * no external dependencies
- *
- * @version 1.6.5
- * @url craig.is/killing/mice
- */
-(function(window, document, undefined) {
-
- // Check if mousetrap is used inside browser, if not, return
- if (!window) {
- return;
- }
-
- /**
- * mapping of special keycodes to their corresponding keys
- *
- * everything in this dictionary cannot use keypress events
- * so it has to be here to map to the correct keycodes for
- * keyup/keydown events
- *
- * @type {Object}
- */
- var _MAP = {
- 8: 'backspace',
- 9: 'tab',
- 13: 'enter',
- 16: 'shift',
- 17: 'ctrl',
- 18: 'alt',
- 20: 'capslock',
- 27: 'esc',
- 32: 'space',
- 33: 'pageup',
- 34: 'pagedown',
- 35: 'end',
- 36: 'home',
- 37: 'left',
- 38: 'up',
- 39: 'right',
- 40: 'down',
- 45: 'ins',
- 46: 'del',
- 91: 'meta',
- 93: 'meta',
- 224: 'meta'
- };
-
- /**
- * mapping for special characters so they can support
- *
- * this dictionary is only used incase you want to bind a
- * keyup or keydown event to one of these keys
- *
- * @type {Object}
- */
- var _KEYCODE_MAP = {
- 106: '*',
- 107: '+',
- 109: '-',
- 110: '.',
- 111 : '/',
- 186: ';',
- 187: '=',
- 188: ',',
- 189: '-',
- 190: '.',
- 191: '/',
- 192: '`',
- 219: '[',
- 220: '\\',
- 221: ']',
- 222: '\''
- };
-
- /**
- * this is a mapping of keys that require shift on a US keypad
- * back to the non shift equivelents
- *
- * this is so you can use keyup events with these keys
- *
- * note that this will only work reliably on US keyboards
- *
- * @type {Object}
- */
- var _SHIFT_MAP = {
- '~': '`',
- '!': '1',
- '@': '2',
- '#': '3',
- '$': '4',
- '%': '5',
- '^': '6',
- '&': '7',
- '*': '8',
- '(': '9',
- ')': '0',
- '_': '-',
- '+': '=',
- ':': ';',
- '\"': '\'',
- '<': ',',
- '>': '.',
- '?': '/',
- '|': '\\'
- };
-
- /**
- * this is a list of special strings you can use to map
- * to modifier keys when you specify your keyboard shortcuts
- *
- * @type {Object}
- */
- var _SPECIAL_ALIASES = {
- 'option': 'alt',
- 'command': 'meta',
- 'return': 'enter',
- 'escape': 'esc',
- 'plus': '+',
- 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
- };
-
- /**
- * variable to store the flipped version of _MAP from above
- * needed to check if we should use keypress or not when no action
- * is specified
- *
- * @type {Object|undefined}
- */
- var _REVERSE_MAP;
-
- /**
- * loop through the f keys, f1 to f19 and add them to the map
- * programatically
- */
- for (var i = 1; i < 20; ++i) {
- _MAP[111 + i] = 'f' + i;
- }
-
- /**
- * loop through to map numbers on the numeric keypad
- */
- for (i = 0; i <= 9; ++i) {
-
- // This needs to use a string cause otherwise since 0 is falsey
- // mousetrap will never fire for numpad 0 pressed as part of a keydown
- // event.
- //
- // @see https://github.com/ccampbell/mousetrap/pull/258
- _MAP[i + 96] = i.toString();
- }
-
- /**
- * cross browser add event method
- *
- * @param {Element|HTMLDocument} object
- * @param {string} type
- * @param {Function} callback
- * @returns void
- */
- function _addEvent(object, type, callback) {
- if (object.addEventListener) {
- object.addEventListener(type, callback, false);
- return;
- }
-
- object.attachEvent('on' + type, callback);
- }
-
- /**
- * takes the event and returns the key character
- *
- * @param {Event} e
- * @return {string}
- */
- function _characterFromEvent(e) {
-
- // for keypress events we should return the character as is
- if (e.type == 'keypress') {
- var character = String.fromCharCode(e.which);
-
- // if the shift key is not pressed then it is safe to assume
- // that we want the character to be lowercase. this means if
- // you accidentally have caps lock on then your key bindings
- // will continue to work
- //
- // the only side effect that might not be desired is if you
- // bind something like 'A' cause you want to trigger an
- // event when capital A is pressed caps lock will no longer
- // trigger the event. shift+a will though.
- if (!e.shiftKey) {
- character = character.toLowerCase();
- }
-
- return character;
- }
-
- // for non keypress events the special maps are needed
- if (_MAP[e.which]) {
- return _MAP[e.which];
- }
-
- if (_KEYCODE_MAP[e.which]) {
- return _KEYCODE_MAP[e.which];
- }
-
- // if it is not in the special map
-
- // with keydown and keyup events the character seems to always
- // come in as an uppercase character whether you are pressing shift
- // or not. we should make sure it is always lowercase for comparisons
- return String.fromCharCode(e.which).toLowerCase();
- }
-
- /**
- * checks if two arrays are equal
- *
- * @param {Array} modifiers1
- * @param {Array} modifiers2
- * @returns {boolean}
- */
- function _modifiersMatch(modifiers1, modifiers2) {
- return modifiers1.sort().join(',') === modifiers2.sort().join(',');
- }
-
- /**
- * takes a key event and figures out what the modifiers are
- *
- * @param {Event} e
- * @returns {Array}
- */
- function _eventModifiers(e) {
- var modifiers = [];
-
- if (e.shiftKey) {
- modifiers.push('shift');
- }
-
- if (e.altKey) {
- modifiers.push('alt');
- }
-
- if (e.ctrlKey) {
- modifiers.push('ctrl');
- }
-
- if (e.metaKey) {
- modifiers.push('meta');
- }
-
- return modifiers;
- }
-
- /**
- * prevents default for this event
- *
- * @param {Event} e
- * @returns void
- */
- function _preventDefault(e) {
- if (e.preventDefault) {
- e.preventDefault();
- return;
- }
-
- e.returnValue = false;
- }
-
- /**
- * stops propogation for this event
- *
- * @param {Event} e
- * @returns void
- */
- function _stopPropagation(e) {
- if (e.stopPropagation) {
- e.stopPropagation();
- return;
- }
-
- e.cancelBubble = true;
- }
-
- /**
- * determines if the keycode specified is a modifier key or not
- *
- * @param {string} key
- * @returns {boolean}
- */
- function _isModifier(key) {
- return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
- }
-
- /**
- * reverses the map lookup so that we can look for specific keys
- * to see what can and can't use keypress
- *
- * @return {Object}
- */
- function _getReverseMap() {
- if (!_REVERSE_MAP) {
- _REVERSE_MAP = {};
- for (var key in _MAP) {
-
- // pull out the numeric keypad from here cause keypress should
- // be able to detect the keys from the character
- if (key > 95 && key < 112) {
- continue;
- }
-
- if (_MAP.hasOwnProperty(key)) {
- _REVERSE_MAP[_MAP[key]] = key;
- }
- }
- }
- return _REVERSE_MAP;
- }
-
- /**
- * picks the best action based on the key combination
- *
- * @param {string} key - character for key
- * @param {Array} modifiers
- * @param {string=} action passed in
- */
- function _pickBestAction(key, modifiers, action) {
-
- // if no action was picked in we should try to pick the one
- // that we think would work best for this key
- if (!action) {
- action = _getReverseMap()[key] ? 'keydown' : 'keypress';
- }
-
- // modifier keys don't work as expected with keypress,
- // switch to keydown
- if (action == 'keypress' && modifiers.length) {
- action = 'keydown';
- }
-
- return action;
- }
-
- /**
- * Converts from a string key combination to an array
- *
- * @param {string} combination like "command+shift+l"
- * @return {Array}
- */
- function _keysFromString(combination) {
- if (combination === '+') {
- return ['+'];
- }
-
- combination = combination.replace(/\+{2}/g, '+plus');
- return combination.split('+');
- }
-
- /**
- * Gets info for a specific key combination
- *
- * @param {string} combination key combination ("command+s" or "a" or "*")
- * @param {string=} action
- * @returns {Object}
- */
- function _getKeyInfo(combination, action) {
- var keys;
- var key;
- var i;
- var modifiers = [];
-
- // take the keys from this pattern and figure out what the actual
- // pattern is all about
- keys = _keysFromString(combination);
-
- for (i = 0; i < keys.length; ++i) {
- key = keys[i];
-
- // normalize key names
- if (_SPECIAL_ALIASES[key]) {
- key = _SPECIAL_ALIASES[key];
- }
-
- // if this is not a keypress event then we should
- // be smart about using shift keys
- // this will only work for US keyboards however
- if (action && action != 'keypress' && _SHIFT_MAP[key]) {
- key = _SHIFT_MAP[key];
- modifiers.push('shift');
- }
-
- // if this key is a modifier then add it to the list of modifiers
- if (_isModifier(key)) {
- modifiers.push(key);
- }
- }
-
- // depending on what the key combination is
- // we will try to pick the best event for it
- action = _pickBestAction(key, modifiers, action);
-
- return {
- key: key,
- modifiers: modifiers,
- action: action
- };
- }
-
- function _belongsTo(element, ancestor) {
- if (element === null || element === document) {
- return false;
- }
-
- if (element === ancestor) {
- return true;
- }
-
- return _belongsTo(element.parentNode, ancestor);
- }
-
- function Mousetrap(targetElement) {
- var self = this;
-
- targetElement = targetElement || document;
-
- if (!(self instanceof Mousetrap)) {
- return new Mousetrap(targetElement);
- }
-
- /**
- * element to attach key events to
- *
- * @type {Element}
- */
- self.target = targetElement;
-
- /**
- * a list of all the callbacks setup via Mousetrap.bind()
- *
- * @type {Object}
- */
- self._callbacks = {};
-
- /**
- * direct map of string combinations to callbacks used for trigger()
- *
- * @type {Object}
- */
- self._directMap = {};
-
- /**
- * keeps track of what level each sequence is at since multiple
- * sequences can start out with the same sequence
- *
- * @type {Object}
- */
- var _sequenceLevels = {};
-
- /**
- * variable to store the setTimeout call
- *
- * @type {null|number}
- */
- var _resetTimer;
-
- /**
- * temporary state where we will ignore the next keyup
- *
- * @type {boolean|string}
- */
- var _ignoreNextKeyup = false;
-
- /**
- * temporary state where we will ignore the next keypress
- *
- * @type {boolean}
- */
- var _ignoreNextKeypress = false;
-
- /**
- * are we currently inside of a sequence?
- * type of action ("keyup" or "keydown" or "keypress") or false
- *
- * @type {boolean|string}
- */
- var _nextExpectedAction = false;
-
- /**
- * resets all sequence counters except for the ones passed in
- *
- * @param {Object} doNotReset
- * @returns void
- */
- function _resetSequences(doNotReset) {
- doNotReset = doNotReset || {};
-
- var activeSequences = false,
- key;
-
- for (key in _sequenceLevels) {
- if (doNotReset[key]) {
- activeSequences = true;
- continue;
- }
- _sequenceLevels[key] = 0;
- }
-
- if (!activeSequences) {
- _nextExpectedAction = false;
- }
- }
-
- /**
- * finds all callbacks that match based on the keycode, modifiers,
- * and action
- *
- * @param {string} character
- * @param {Array} modifiers
- * @param {Event|Object} e
- * @param {string=} sequenceName - name of the sequence we are looking for
- * @param {string=} combination
- * @param {number=} level
- * @returns {Array}
- */
- function _getMatches(character, modifiers, e, sequenceName, combination, level) {
- var i;
- var callback;
- var matches = [];
- var action = e.type;
-
- // if there are no events related to this keycode
- if (!self._callbacks[character]) {
- return [];
- }
-
- // if a modifier key is coming up on its own we should allow it
- if (action == 'keyup' && _isModifier(character)) {
- modifiers = [character];
- }
-
- // loop through all callbacks for the key that was pressed
- // and see if any of them match
- for (i = 0; i < self._callbacks[character].length; ++i) {
- callback = self._callbacks[character][i];
-
- // if a sequence name is not specified, but this is a sequence at
- // the wrong level then move onto the next match
- if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
- continue;
- }
-
- // if the action we are looking for doesn't match the action we got
- // then we should keep going
- if (action != callback.action) {
- continue;
- }
-
- // if this is a keypress event and the meta key and control key
- // are not pressed that means that we need to only look at the
- // character, otherwise check the modifiers as well
- //
- // chrome will not fire a keypress if meta or control is down
- // safari will fire a keypress if meta or meta+shift is down
- // firefox will fire a keypress if meta, alt or control is down
- if ((action == 'keypress' && !e.metaKey && !e.altKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
-
- // when you bind a combination or sequence a second time it
- // should overwrite the first one. if a sequenceName or
- // combination is specified in this call it does just that
- //
- // @todo make deleting its own method?
- var deleteCombo = !sequenceName && callback.combo == combination;
- var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
- if (deleteCombo || deleteSequence) {
- self._callbacks[character].splice(i, 1);
- }
-
- matches.push(callback);
- }
- }
-
- return matches;
- }
-
- /**
- * actually calls the callback function
- *
- * if your callback function returns false this will use the jquery
- * convention - prevent default and stop propogation on the event
- *
- * @param {Function} callback
- * @param {Event} e
- * @returns void
- */
- function _fireCallback(callback, e, combo, sequence) {
-
- // if this event should not happen stop here
- if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
- return;
- }
-
- if (callback(e, combo) === false) {
- _preventDefault(e);
- _stopPropagation(e);
- }
- }
-
- /**
- * handles a character key event
- *
- * @param {string} character
- * @param {Array} modifiers
- * @param {Event} e
- * @returns void
- */
- self._handleKey = function(character, modifiers, e) {
- var callbacks = _getMatches(character, modifiers, e);
- var i;
- var doNotReset = {};
- var maxLevel = 0;
- var processedSequenceCallback = false;
-
- // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
- for (i = 0; i < callbacks.length; ++i) {
- if (callbacks[i].seq) {
- maxLevel = Math.max(maxLevel, callbacks[i].level);
- }
- }
-
- // loop through matching callbacks for this key event
- for (i = 0; i < callbacks.length; ++i) {
-
- // fire for all sequence callbacks
- // this is because if for example you have multiple sequences
- // bound such as "g i" and "g t" they both need to fire the
- // callback for matching g cause otherwise you can only ever
- // match the first one
- if (callbacks[i].seq) {
-
- // only fire callbacks for the maxLevel to prevent
- // subsequences from also firing
- //
- // for example 'a option b' should not cause 'option b' to fire
- // even though 'option b' is part of the other sequence
- //
- // any sequences that do not match here will be discarded
- // below by the _resetSequences call
- if (callbacks[i].level != maxLevel) {
- continue;
- }
-
- processedSequenceCallback = true;
-
- // keep a list of which sequences were matches for later
- doNotReset[callbacks[i].seq] = 1;
- _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
- continue;
- }
-
- // if there were no sequence matches but we are still here
- // that means this is a regular match so we should fire that
- if (!processedSequenceCallback) {
- _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
- }
- }
-
- // if the key you pressed matches the type of sequence without
- // being a modifier (ie "keyup" or "keypress") then we should
- // reset all sequences that were not matched by this event
- //
- // this is so, for example, if you have the sequence "h a t" and you
- // type "h e a r t" it does not match. in this case the "e" will
- // cause the sequence to reset
- //
- // modifier keys are ignored because you can have a sequence
- // that contains modifiers such as "enter ctrl+space" and in most
- // cases the modifier key will be pressed before the next key
- //
- // also if you have a sequence such as "ctrl+b a" then pressing the
- // "b" key will trigger a "keypress" and a "keydown"
- //
- // the "keydown" is expected when there is a modifier, but the
- // "keypress" ends up matching the _nextExpectedAction since it occurs
- // after and that causes the sequence to reset
- //
- // we ignore keypresses in a sequence that directly follow a keydown
- // for the same character
- var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
- if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
- _resetSequences(doNotReset);
- }
-
- _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
- };
-
- /**
- * handles a keydown event
- *
- * @param {Event} e
- * @returns void
- */
- function _handleKeyEvent(e) {
-
- // normalize e.which for key events
- // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
- if (typeof e.which !== 'number') {
- e.which = e.keyCode;
- }
-
- var character = _characterFromEvent(e);
-
- // no character found then stop
- if (!character) {
- return;
- }
-
- // need to use === for the character check because the character can be 0
- if (e.type == 'keyup' && _ignoreNextKeyup === character) {
- _ignoreNextKeyup = false;
- return;
- }
-
- self.handleKey(character, _eventModifiers(e), e);
- }
-
- /**
- * called to set a 1 second timeout on the specified sequence
- *
- * this is so after each key press in the sequence you have 1 second
- * to press the next key before you have to start over
- *
- * @returns void
- */
- function _resetSequenceTimer() {
- clearTimeout(_resetTimer);
- _resetTimer = setTimeout(_resetSequences, 1000);
- }
-
- /**
- * binds a key sequence to an event
- *
- * @param {string} combo - combo specified in bind call
- * @param {Array} keys
- * @param {Function} callback
- * @param {string=} action
- * @returns void
- */
- function _bindSequence(combo, keys, callback, action) {
-
- // start off by adding a sequence level record for this combination
- // and setting the level to 0
- _sequenceLevels[combo] = 0;
-
- /**
- * callback to increase the sequence level for this sequence and reset
- * all other sequences that were active
- *
- * @param {string} nextAction
- * @returns {Function}
- */
- function _increaseSequence(nextAction) {
- return function() {
- _nextExpectedAction = nextAction;
- ++_sequenceLevels[combo];
- _resetSequenceTimer();
- };
- }
-
- /**
- * wraps the specified callback inside of another function in order
- * to reset all sequence counters as soon as this sequence is done
- *
- * @param {Event} e
- * @returns void
- */
- function _callbackAndReset(e) {
- _fireCallback(callback, e, combo);
-
- // we should ignore the next key up if the action is key down
- // or keypress. this is so if you finish a sequence and
- // release the key the final key will not trigger a keyup
- if (action !== 'keyup') {
- _ignoreNextKeyup = _characterFromEvent(e);
- }
-
- // weird race condition if a sequence ends with the key
- // another sequence begins with
- setTimeout(_resetSequences, 10);
- }
-
- // loop through keys one at a time and bind the appropriate callback
- // function. for any key leading up to the final one it should
- // increase the sequence. after the final, it should reset all sequences
- //
- // if an action is specified in the original bind call then that will
- // be used throughout. otherwise we will pass the action that the
- // next key in the sequence should match. this allows a sequence
- // to mix and match keypress and keydown events depending on which
- // ones are better suited to the key provided
- for (var i = 0; i < keys.length; ++i) {
- var isFinal = i + 1 === keys.length;
- var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
- _bindSingle(keys[i], wrappedCallback, action, combo, i);
- }
- }
-
- /**
- * binds a single keyboard combination
- *
- * @param {string} combination
- * @param {Function} callback
- * @param {string=} action
- * @param {string=} sequenceName - name of sequence if part of sequence
- * @param {number=} level - what part of the sequence the command is
- * @returns void
- */
- function _bindSingle(combination, callback, action, sequenceName, level) {
-
- // store a direct mapped reference for use with Mousetrap.trigger
- self._directMap[combination + ':' + action] = callback;
-
- // make sure multiple spaces in a row become a single space
- combination = combination.replace(/\s+/g, ' ');
-
- var sequence = combination.split(' ');
- var info;
-
- // if this pattern is a sequence of keys then run through this method
- // to reprocess each pattern one key at a time
- if (sequence.length > 1) {
- _bindSequence(combination, sequence, callback, action);
- return;
- }
-
- info = _getKeyInfo(combination, action);
-
- // make sure to initialize array if this is the first time
- // a callback is added for this key
- self._callbacks[info.key] = self._callbacks[info.key] || [];
-
- // remove an existing match if there is one
- _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
-
- // add this call back to the array
- // if it is a sequence put it at the beginning
- // if not put it at the end
- //
- // this is important because the way these are processed expects
- // the sequence ones to come first
- self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
- callback: callback,
- modifiers: info.modifiers,
- action: info.action,
- seq: sequenceName,
- level: level,
- combo: combination
- });
- }
-
- /**
- * binds multiple combinations to the same callback
- *
- * @param {Array} combinations
- * @param {Function} callback
- * @param {string|undefined} action
- * @returns void
- */
- self._bindMultiple = function(combinations, callback, action) {
- for (var i = 0; i < combinations.length; ++i) {
- _bindSingle(combinations[i], callback, action);
- }
- };
-
- // start!
- _addEvent(targetElement, 'keypress', _handleKeyEvent);
- _addEvent(targetElement, 'keydown', _handleKeyEvent);
- _addEvent(targetElement, 'keyup', _handleKeyEvent);
- }
-
- /**
- * binds an event to mousetrap
- *
- * can be a single key, a combination of keys separated with +,
- * an array of keys, or a sequence of keys separated by spaces
- *
- * be sure to list the modifier keys first to make sure that the
- * correct key ends up getting bound (the last key in the pattern)
- *
- * @param {string|Array} keys
- * @param {Function} callback
- * @param {string=} action - 'keypress', 'keydown', or 'keyup'
- * @returns void
- */
- Mousetrap.prototype.bind = function(keys, callback, action) {
- var self = this;
- keys = keys instanceof Array ? keys : [keys];
- self._bindMultiple.call(self, keys, callback, action);
- return self;
- };
-
- /**
- * unbinds an event to mousetrap
- *
- * the unbinding sets the callback function of the specified key combo
- * to an empty function and deletes the corresponding key in the
- * _directMap dict.
- *
- * TODO: actually remove this from the _callbacks dictionary instead
- * of binding an empty function
- *
- * the keycombo+action has to be exactly the same as
- * it was defined in the bind method
- *
- * @param {string|Array} keys
- * @param {string} action
- * @returns void
- */
- Mousetrap.prototype.unbind = function(keys, action) {
- var self = this;
- return self.bind.call(self, keys, function() {}, action);
- };
-
- /**
- * triggers an event that has already been bound
- *
- * @param {string} keys
- * @param {string=} action
- * @returns void
- */
- Mousetrap.prototype.trigger = function(keys, action) {
- var self = this;
- if (self._directMap[keys + ':' + action]) {
- self._directMap[keys + ':' + action]({}, keys);
- }
- return self;
- };
-
- /**
- * resets the library back to its initial state. this is useful
- * if you want to clear out the current keyboard shortcuts and bind
- * new ones - for example if you switch to another page
- *
- * @returns void
- */
- Mousetrap.prototype.reset = function() {
- var self = this;
- self._callbacks = {};
- self._directMap = {};
- return self;
- };
-
- /**
- * should we stop this event before firing off callbacks
- *
- * @param {Event} e
- * @param {Element} element
- * @return {boolean}
- */
- Mousetrap.prototype.stopCallback = function(e, element) {
- var self = this;
-
- // if the element has the class "mousetrap" then no need to stop
- if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
- return false;
- }
-
- if (_belongsTo(element, self.target)) {
- return false;
- }
-
- // Events originating from a shadow DOM are re-targetted and `e.target` is the shadow host,
- // not the initial event target in the shadow tree. Note that not all events cross the
- // shadow boundary.
- // For shadow trees with `mode: 'open'`, the initial event target is the first element in
- // the event’s composed path. For shadow trees with `mode: 'closed'`, the initial event
- // target cannot be obtained.
- if ('composedPath' in e && typeof e.composedPath === 'function') {
- // For open shadow trees, update `element` so that the following check works.
- var initialEventTarget = e.composedPath()[0];
- if (initialEventTarget !== e.target) {
- element = initialEventTarget;
- }
- }
-
- // stop for input, select, and textarea
- return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
- };
-
- /**
- * exposes _handleKey publicly so it can be overwritten by extensions
- */
- Mousetrap.prototype.handleKey = function() {
- var self = this;
- return self._handleKey.apply(self, arguments);
- };
-
- /**
- * allow custom key mappings
- */
- Mousetrap.addKeycodes = function(object) {
- for (var key in object) {
- if (object.hasOwnProperty(key)) {
- _MAP[key] = object[key];
- }
- }
- _REVERSE_MAP = null;
- };
-
- /**
- * Init the global mousetrap functions
- *
- * This method is needed to allow the global mousetrap functions to work
- * now that mousetrap is a constructor function.
- */
- Mousetrap.init = function() {
- var documentMousetrap = Mousetrap(document);
- for (var method in documentMousetrap) {
- if (method.charAt(0) !== '_') {
- Mousetrap[method] = (function(method) {
- return function() {
- return documentMousetrap[method].apply(documentMousetrap, arguments);
- };
- } (method));
- }
- }
- };
-
- Mousetrap.init();
-
- // expose mousetrap to the global object
- window.Mousetrap = Mousetrap;
-
- // expose as a common js module
- if (typeof module !== 'undefined' && module.exports) {
- module.exports = Mousetrap;
- }
-
- // expose mousetrap as an AMD module
- if (typeof define === 'function' && define.amd) {
- define(function() {
- return Mousetrap;
- });
- }
-}) (typeof window !== 'undefined' ? window : null, typeof window !== 'undefined' ? document : null);
diff --git a/yarn.lock b/yarn.lock
index 2636311b107..7bf8c178c36 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -245,6 +245,11 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
+"@discourse/itsatrap@^2.0.10":
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/@discourse/itsatrap/-/itsatrap-2.0.10.tgz#c7e750eeb32b54e769e952c4ecc472213eb1385a"
+ integrity sha512-Jn1gdiyHMGUsmUfLFf4Q7VnTAv0l7NePbegU6pKhKHEmbzV3FosGxq30fTOYgVyTS1bxqGjlA6LvQttJpv3ROw==
+
"@ember-data/rfc395-data@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@ember-data/rfc395-data/-/rfc395-data-0.0.4.tgz#ecb86efdf5d7733a76ff14ea651a1b0ed1f8a843"
@@ -3116,10 +3121,6 @@ moment@2.29.1:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425"
integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==
-"mousetrap@https://github.com/discourse/mousetrap#firefox-alt-key":
- version "1.6.5"
- resolved "https://github.com/discourse/mousetrap#cc8e2c0b9229e1a01ce68de4f339b6fd35503041"
-
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"