63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/addon/schema.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "namespace": "tbkeys",
4 | "events": [
5 | {
6 | "name": "onSendMessage",
7 | "type": "function",
8 | "description": "Event marking when tbkeys wants a message sent to another MailExtension",
9 | "parameters": [
10 | {
11 | "name": "extensionID",
12 | "description": "ID of extension to send message to",
13 | "type": "string"
14 | },
15 | {
16 | "name": "message",
17 | "description": "Message to send to extension",
18 | "type": "any"
19 | }
20 | ]
21 | }
22 | ],
23 | "functions": [
24 | {
25 | "name": "bindKeys",
26 | "type": "function",
27 | "description": "Bind keys",
28 | "async": true,
29 | "parameters": [
30 | {
31 | "name": "keyBindings",
32 | "type": "object",
33 | "description": "Mapping of window types to keys",
34 | "properties": {
35 | "main": { "$ref": "Keys" },
36 | "compose": { "$ref": "Keys" }
37 | },
38 | "additionalProperties": false,
39 | "required": ["main", "compose"]
40 | }
41 | ]
42 | }
43 | ],
44 | "types": [
45 | {
46 | "id": "Keys",
47 | "type": "object",
48 | "description": "Mapping of key sequences to commands",
49 | "additionalProperties": { "type": "string" }
50 | }
51 | ]
52 | }
53 | ]
54 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BLDDIR = build
2 |
3 | # Necessary because zip copies leading directories if run from above targets
4 | ABS_BLDDIR := $(shell readlink -f $(BLDDIR))
5 |
6 | all: tbkeys tbkeys-lite
7 |
8 | tbkeys: $(BLDDIR)/tbkeys.xpi
9 |
10 | tbkeys-lite: $(BLDDIR)/tbkeys-lite.xpi
11 |
12 | SRC_FILES = $(wildcard addon/*.json) $(wildcard addon/*.js) $(wildcard addon/*.html) $(wildcard addon/modules/*.js) $(wildcard addon/*.md)
13 | ADDON_FILES = $(subst addon/,,$(SRC_FILES))
14 |
15 | $(BLDDIR)/tbkeys.xpi: $(SRC_FILES)
16 | @mkdir -p $(dir $@)
17 | rm -f $@
18 | cd addon; zip -FSr $(ABS_BLDDIR)/tbkeys.xpi $(ADDON_FILES)
19 |
20 | $(BLDDIR)/tbkeys-lite.xpi: $(SRC_FILES)
21 | rm -rf $(dir $@)/lite
22 | @mkdir -p $(dir $@)
23 | cp -r addon $(dir $@)/lite
24 | # Drop update_url
25 | sed -i '/update_url/d' $(dir $@)/lite/manifest.json
26 | sed -i 's/\( *"strict_min_version".*\),$$/\1,/' $(dir $@)/lite/manifest.json
27 | sed -i 's/\( *"strict_max_version": \)\(.*\),$$/\1"136.*"/' $(dir $@)/lite/manifest.json
28 | # Drop eval()
29 | sed -i 's#^\( *\)eval(.*#\1// Do nothing#' $(dir $@)/lite/implementation.js
30 | # Change name
31 | sed -i 's/tbkeys@/tbkeys-lite@/' $(dir $@)/lite/manifest.json
32 | sed -i 's/"name": "tbkeys"/"name": "tbkeys-lite"/' $(dir $@)/lite/manifest.json
33 | sed -i 's/tbkeys@/tbkeys-lite@/' $(dir $@)/lite/implementation.js
34 | # Build xpi
35 | cd $(dir $@)/lite; zip -FSr $(ABS_BLDDIR)/tbkeys-lite.xpi $(ADDON_FILES)
36 |
37 | lint:
38 | npx prettier --write .
39 | npx eslint .
40 |
41 | clean:
42 | rm -f $(BLDDIR)/tbkeys*.xpi
43 | rm -rf $(BLDDIR)/lite
44 |
45 | .PHONY: all clean xpi lint
46 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | - 2.4.1:
2 |
3 | - Prevent triggering in the addons search box in Thunderbird 141.
4 |
5 | - 2.4.0:
6 |
7 | - Drop support for versions of Thunderbird older than 128 and fix compatibility with Thunderbird 136.
8 | Those versions can continue using 2.3.0.
9 |
10 | - 2.3.0:
11 |
12 | - Drop support for versions of Thunderbird older than 115.
13 | Those versions can continue using 2.2.5.
14 | This change is mainly a precaution to acknowledge that those older versions are not tested as part of development and could be broken by future updates (it also fulfills a request from addons.thunderbird.net).
15 |
16 | - 2.2.5:
17 |
18 | - Do not capture keys in the quick search box in Thunderbird 128+
19 |
20 | - 2.2.4:
21 |
22 | - Do not capture keys in the quick search box on Thunderbird 115+
23 |
24 | - 2.2.3:
25 |
26 | - Mark tbkeys-list as supporting Thunderbird 115
27 | - Update references to Services API to be compatible with Thunderbird 117
28 | - Do not capture keys in the search box of Thunderbird 113+
29 |
30 | - 2.2.2:
31 |
32 | - Mark tbkeys-lite as supporting Thunderbird 102.\* instead of 103.0.
33 | This specification is preferred by addons.thunderbird.net.
34 | It does not correspond to the actual maximum verison for which tbkeys works.
35 |
36 | - 2.2.1:
37 |
38 | - Mark tbkeys-lite as supporting Thunderbird 103
39 |
40 | - 2.2.0:
41 |
42 | - Support for sending messages to other extensions
43 |
44 | - 2.1.4:
45 |
46 | - Do not capture keys in the text fields of the New Event tab
47 |
48 | - 2.1.3:
49 |
50 | - Do not capture keys in Thunderbird's builtin web browser
51 |
52 | - 2.1.2:
53 |
54 | - Code changes to avoid tbkeys failing to load on startup
55 |
56 | - 2.1.1:
57 |
58 | - Fix keys being captured in some textboxes like the signature box in settings.
59 |
60 | - 2.1.0:
61 | - Support for key bindings in the compose window
62 | - New lite packaging of the xpi without `eval()` support
63 | - Command types `cmd`, `func`, `tbkeys`, and `unset` for simpler settings syntax
64 | - Button to unset single key shortcuts
65 | - Button to reset to default settings
66 | - Fix keys being captured in chat input on Thunderbird 68
67 | - More documentation, including listing recipes for some requested functions
68 |
--------------------------------------------------------------------------------
/addon/background.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /* global browser */
3 | var defaults = {
4 | mainkeys: `{
5 | "j": "cmd:cmd_nextMsg",
6 | "k": "cmd:cmd_previousMsg",
7 | "o": "cmd:cmd_openMessage",
8 | "f": "cmd:cmd_forward",
9 | "#": "cmd:cmd_delete",
10 | "r": "cmd:cmd_reply",
11 | "a": "cmd:cmd_replyall",
12 | "x": "cmd:cmd_archive",
13 | "c": "func:MsgNewMessage",
14 | "u": "tbkeys:closeMessageAndRefresh"
15 | }`,
16 | composekeys: "{}",
17 | };
18 | var optionNames = Object.getOwnPropertyNames(defaults);
19 |
20 | // Helper function for getSettings
21 | // Taken from https://thunderbird.topicbox.com/groups/addons/T46e96308f41c0de1
22 | const promiseWithTimeout = function (ms, promise) {
23 | // Create a promise that rejects in milliseconds
24 | let timeout = new Promise((resolve, reject) => {
25 | let id = setTimeout(() => {
26 | clearTimeout(id);
27 | reject(new Error("Timed out in " + ms + "ms."));
28 | }, ms);
29 | });
30 |
31 | // Returns a race between our timeout and the passed in promise
32 | return Promise.race([promise, timeout]);
33 | };
34 |
35 | // Retrieve user settings from storage, inserting default values and migrating
36 | // obsolete storage keys.
37 | async function getSettings() {
38 | let settings;
39 | const retries = 7;
40 | const baseTimeout = 700;
41 | // Storage retrieval does not always work, so retry in a loop.
42 | // See https://thunderbird.topicbox.com/groups/addons/T46e96308f41c0de1
43 | for (let tryNum = 0; ; tryNum++) {
44 | try {
45 | settings = await promiseWithTimeout(
46 | baseTimeout * (tryNum + 1),
47 | browser.storage.local.get()
48 | );
49 | break;
50 | } catch (error) {
51 | if (tryNum >= retries) {
52 | error.message = "TBKeys: could not load settings -- " + error.message;
53 | throw error;
54 | }
55 | }
56 | }
57 |
58 | // Migrate old "keys" setting to "mainkeys"
59 | if (Object.prototype.hasOwnProperty.call(settings, "keys")) {
60 | settings.mainkeys = settings.keys;
61 | await browser.storage.local.remove("keys");
62 | await browser.storage.local.set({ mainkeys: settings.mainkeys });
63 | }
64 |
65 | for (let setting of optionNames) {
66 | if (!Object.prototype.hasOwnProperty.call(settings, setting)) {
67 | settings[setting] = defaults[setting];
68 | }
69 | }
70 |
71 | return settings;
72 | }
73 |
74 | // Apply key bindings
75 | async function applyKeys() {
76 | let settings = await getSettings();
77 |
78 | await browser.tbkeys.bindKeys({
79 | main: JSON.parse(settings.mainkeys),
80 | compose: JSON.parse(settings.composekeys),
81 | });
82 | }
83 | applyKeys();
84 |
85 | browser.tbkeys.onSendMessage.addListener(async (extensionID, message) => {
86 | browser.runtime.sendMessage(extensionID, message);
87 | });
88 |
--------------------------------------------------------------------------------
/updates.json:
--------------------------------------------------------------------------------
1 | {
2 | "addons": {
3 | "tbkeys@addons.thunderbird.net": {
4 | "updates": [
5 | {
6 | "version": "1.0",
7 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v1.0.0/tbkeys.xpi",
8 | "browser_specific_settings": {
9 | "gecko": { "strict_max_version": "68" }
10 | }
11 | },
12 | {
13 | "version": "2.0.0",
14 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.0.0/tbkeys.xpi"
15 | },
16 | {
17 | "version": "2.0.1",
18 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.0.1/tbkeys.xpi"
19 | },
20 | {
21 | "version": "2.0.2",
22 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.0.2/tbkeys.xpi"
23 | },
24 | {
25 | "version": "2.1.0",
26 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.1.0/tbkeys.xpi"
27 | },
28 | {
29 | "version": "2.1.1",
30 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.1.1/tbkeys.xpi"
31 | },
32 | {
33 | "version": "2.1.2",
34 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.1.2/tbkeys.xpi"
35 | },
36 | {
37 | "version": "2.1.3",
38 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.1.3/tbkeys.xpi"
39 | },
40 | {
41 | "version": "2.1.4",
42 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.1.4/tbkeys.xpi"
43 | },
44 | {
45 | "version": "2.2.0",
46 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.2.0/tbkeys.xpi"
47 | },
48 | {
49 | "version": "2.2.1",
50 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.2.1/tbkeys.xpi"
51 | },
52 | {
53 | "version": "2.2.2",
54 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.2.2/tbkeys.xpi"
55 | },
56 | {
57 | "version": "2.2.3",
58 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.2.3/tbkeys.xpi"
59 | },
60 | {
61 | "version": "2.2.4",
62 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.2.4/tbkeys.xpi"
63 | },
64 | {
65 | "version": "2.2.5",
66 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.2.5/tbkeys.xpi"
67 | },
68 | {
69 | "version": "2.3.0",
70 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.3.0/tbkeys.xpi",
71 | "applications": {
72 | "gecko": { "strict_min_version": "115.0" }
73 | }
74 | },
75 | {
76 | "version": "2.4.0",
77 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.4.0/tbkeys.xpi",
78 | "applications": {
79 | "gecko": { "strict_min_version": "128.0" }
80 | }
81 | },
82 | {
83 | "version": "2.4.1",
84 | "update_link": "https://github.com/wshanks/tbkeys/releases/download/v2.4.1/tbkeys.xpi",
85 | "applications": {
86 | "gecko": { "strict_min_version": "128.0" }
87 | }
88 | }
89 | ]
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/addon/options.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /* global browser */
3 | var background = browser.extension.getBackgroundPage();
4 |
5 | // Save options currently in input fields if they pass validation
6 | async function saveOptions(e) {
7 | e.preventDefault();
8 | let settings = {};
9 |
10 | if (!validateKeys()) {
11 | return;
12 | }
13 |
14 | let element;
15 | let value;
16 | for (let setting of background.optionNames) {
17 | element = document.querySelector("#" + setting);
18 | if (element.type == "checkbox") {
19 | value = element.checked;
20 | } else {
21 | value = element.value;
22 | }
23 | // Only save values set to a new, non-empty value
24 | if (value != background.defaults[setting] && value != "") {
25 | settings[setting] = value;
26 | }
27 | }
28 | await browser.storage.local.set(settings);
29 | for (let setting of background.optionNames) {
30 | if (!Object.prototype.hasOwnProperty.call(settings, setting)) {
31 | await browser.storage.local.remove(setting);
32 | }
33 | }
34 | await background.applyKeys();
35 | await restoreOptions();
36 | }
37 |
38 | // Restore currently stored settings to the input fields
39 | async function restoreOptions() {
40 | let settings = await background.getSettings();
41 | let element;
42 | for (let setting in settings) {
43 | element = document.querySelector("#" + setting);
44 | if (element.type == "checkbox") {
45 | element.checked = settings[setting];
46 | } else {
47 | element.value = settings[setting];
48 | }
49 | }
50 | }
51 |
52 | // Restore the default settings to the input fields and storage
53 | async function restoreDefaults(e) {
54 | e.preventDefault();
55 | await browser.storage.local.remove(background.optionNames);
56 | await restoreOptions();
57 | }
58 |
59 | // Apply "unset" to all single keys not currently set to something else
60 | async function unsetSingleKeys(e) {
61 | e.preventDefault();
62 | let settings = await browser.storage.local.get("mainkeys");
63 | if (!Object.prototype.hasOwnProperty.call(settings, "mainkeys")) {
64 | settings.mainkeys = background.defaults.mainkeys;
65 | }
66 | let keys = JSON.parse(settings.mainkeys);
67 | let singles = [
68 | "0",
69 | "1",
70 | "2",
71 | "3",
72 | "4",
73 | "5",
74 | "6",
75 | "7",
76 | "8",
77 | "9",
78 | "a",
79 | "b",
80 | "c",
81 | "f",
82 | "j",
83 | "k",
84 | "m",
85 | "o",
86 | "p",
87 | "r",
88 | "s",
89 | "t",
90 | "u",
91 | "w",
92 | "x",
93 | "#",
94 | "]",
95 | "[",
96 | ];
97 | for (let key of singles) {
98 | if (!Object.prototype.hasOwnProperty.call(keys, key)) {
99 | keys[key] = "unset";
100 | }
101 | }
102 | await browser.storage.local.set({ mainkeys: JSON.stringify(keys, null, 4) });
103 | await background.applyKeys();
104 | await restoreOptions();
105 | }
106 |
107 | function validateKeys() {
108 | let keysFields = document.querySelectorAll(".json");
109 | let valid = true;
110 | for (let keysField of keysFields) {
111 | try {
112 | if (keysField.value != "") {
113 | JSON.parse(keysField.value);
114 | }
115 | keysField.setCustomValidity("");
116 | } catch {
117 | keysField.setCustomValidity("Invalid JSON");
118 | valid = false;
119 | }
120 | }
121 | return valid;
122 | }
123 |
124 | document.addEventListener("DOMContentLoaded", restoreOptions);
125 | document.querySelector("#save").addEventListener("submit", saveOptions);
126 | document.querySelector("#restore").addEventListener("submit", restoreDefaults);
127 | document.querySelector("#unset").addEventListener("submit", unsetSingleKeys);
128 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our
7 | community a harassment-free experience for everyone, regardless of age, body
8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
9 | identity and expression, level of experience, education, socio-economic status,
10 | nationality, personal appearance, race, religion, or sexual identity
11 | and orientation.
12 |
13 | We pledge to act and interact in ways that contribute to an open, welcoming,
14 | diverse, inclusive, and healthy community.
15 |
16 | ## Our Standards
17 |
18 | Examples of behavior that contributes to a positive environment for our
19 | community include:
20 |
21 | * Demonstrating empathy and kindness toward other people
22 | * Being respectful of differing opinions, viewpoints, and experiences
23 | * Giving and gracefully accepting constructive feedback
24 | * Accepting responsibility and apologizing to those affected by our mistakes,
25 | and learning from the experience
26 | * Focusing on what is best not just for us as individuals, but for the
27 | overall community
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | * The use of sexualized language or imagery, and sexual attention or
32 | advances of any kind
33 | * Trolling, insulting or derogatory comments, and personal or political attacks
34 | * Public or private harassment
35 | * Publishing others' private information, such as a physical or email
36 | address, without their explicit permission
37 | * Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions that are
49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
50 | decisions when appropriate.
51 |
52 | ## Scope
53 |
54 | This Code of Conduct applies within all community spaces, and also applies when
55 | an individual is officially representing the community in public spaces.
56 | Examples of representing our community include using an official e-mail address,
57 | posting via an official social media account, or acting as an appointed
58 | representative at an online or offline event.
59 |
60 | ## Enforcement
61 |
62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
63 | reported to the community leaders responsible for enforcement using
64 | the contact information provided at (https://github.com/wshanks).
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series
87 | of actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or
94 | permanent ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within
114 | the community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.0, available at
120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
121 |
122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
123 | enforcement ladder](https://github.com/mozilla/diversity).
124 |
125 | [homepage]: https://www.contributor-covenant.org
126 |
127 | For answers to common questions about this code of conduct, see the FAQ at
128 | https://www.contributor-covenant.org/faq. Translations are available at
129 | https://www.contributor-covenant.org/translations.
130 |
131 |
--------------------------------------------------------------------------------
/addon/implementation.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /* global ChromeUtils, Services */
3 |
4 | var { ExtensionCommon } = ChromeUtils.importESModule(
5 | "resource://gre/modules/ExtensionCommon.sys.mjs"
6 | );
7 | var { ExtensionParent } = ChromeUtils.importESModule(
8 | "resource://gre/modules/ExtensionParent.sys.mjs"
9 | );
10 | var { ExtensionSupport } = ChromeUtils.importESModule(
11 | "resource:///modules/ExtensionSupport.sys.mjs"
12 | );
13 |
14 | const EXTENSION_NAME = "tbkeys@addons.thunderbird.net";
15 | var extension = ExtensionParent.GlobalManager.getExtension(EXTENSION_NAME);
16 |
17 | // Extra functions available for binding with tbkeys
18 | var builtins = {
19 | closeMessageAndRefresh: function (win) {
20 | if (
21 | win.document.getElementById("tabmail").tabContainer.selectedIndex != 0
22 | ) {
23 | win.CloseTabOrWindow();
24 | }
25 | win.goDoCommand("cmd_getMsgsForAuthAccounts");
26 | win.goDoCommand("cmd_expandAllThreads");
27 | },
28 | };
29 |
30 | // Table to translate internal Thunderbird window names to shorter forms
31 | // exposed in tbkeys' preferences.
32 | const WINDOW_TYPES = {
33 | "mail:3pane": "main",
34 | msgcompose: "compose",
35 | };
36 |
37 | // Function called by Mousetrap to test if it should stop processing a key event
38 | //
39 | // This function is based on the default callback in Mousetrap but is extended
40 | // to include more text input fields that are specific to Thunderbird.
41 | // Additionally, it does not ignore text fields if the first key includes
42 | // modifiers other than shift.
43 | function stopCallback(e, element, combo, seq) {
44 | let tagName = element.tagName.toLowerCase();
45 | // Uncomment the following line to debug why tbkeys is triggering in an input
46 | // field where it should not trigger:
47 | // Services.console.logStringMessage(`tbkeys triggered by tag ${tagName}`)
48 | let isText =
49 | tagName == "imconversation" ||
50 | tagName == "textbox" ||
51 | tagName == "input" ||
52 | tagName == "select" ||
53 | tagName == "textarea" ||
54 | tagName == "html:input" ||
55 | tagName == "search-textbox" ||
56 | tagName == "xul:search-textbox" ||
57 | tagName == "html:textarea" ||
58 | tagName == "browser" ||
59 | tagName == "global-search-bar" ||
60 | tagName == "search-bar" ||
61 | tagName == "moz-input-search" ||
62 | (element.contentEditable && element.contentEditable == "true");
63 |
64 | if (!isText && element.contentEditable == "inherit") {
65 | let ancestor = element;
66 | while (ancestor.contentEditable == "inherit") {
67 | ancestor = ancestor.parentElement;
68 | if (ancestor === null) {
69 | if (element.ownerDocument.designMode == "on") {
70 | isText = true;
71 | }
72 | break;
73 | }
74 | if (ancestor.contentEditable == "true") {
75 | isText = true;
76 | break;
77 | }
78 | }
79 | }
80 |
81 | let firstCombo = combo;
82 | if (seq !== undefined) {
83 | firstCombo = seq.trim().split(" ")[0];
84 | }
85 | let modifiers = ["ctrl", "alt", "meta", "option", "command"];
86 | let hasModifier = false;
87 | for (let mod of modifiers) {
88 | if (firstCombo.includes(mod)) {
89 | hasModifier = true;
90 | break;
91 | }
92 | }
93 |
94 | return isText && !hasModifier;
95 | }
96 |
97 | // Build a callback function to execute a tbkeys command
98 | //
99 | // win is the window in which the command should be executed
100 | //
101 | // command should be a string formatted as type:body where type is cmd, func,
102 | // tbkeys, unset, or eval and body is the type-specific content of the command
103 | function buildKeyCommand(win, command) {
104 | let callback = function () {
105 | // window is defined here so that it is available for use with eval() in
106 | // the non-lite version of tbkeys
107 | // eslint-disable-next-line no-unused-vars
108 | let window = win;
109 |
110 | let cmdType = command.split(":", 1)[0];
111 | let cmdBody = command.slice(cmdType.length + 1);
112 | switch (cmdType) {
113 | case "cmd":
114 | win.goDoCommand(cmdBody);
115 | break;
116 | case "func":
117 | win[cmdBody]();
118 | break;
119 | case "tbkeys":
120 | builtins[cmdBody](win);
121 | break;
122 | case "memsg":
123 | Services.obs.notifyObservers(null, "tbkeys-memsg", cmdBody);
124 | break;
125 | case "unset":
126 | break;
127 | default:
128 | eval(command);
129 | break;
130 | }
131 | return false;
132 | };
133 |
134 | return callback;
135 | }
136 |
137 | var TBKeys = {
138 | // keys stores keybindings so they can be applied to new windows that are
139 | // opened after the bindings have been set
140 | //
141 | // Initialized to empty key bindings for each window type
142 | keys: Object.fromEntries(Object.values(WINDOW_TYPES).map((t) => [t, {}])),
143 |
144 | // The init() function uses the `initialized` flag so that its initialization
145 | // code can be run only once but it can be called at the latest possible
146 | // moment (at the first usage of the experiment API).
147 | initialized: false,
148 | meMsgCallback: null,
149 | init: function () {
150 | if (this.initialized) {
151 | return;
152 | }
153 | ExtensionSupport.registerWindowListener(EXTENSION_NAME, {
154 | chromeURLs: [
155 | "chrome://messenger/content/messengercompose/messengercompose.xul",
156 | "chrome://messenger/content/messengercompose/messengercompose.xhtml",
157 | "chrome://messenger/content/messenger.xul",
158 | "chrome://messenger/content/messenger.xhtml",
159 | ],
160 | onLoadWindow: TBKeys.loadWindowChrome.bind(TBKeys),
161 | onUnloadWindow: TBKeys.unloadWindowChrome,
162 | });
163 | this.initialized = true;
164 | },
165 |
166 | loadWindowChrome: function (win) {
167 | Services.scriptloader.loadSubScript(
168 | extension.rootURI.resolve("modules/mousetrap.js"),
169 | win
170 | );
171 | win.Mousetrap.prototype.stopCallback = stopCallback;
172 | let type = win.document.documentElement.getAttribute("windowtype");
173 | let keys = this.keys[WINDOW_TYPES[type]];
174 | this.bindKeysInWindow(win, keys);
175 | },
176 |
177 | unloadWindowChrome: function (win) {
178 | if (typeof win.Mousetrap != "undefined") {
179 | win.Mousetrap.reset();
180 | }
181 | delete win.Mousetrap;
182 | },
183 |
184 | bindKeysInWindow: function (win, keys) {
185 | win.Mousetrap.reset();
186 | for (let [key, command] of Object.entries(keys)) {
187 | win.Mousetrap.bind(key, buildKeyCommand(win, command));
188 | }
189 | },
190 |
191 | // Set all keybindings for all windows
192 | //
193 | // keyBindings has the structure:
194 | // {windowType: {keySequence: keyCommand}}
195 | // keyBindings should have all WINDOW_TYPES values
196 | bindKeys: function (keyBindings) {
197 | this.init();
198 | this.keys = keyBindings;
199 | for (const [tbWinName, shortWinName] of Object.entries(WINDOW_TYPES)) {
200 | let windows = Services.wm.getEnumerator(tbWinName);
201 | while (windows.hasMoreElements()) {
202 | let win = windows.getNext();
203 |
204 | if (typeof win.Mousetrap != "undefined") {
205 | this.bindKeysInWindow(win, this.keys[shortWinName]);
206 | }
207 | }
208 | }
209 | },
210 |
211 | MEMsgObserver: {
212 | observe: function (subject, topic, data) {
213 | switch (topic) {
214 | case "tbkeys-memsg":
215 | if (TBKeys.meMsgCallback !== null) {
216 | let extensionID = data.split(":", 1)[0];
217 | let message = data.slice(extensionID.length + 1);
218 | TBKeys.meMsgCallback(extensionID, message);
219 | }
220 | break;
221 | default:
222 | }
223 | },
224 | },
225 | };
226 |
227 | // eslint-disable-next-line no-unused-vars
228 | var tbkeys = class extends ExtensionCommon.ExtensionAPI {
229 | onShutdown(isAppShutdown) {
230 | ExtensionSupport.unregisterWindowListener(EXTENSION_NAME);
231 | let windows = Services.wm.getEnumerator(null);
232 | while (windows.hasMoreElements()) {
233 | TBKeys.unloadWindowChrome(windows.getNext());
234 | }
235 |
236 | if (isAppShutdown) return;
237 |
238 | // Thunderbird might still cache some of your JavaScript files and even
239 | // if JSMs have been unloaded, the last used version could be reused on
240 | // next load, ignoring any changes. Get around this issue by
241 | // invalidating the caches (this is identical to restarting TB with the
242 | // -purgecaches parameter):
243 | Services.obs.notifyObservers(null, "startupcache-invalidate", null);
244 | }
245 |
246 | getAPI(context) {
247 | return {
248 | tbkeys: {
249 | bindKeys: async function (keyBindings) {
250 | TBKeys.bindKeys(keyBindings);
251 | },
252 | onSendMessage: new ExtensionCommon.EventManager({
253 | context,
254 | name: "tbkeys.onSendMessage",
255 | register: (fire) => {
256 | TBKeys.meMsgCallback = (extensionID, message) => {
257 | fire.async(extensionID, message);
258 | };
259 | Services.obs.addObserver(
260 | TBKeys.MEMsgObserver,
261 | "tbkeys-memsg",
262 | false
263 | );
264 | return () => {
265 | Services.obs.removeObserver(TBKeys.MEMsgObserver, "tbkeys-memsg");
266 | TBKeys.meMsgCallback = null;
267 | };
268 | },
269 | }).api(),
270 | },
271 | };
272 | }
273 | };
274 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](code_of_conduct.md)
2 |
3 | # tbkeys
4 |
5 | `tbkeys` is an add-on for Thunderbird that uses [Mousetrap](https://craig.is/killing/mice) to bind key sequences to custom commands.
6 |
7 | ## Install
8 |
9 | - Download the tbkeys.xpi file from one of the releases listed on the [GitHub releases page](https://github.com/willsALMANJ/tbkeys/releases).
10 | - Open the Add-ons Manager in Thunderbird (Tools->Add-ons).
11 | - Click on the gear icon in the upper right and choose "Install Add-on From File..." and then select the downloaded tbkeys.xpi file.
12 | - The add-on will self-update from the GitHub releases page when future updates are released.
13 |
14 | The [tbkeys-lite](#tbkeys-lite) version of the addon can also be installed from addons.thunderbird.net by searching for "tbkeys-lite" in the Thunderbird addons manager or by downloading the xpi file from [this page](https://addons.thunderbird.net/en-US/thunderbird/addon/tbkeys-lite/) and following the steps above.
15 |
16 | ## Default key bindings
17 |
18 | The default key bindings for the main window are modeled on GMail's key bindings.
19 |
20 | | Key | Function |
21 | | --- | ------------------------------------------------- |
22 | | c | Compose new message |
23 | | r | Reply |
24 | | a | Reply all |
25 | | f | Forward |
26 | | # | Delete |
27 | | u | Refresh mail. If a message tab is open, close it. |
28 | | j | Next message |
29 | | k | Previous message |
30 | | o | Open message |
31 | | x | Archive message |
32 |
33 | ## Customizing key bindings
34 |
35 | To customize key bindings, modify the "key bindings" entries in the add-on's preferences pane which can be accessed from the add-on's entry in the Add-ons Manager ("Add-ons" in the Thunderbird menu).
36 | Here are some things to consider when setting key bindings:
37 |
38 | - The "key bindings" entry should be a JSON object mapping key bindings (with Mousetrap syntax as described [here](https://craig.is/killing/mice)) to a valid command (see the [Command syntax](#command-syntax)) section.
39 | - There are separate fields in the preferences page for setting key bindings for the main Thunderbird window and the compose window.
40 | Key bindings do not fire in other windows.
41 | - Key bindings do not fire in text input fields unless the first key combo includes a modifier other than `shift`.
42 | - The preferences page will not allow invalid JSON to be submitted, but it does not sanity check the key bindings otherwise.
43 | - This [old wiki page about Keyconfig](http://kb.mozillazine.org/Keyconfig_extension:_Thunderbird) also has some commands that are still valid.
44 | - The Developer Toolbox (Tools->Developer Tools->Developer Toolbox in the menu) can be useful for poking around at the UI to find the name of an element to call a function on.
45 | - Defining a key sequence (meaning multiple keys in succession) where the first key combination in the sequence is the same as a built-in shortcut (like `ctrl+j ctrl+k`) is not supported.
46 | Single keys with modifiers may be mapped to override the built-in shortcuts but not sequences.
47 |
48 | ### Command syntax
49 |
50 | A few different styles of commands can be specified for key bindings.
51 | They are:
52 |
53 | - **Simple commands**: These commands follow the format `cmd:` where `` is a command that Thunderbird can execute with `goDoCommand()`.
54 | Most command names can be found in [the main command set file](https://hg.mozilla.org/comm-central/file/tip/mail/base/content/mainCommandSet.inc.xhtml) of the Thunderbird source code.
55 | - **Simple function calls**: These commands follow the format `func:` where `` is a function defined on the Thunderbird window object.
56 | That function is called without any arguments.
57 | - **Custom function calls**: These commands follow the format `tbkeys:` where `` is the name of a custom function written in tbkeys.
58 | Currently, the only available custom function is `closeMessageAndRefresh` which closes the open tab if it is not the first tab and then refreshes all accounts.
59 | This behavior mimics the behavior of the GMail keybinding `u`.
60 | - **Unset binding**: These entries simply contain the text `unset`.
61 | When an `unset` keybinding is triggered, nothing happens.
62 | This can be useful unbinding built-in Thunderbird key bindings which you do not wish to trigger by accident.
63 | - **MailExtension messages**: These commands follow the format `memsg::` where `` is the ID of the Thunderbird extension to which to send a message and `` is a string message to send to the extension using the `browser.runtime.sendMessage()` MailExtension API.
64 | Currently, only string messages are supported because `tbkeys` stores its commands as strings, though that restriction could possibly be relaxed in the future.
65 | - **Eval commands**: These entries may contain arbitrary javascript code on which tbkeys will call `eval()` when the key binding is triggered.
66 | Any entry not matching the prefixes of the other command types is treated as an eval command.
67 | **NOTE:** eval commands are not available in tbkeys-lite and will function the same as unset commands instead.
68 |
69 | ## Common key bindings
70 |
71 | Here are some examples of eval commands for commonly desired key bindings:
72 |
73 | - **Next tab**: `window.document.getElementById('tabmail-tabs').advanceSelectedTab(1, true)`
74 | - **Previous tab**: `window.document.getElementById('tabmail-tabs').advanceSelectedTab(-1, true)`
75 | - **Close tab**: `func:CloseTabOrWindow`
76 | - **Scroll message list down**: `window.document.getElementById('threadTree').scrollByLines(1)`
77 | - **Scroll message list up**: `window.document.getElementById('threadTree').scrollByLines(-1)`
78 | - **Scroll message body down**:
79 | - v115+: `window.gTabmail.currentAboutMessage.getMessagePaneBrowser().contentWindow.scrollBy(0, 100)`
80 | - v102: `window.document.getElementById('messagepane').contentDocument.documentElement.getElementsByTagName('body')[0].scrollBy(0, 100)`
81 | - **Scroll message body up**:
82 | - v115+: `window.gTabmail.currentAboutMessage.getMessagePaneBrowser().contentWindow.scrollBy(0, -100)`
83 | - v102: `window.document.getElementById('messagepane').contentDocument.documentElement.getElementsByTagName('body')[0].scrollBy(0, -100)`
84 | - **Create new folder**: `window.goDoCommand('cmd_newFolder')`
85 | - **Subscribe to feed**: `window.openSubscriptionsDialog(window.GetSelectedMsgFolders()[0])`
86 |
87 | ## Unsetting default key bindings
88 |
89 | The "Unset singles" button in the preferences pane can be used to unset Thunderbird's default single key bindings in the main window.
90 | This function set all of Thunderbird's default single key shortcuts to `unset` unless they are currently set in tbkey's preferences (that is, it won't overwrite tbkeys' existing settings for single key shortcuts).
91 |
92 | ## tbkeys and tbkeys-lite
93 |
94 | tbkeys-lite is a version of tbkeys with the ability to execute arbitrary javascript removed.
95 |
96 | ## Security, privacy, and implementation
97 |
98 | Before installation, Thunderbird will prompt about the extension requiring permission to "Have full, unrestricted access to Thunderbird, and your computer."
99 | The reason for this permission request is that tbkeys must inject a key listener into the Thunderbird user interface in order to listen for key bindings.
100 | To do this, tbkeys uses the older Thunderbird extension interface that predates MailExtensions.
101 | This interface is what all extensions used prior to Thunderbird 68.
102 | The new MailExtensions APIs which provide tighter control on what extensions can do do not have a keyboard shortcut API.
103 | If you are interested in seeing a keyboard shortcut API added to Thunderbird, please consider contributing code to the project.
104 | Perhaps [this ticket](https://bugzilla.mozilla.org/show_bug.cgi?id=1591730) in the Thunderbird issue tracker could be a starting point.
105 |
106 | To discuss the security considerations related to tbkeys further, it is necessary to review its implementation.
107 | As mentioned in the [intro](#intro), tbkeys relies on the Mousetrap library for managing the keybindings.
108 | The bulk of the logic of tbkeys is in [implementation.js](addon/implementation.js) which is a [MailExtension experiment](https://developer.thunderbird.net/add-ons/mailextensions/experiments).
109 | `implementation.js` sets up the experiment API which can be called by tbkey's standard (restricted in scope) MailExtension to bind keyboard shortcuts to functions (including a null function for unbinding) and to messages to send to other extensions.
110 | `implementation.js` also loads Mousetrap into each Thunderbird window, tweaks the conditions upon which Mousetrap captures a key even to account for Thunderbird specific UI elements, and defines the function that executes what the user specifies for each key binding.
111 | That is all that `implementation.js` does.
112 | It does not access the local file system or any message data and does not access the network.
113 |
114 | One of the command modes tbkeys supports is [eval](#eval).
115 | This mode uses `eval()` to execute arbitrary code provided by the user in `implementation.js` with full access to Thunderbird's internals.
116 | If one does not need to bind to arbitrary code, perhaps there is some security gained by using [tbkeys-lite](#tbkeys-lite) which does not support eval commands.
117 | tbkeys-lite is the version published on [Thunderbird's Add-ons page](https://addons.thunderbird.net/en-US/thunderbird/addon/tbkeys-lite/).
118 | Add-ons published there undergo an independent manual review.
119 | Having that barrier of review between yourself and the developer provides an added layer of security.
120 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2019 Will Shanks.
2 | tbkeys is licensed under a [Mozilla Public License, v. 2.0](http://mozilla.org/MPL/2.0/) (full text below).
3 |
4 | Full license:
5 |
6 | Mozilla Public License Version 2.0
7 | ==================================
8 |
9 | 1. Definitions
10 | --------------
11 |
12 | 1.1. "Contributor"
13 | means each individual or legal entity that creates, contributes to
14 | the creation of, or owns Covered Software.
15 |
16 | 1.2. "Contributor Version"
17 | means the combination of the Contributions of others (if any) used
18 | by a Contributor and that particular Contributor's Contribution.
19 |
20 | 1.3. "Contribution"
21 | means Covered Software of a particular Contributor.
22 |
23 | 1.4. "Covered Software"
24 | means Source Code Form to which the initial Contributor has attached
25 | the notice in Exhibit A, the Executable Form of such Source Code
26 | Form, and Modifications of such Source Code Form, in each case
27 | including portions thereof.
28 |
29 | 1.5. "Incompatible With Secondary Licenses"
30 | means
31 |
32 | (a) that the initial Contributor has attached the notice described
33 | in Exhibit B to the Covered Software; or
34 |
35 | (b) that the Covered Software was made available under the terms of
36 | version 1.1 or earlier of the License, but not also under the
37 | terms of a Secondary License.
38 |
39 | 1.6. "Executable Form"
40 | means any form of the work other than Source Code Form.
41 |
42 | 1.7. "Larger Work"
43 | means a work that combines Covered Software with other material, in
44 | a separate file or files, that is not Covered Software.
45 |
46 | 1.8. "License"
47 | means this document.
48 |
49 | 1.9. "Licensable"
50 | means having the right to grant, to the maximum extent possible,
51 | whether at the time of the initial grant or subsequently, any and
52 | all of the rights conveyed by this License.
53 |
54 | 1.10. "Modifications"
55 | means any of the following:
56 |
57 | (a) any file in Source Code Form that results from an addition to,
58 | deletion from, or modification of the contents of Covered
59 | Software; or
60 |
61 | (b) any new file in Source Code Form that contains any Covered
62 | Software.
63 |
64 | 1.11. "Patent Claims" of a Contributor
65 | means any patent claim(s), including without limitation, method,
66 | process, and apparatus claims, in any patent Licensable by such
67 | Contributor that would be infringed, but for the grant of the
68 | License, by the making, using, selling, offering for sale, having
69 | made, import, or transfer of either its Contributions or its
70 | Contributor Version.
71 |
72 | 1.12. "Secondary License"
73 | means either the GNU General Public License, Version 2.0, the GNU
74 | Lesser General Public License, Version 2.1, the GNU Affero General
75 | Public License, Version 3.0, or any later versions of those
76 | licenses.
77 |
78 | 1.13. "Source Code Form"
79 | means the form of the work preferred for making modifications.
80 |
81 | 1.14. "You" (or "Your")
82 | means an individual or a legal entity exercising rights under this
83 | License. For legal entities, "You" includes any entity that
84 | controls, is controlled by, or is under common control with You. For
85 | purposes of this definition, "control" means (a) the power, direct
86 | or indirect, to cause the direction or management of such entity,
87 | whether by contract or otherwise, or (b) ownership of more than
88 | fifty percent (50%) of the outstanding shares or beneficial
89 | ownership of such entity.
90 |
91 | 2. License Grants and Conditions
92 | --------------------------------
93 |
94 | 2.1. Grants
95 |
96 | Each Contributor hereby grants You a world-wide, royalty-free,
97 | non-exclusive license:
98 |
99 | (a) under intellectual property rights (other than patent or trademark)
100 | Licensable by such Contributor to use, reproduce, make available,
101 | modify, display, perform, distribute, and otherwise exploit its
102 | Contributions, either on an unmodified basis, with Modifications, or
103 | as part of a Larger Work; and
104 |
105 | (b) under Patent Claims of such Contributor to make, use, sell, offer
106 | for sale, have made, import, and otherwise transfer either its
107 | Contributions or its Contributor Version.
108 |
109 | 2.2. Effective Date
110 |
111 | The licenses granted in Section 2.1 with respect to any Contribution
112 | become effective for each Contribution on the date the Contributor first
113 | distributes such Contribution.
114 |
115 | 2.3. Limitations on Grant Scope
116 |
117 | The licenses granted in this Section 2 are the only rights granted under
118 | this License. No additional rights or licenses will be implied from the
119 | distribution or licensing of Covered Software under this License.
120 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
121 | Contributor:
122 |
123 | (a) for any code that a Contributor has removed from Covered Software;
124 | or
125 |
126 | (b) for infringements caused by: (i) Your and any other third party's
127 | modifications of Covered Software, or (ii) the combination of its
128 | Contributions with other software (except as part of its Contributor
129 | Version); or
130 |
131 | (c) under Patent Claims infringed by Covered Software in the absence of
132 | its Contributions.
133 |
134 | This License does not grant any rights in the trademarks, service marks,
135 | or logos of any Contributor (except as may be necessary to comply with
136 | the notice requirements in Section 3.4).
137 |
138 | 2.4. Subsequent Licenses
139 |
140 | No Contributor makes additional grants as a result of Your choice to
141 | distribute the Covered Software under a subsequent version of this
142 | License (see Section 10.2) or under the terms of a Secondary License (if
143 | permitted under the terms of Section 3.3).
144 |
145 | 2.5. Representation
146 |
147 | Each Contributor represents that the Contributor believes its
148 | Contributions are its original creation(s) or it has sufficient rights
149 | to grant the rights to its Contributions conveyed by this License.
150 |
151 | 2.6. Fair Use
152 |
153 | This License is not intended to limit any rights You have under
154 | applicable copyright doctrines of fair use, fair dealing, or other
155 | equivalents.
156 |
157 | 2.7. Conditions
158 |
159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
160 | in Section 2.1.
161 |
162 | 3. Responsibilities
163 | -------------------
164 |
165 | 3.1. Distribution of Source Form
166 |
167 | All distribution of Covered Software in Source Code Form, including any
168 | Modifications that You create or to which You contribute, must be under
169 | the terms of this License. You must inform recipients that the Source
170 | Code Form of the Covered Software is governed by the terms of this
171 | License, and how they can obtain a copy of this License. You may not
172 | attempt to alter or restrict the recipients' rights in the Source Code
173 | Form.
174 |
175 | 3.2. Distribution of Executable Form
176 |
177 | If You distribute Covered Software in Executable Form then:
178 |
179 | (a) such Covered Software must also be made available in Source Code
180 | Form, as described in Section 3.1, and You must inform recipients of
181 | the Executable Form how they can obtain a copy of such Source Code
182 | Form by reasonable means in a timely manner, at a charge no more
183 | than the cost of distribution to the recipient; and
184 |
185 | (b) You may distribute such Executable Form under the terms of this
186 | License, or sublicense it under different terms, provided that the
187 | license for the Executable Form does not attempt to limit or alter
188 | the recipients' rights in the Source Code Form under this License.
189 |
190 | 3.3. Distribution of a Larger Work
191 |
192 | You may create and distribute a Larger Work under terms of Your choice,
193 | provided that You also comply with the requirements of this License for
194 | the Covered Software. If the Larger Work is a combination of Covered
195 | Software with a work governed by one or more Secondary Licenses, and the
196 | Covered Software is not Incompatible With Secondary Licenses, this
197 | License permits You to additionally distribute such Covered Software
198 | under the terms of such Secondary License(s), so that the recipient of
199 | the Larger Work may, at their option, further distribute the Covered
200 | Software under the terms of either this License or such Secondary
201 | License(s).
202 |
203 | 3.4. Notices
204 |
205 | You may not remove or alter the substance of any license notices
206 | (including copyright notices, patent notices, disclaimers of warranty,
207 | or limitations of liability) contained within the Source Code Form of
208 | the Covered Software, except that You may alter any license notices to
209 | the extent required to remedy known factual inaccuracies.
210 |
211 | 3.5. Application of Additional Terms
212 |
213 | You may choose to offer, and to charge a fee for, warranty, support,
214 | indemnity or liability obligations to one or more recipients of Covered
215 | Software. However, You may do so only on Your own behalf, and not on
216 | behalf of any Contributor. You must make it absolutely clear that any
217 | such warranty, support, indemnity, or liability obligation is offered by
218 | You alone, and You hereby agree to indemnify every Contributor for any
219 | liability incurred by such Contributor as a result of warranty, support,
220 | indemnity or liability terms You offer. You may include additional
221 | disclaimers of warranty and limitations of liability specific to any
222 | jurisdiction.
223 |
224 | 4. Inability to Comply Due to Statute or Regulation
225 | ---------------------------------------------------
226 |
227 | If it is impossible for You to comply with any of the terms of this
228 | License with respect to some or all of the Covered Software due to
229 | statute, judicial order, or regulation then You must: (a) comply with
230 | the terms of this License to the maximum extent possible; and (b)
231 | describe the limitations and the code they affect. Such description must
232 | be placed in a text file included with all distributions of the Covered
233 | Software under this License. Except to the extent prohibited by statute
234 | or regulation, such description must be sufficiently detailed for a
235 | recipient of ordinary skill to be able to understand it.
236 |
237 | 5. Termination
238 | --------------
239 |
240 | 5.1. The rights granted under this License will terminate automatically
241 | if You fail to comply with any of its terms. However, if You become
242 | compliant, then the rights granted under this License from a particular
243 | Contributor are reinstated (a) provisionally, unless and until such
244 | Contributor explicitly and finally terminates Your grants, and (b) on an
245 | ongoing basis, if such Contributor fails to notify You of the
246 | non-compliance by some reasonable means prior to 60 days after You have
247 | come back into compliance. Moreover, Your grants from a particular
248 | Contributor are reinstated on an ongoing basis if such Contributor
249 | notifies You of the non-compliance by some reasonable means, this is the
250 | first time You have received notice of non-compliance with this License
251 | from such Contributor, and You become compliant prior to 30 days after
252 | Your receipt of the notice.
253 |
254 | 5.2. If You initiate litigation against any entity by asserting a patent
255 | infringement claim (excluding declaratory judgment actions,
256 | counter-claims, and cross-claims) alleging that a Contributor Version
257 | directly or indirectly infringes any patent, then the rights granted to
258 | You by any and all Contributors for the Covered Software under Section
259 | 2.1 of this License shall terminate.
260 |
261 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
262 | end user license agreements (excluding distributors and resellers) which
263 | have been validly granted by You or Your distributors under this License
264 | prior to termination shall survive termination.
265 |
266 | ************************************************************************
267 | * *
268 | * 6. Disclaimer of Warranty *
269 | * ------------------------- *
270 | * *
271 | * Covered Software is provided under this License on an "as is" *
272 | * basis, without warranty of any kind, either expressed, implied, or *
273 | * statutory, including, without limitation, warranties that the *
274 | * Covered Software is free of defects, merchantable, fit for a *
275 | * particular purpose or non-infringing. The entire risk as to the *
276 | * quality and performance of the Covered Software is with You. *
277 | * Should any Covered Software prove defective in any respect, You *
278 | * (not any Contributor) assume the cost of any necessary servicing, *
279 | * repair, or correction. This disclaimer of warranty constitutes an *
280 | * essential part of this License. No use of any Covered Software is *
281 | * authorized under this License except under this disclaimer. *
282 | * *
283 | ************************************************************************
284 |
285 | ************************************************************************
286 | * *
287 | * 7. Limitation of Liability *
288 | * -------------------------- *
289 | * *
290 | * Under no circumstances and under no legal theory, whether tort *
291 | * (including negligence), contract, or otherwise, shall any *
292 | * Contributor, or anyone who distributes Covered Software as *
293 | * permitted above, be liable to You for any direct, indirect, *
294 | * special, incidental, or consequential damages of any character *
295 | * including, without limitation, damages for lost profits, loss of *
296 | * goodwill, work stoppage, computer failure or malfunction, or any *
297 | * and all other commercial damages or losses, even if such party *
298 | * shall have been informed of the possibility of such damages. This *
299 | * limitation of liability shall not apply to liability for death or *
300 | * personal injury resulting from such party's negligence to the *
301 | * extent applicable law prohibits such limitation. Some *
302 | * jurisdictions do not allow the exclusion or limitation of *
303 | * incidental or consequential damages, so this exclusion and *
304 | * limitation may not apply to You. *
305 | * *
306 | ************************************************************************
307 |
308 | 8. Litigation
309 | -------------
310 |
311 | Any litigation relating to this License may be brought only in the
312 | courts of a jurisdiction where the defendant maintains its principal
313 | place of business and such litigation shall be governed by laws of that
314 | jurisdiction, without reference to its conflict-of-law provisions.
315 | Nothing in this Section shall prevent a party's ability to bring
316 | cross-claims or counter-claims.
317 |
318 | 9. Miscellaneous
319 | ----------------
320 |
321 | This License represents the complete agreement concerning the subject
322 | matter hereof. If any provision of this License is held to be
323 | unenforceable, such provision shall be reformed only to the extent
324 | necessary to make it enforceable. Any law or regulation which provides
325 | that the language of a contract shall be construed against the drafter
326 | shall not be used to construe this License against a Contributor.
327 |
328 | 10. Versions of the License
329 | ---------------------------
330 |
331 | 10.1. New Versions
332 |
333 | Mozilla Foundation is the license steward. Except as provided in Section
334 | 10.3, no one other than the license steward has the right to modify or
335 | publish new versions of this License. Each version will be given a
336 | distinguishing version number.
337 |
338 | 10.2. Effect of New Versions
339 |
340 | You may distribute the Covered Software under the terms of the version
341 | of the License under which You originally received the Covered Software,
342 | or under the terms of any subsequent version published by the license
343 | steward.
344 |
345 | 10.3. Modified Versions
346 |
347 | If you create software not governed by this License, and you want to
348 | create a new license for such software, you may create and use a
349 | modified version of this License if you rename the license and remove
350 | any references to the name of the license steward (except to note that
351 | such modified license differs from this License).
352 |
353 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
354 | Licenses
355 |
356 | If You choose to distribute Source Code Form that is Incompatible With
357 | Secondary Licenses under the terms of this version of the License, the
358 | notice described in Exhibit B of this License must be attached.
359 |
360 | Exhibit A - Source Code Form License Notice
361 | -------------------------------------------
362 |
363 | This Source Code Form is subject to the terms of the Mozilla Public
364 | License, v. 2.0. If a copy of the MPL was not distributed with this
365 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
366 |
367 | If it is not possible or desirable to put the notice in a particular
368 | file, then You may include the notice in a location (such as a LICENSE
369 | file in a relevant directory) where a recipient would be likely to look
370 | for such a notice.
371 |
372 | You may add additional accurate notices of copyright ownership.
373 |
374 | Exhibit B - "Incompatible With Secondary Licenses" Notice
375 | ---------------------------------------------------------
376 |
377 | This Source Code Form is "Incompatible With Secondary Licenses", as
378 | defined by the Mozilla Public License, v. 2.0.
379 |
--------------------------------------------------------------------------------
/addon/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2019 Will Shanks.
2 | tbkeys is licensed under a [Mozilla Public License, v. 2.0](http://mozilla.org/MPL/2.0/) (full text below).
3 |
4 | Full license:
5 |
6 | Mozilla Public License Version 2.0
7 | ==================================
8 |
9 | 1. Definitions
10 | --------------
11 |
12 | 1.1. "Contributor"
13 | means each individual or legal entity that creates, contributes to
14 | the creation of, or owns Covered Software.
15 |
16 | 1.2. "Contributor Version"
17 | means the combination of the Contributions of others (if any) used
18 | by a Contributor and that particular Contributor's Contribution.
19 |
20 | 1.3. "Contribution"
21 | means Covered Software of a particular Contributor.
22 |
23 | 1.4. "Covered Software"
24 | means Source Code Form to which the initial Contributor has attached
25 | the notice in Exhibit A, the Executable Form of such Source Code
26 | Form, and Modifications of such Source Code Form, in each case
27 | including portions thereof.
28 |
29 | 1.5. "Incompatible With Secondary Licenses"
30 | means
31 |
32 | (a) that the initial Contributor has attached the notice described
33 | in Exhibit B to the Covered Software; or
34 |
35 | (b) that the Covered Software was made available under the terms of
36 | version 1.1 or earlier of the License, but not also under the
37 | terms of a Secondary License.
38 |
39 | 1.6. "Executable Form"
40 | means any form of the work other than Source Code Form.
41 |
42 | 1.7. "Larger Work"
43 | means a work that combines Covered Software with other material, in
44 | a separate file or files, that is not Covered Software.
45 |
46 | 1.8. "License"
47 | means this document.
48 |
49 | 1.9. "Licensable"
50 | means having the right to grant, to the maximum extent possible,
51 | whether at the time of the initial grant or subsequently, any and
52 | all of the rights conveyed by this License.
53 |
54 | 1.10. "Modifications"
55 | means any of the following:
56 |
57 | (a) any file in Source Code Form that results from an addition to,
58 | deletion from, or modification of the contents of Covered
59 | Software; or
60 |
61 | (b) any new file in Source Code Form that contains any Covered
62 | Software.
63 |
64 | 1.11. "Patent Claims" of a Contributor
65 | means any patent claim(s), including without limitation, method,
66 | process, and apparatus claims, in any patent Licensable by such
67 | Contributor that would be infringed, but for the grant of the
68 | License, by the making, using, selling, offering for sale, having
69 | made, import, or transfer of either its Contributions or its
70 | Contributor Version.
71 |
72 | 1.12. "Secondary License"
73 | means either the GNU General Public License, Version 2.0, the GNU
74 | Lesser General Public License, Version 2.1, the GNU Affero General
75 | Public License, Version 3.0, or any later versions of those
76 | licenses.
77 |
78 | 1.13. "Source Code Form"
79 | means the form of the work preferred for making modifications.
80 |
81 | 1.14. "You" (or "Your")
82 | means an individual or a legal entity exercising rights under this
83 | License. For legal entities, "You" includes any entity that
84 | controls, is controlled by, or is under common control with You. For
85 | purposes of this definition, "control" means (a) the power, direct
86 | or indirect, to cause the direction or management of such entity,
87 | whether by contract or otherwise, or (b) ownership of more than
88 | fifty percent (50%) of the outstanding shares or beneficial
89 | ownership of such entity.
90 |
91 | 2. License Grants and Conditions
92 | --------------------------------
93 |
94 | 2.1. Grants
95 |
96 | Each Contributor hereby grants You a world-wide, royalty-free,
97 | non-exclusive license:
98 |
99 | (a) under intellectual property rights (other than patent or trademark)
100 | Licensable by such Contributor to use, reproduce, make available,
101 | modify, display, perform, distribute, and otherwise exploit its
102 | Contributions, either on an unmodified basis, with Modifications, or
103 | as part of a Larger Work; and
104 |
105 | (b) under Patent Claims of such Contributor to make, use, sell, offer
106 | for sale, have made, import, and otherwise transfer either its
107 | Contributions or its Contributor Version.
108 |
109 | 2.2. Effective Date
110 |
111 | The licenses granted in Section 2.1 with respect to any Contribution
112 | become effective for each Contribution on the date the Contributor first
113 | distributes such Contribution.
114 |
115 | 2.3. Limitations on Grant Scope
116 |
117 | The licenses granted in this Section 2 are the only rights granted under
118 | this License. No additional rights or licenses will be implied from the
119 | distribution or licensing of Covered Software under this License.
120 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
121 | Contributor:
122 |
123 | (a) for any code that a Contributor has removed from Covered Software;
124 | or
125 |
126 | (b) for infringements caused by: (i) Your and any other third party's
127 | modifications of Covered Software, or (ii) the combination of its
128 | Contributions with other software (except as part of its Contributor
129 | Version); or
130 |
131 | (c) under Patent Claims infringed by Covered Software in the absence of
132 | its Contributions.
133 |
134 | This License does not grant any rights in the trademarks, service marks,
135 | or logos of any Contributor (except as may be necessary to comply with
136 | the notice requirements in Section 3.4).
137 |
138 | 2.4. Subsequent Licenses
139 |
140 | No Contributor makes additional grants as a result of Your choice to
141 | distribute the Covered Software under a subsequent version of this
142 | License (see Section 10.2) or under the terms of a Secondary License (if
143 | permitted under the terms of Section 3.3).
144 |
145 | 2.5. Representation
146 |
147 | Each Contributor represents that the Contributor believes its
148 | Contributions are its original creation(s) or it has sufficient rights
149 | to grant the rights to its Contributions conveyed by this License.
150 |
151 | 2.6. Fair Use
152 |
153 | This License is not intended to limit any rights You have under
154 | applicable copyright doctrines of fair use, fair dealing, or other
155 | equivalents.
156 |
157 | 2.7. Conditions
158 |
159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
160 | in Section 2.1.
161 |
162 | 3. Responsibilities
163 | -------------------
164 |
165 | 3.1. Distribution of Source Form
166 |
167 | All distribution of Covered Software in Source Code Form, including any
168 | Modifications that You create or to which You contribute, must be under
169 | the terms of this License. You must inform recipients that the Source
170 | Code Form of the Covered Software is governed by the terms of this
171 | License, and how they can obtain a copy of this License. You may not
172 | attempt to alter or restrict the recipients' rights in the Source Code
173 | Form.
174 |
175 | 3.2. Distribution of Executable Form
176 |
177 | If You distribute Covered Software in Executable Form then:
178 |
179 | (a) such Covered Software must also be made available in Source Code
180 | Form, as described in Section 3.1, and You must inform recipients of
181 | the Executable Form how they can obtain a copy of such Source Code
182 | Form by reasonable means in a timely manner, at a charge no more
183 | than the cost of distribution to the recipient; and
184 |
185 | (b) You may distribute such Executable Form under the terms of this
186 | License, or sublicense it under different terms, provided that the
187 | license for the Executable Form does not attempt to limit or alter
188 | the recipients' rights in the Source Code Form under this License.
189 |
190 | 3.3. Distribution of a Larger Work
191 |
192 | You may create and distribute a Larger Work under terms of Your choice,
193 | provided that You also comply with the requirements of this License for
194 | the Covered Software. If the Larger Work is a combination of Covered
195 | Software with a work governed by one or more Secondary Licenses, and the
196 | Covered Software is not Incompatible With Secondary Licenses, this
197 | License permits You to additionally distribute such Covered Software
198 | under the terms of such Secondary License(s), so that the recipient of
199 | the Larger Work may, at their option, further distribute the Covered
200 | Software under the terms of either this License or such Secondary
201 | License(s).
202 |
203 | 3.4. Notices
204 |
205 | You may not remove or alter the substance of any license notices
206 | (including copyright notices, patent notices, disclaimers of warranty,
207 | or limitations of liability) contained within the Source Code Form of
208 | the Covered Software, except that You may alter any license notices to
209 | the extent required to remedy known factual inaccuracies.
210 |
211 | 3.5. Application of Additional Terms
212 |
213 | You may choose to offer, and to charge a fee for, warranty, support,
214 | indemnity or liability obligations to one or more recipients of Covered
215 | Software. However, You may do so only on Your own behalf, and not on
216 | behalf of any Contributor. You must make it absolutely clear that any
217 | such warranty, support, indemnity, or liability obligation is offered by
218 | You alone, and You hereby agree to indemnify every Contributor for any
219 | liability incurred by such Contributor as a result of warranty, support,
220 | indemnity or liability terms You offer. You may include additional
221 | disclaimers of warranty and limitations of liability specific to any
222 | jurisdiction.
223 |
224 | 4. Inability to Comply Due to Statute or Regulation
225 | ---------------------------------------------------
226 |
227 | If it is impossible for You to comply with any of the terms of this
228 | License with respect to some or all of the Covered Software due to
229 | statute, judicial order, or regulation then You must: (a) comply with
230 | the terms of this License to the maximum extent possible; and (b)
231 | describe the limitations and the code they affect. Such description must
232 | be placed in a text file included with all distributions of the Covered
233 | Software under this License. Except to the extent prohibited by statute
234 | or regulation, such description must be sufficiently detailed for a
235 | recipient of ordinary skill to be able to understand it.
236 |
237 | 5. Termination
238 | --------------
239 |
240 | 5.1. The rights granted under this License will terminate automatically
241 | if You fail to comply with any of its terms. However, if You become
242 | compliant, then the rights granted under this License from a particular
243 | Contributor are reinstated (a) provisionally, unless and until such
244 | Contributor explicitly and finally terminates Your grants, and (b) on an
245 | ongoing basis, if such Contributor fails to notify You of the
246 | non-compliance by some reasonable means prior to 60 days after You have
247 | come back into compliance. Moreover, Your grants from a particular
248 | Contributor are reinstated on an ongoing basis if such Contributor
249 | notifies You of the non-compliance by some reasonable means, this is the
250 | first time You have received notice of non-compliance with this License
251 | from such Contributor, and You become compliant prior to 30 days after
252 | Your receipt of the notice.
253 |
254 | 5.2. If You initiate litigation against any entity by asserting a patent
255 | infringement claim (excluding declaratory judgment actions,
256 | counter-claims, and cross-claims) alleging that a Contributor Version
257 | directly or indirectly infringes any patent, then the rights granted to
258 | You by any and all Contributors for the Covered Software under Section
259 | 2.1 of this License shall terminate.
260 |
261 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
262 | end user license agreements (excluding distributors and resellers) which
263 | have been validly granted by You or Your distributors under this License
264 | prior to termination shall survive termination.
265 |
266 | ************************************************************************
267 | * *
268 | * 6. Disclaimer of Warranty *
269 | * ------------------------- *
270 | * *
271 | * Covered Software is provided under this License on an "as is" *
272 | * basis, without warranty of any kind, either expressed, implied, or *
273 | * statutory, including, without limitation, warranties that the *
274 | * Covered Software is free of defects, merchantable, fit for a *
275 | * particular purpose or non-infringing. The entire risk as to the *
276 | * quality and performance of the Covered Software is with You. *
277 | * Should any Covered Software prove defective in any respect, You *
278 | * (not any Contributor) assume the cost of any necessary servicing, *
279 | * repair, or correction. This disclaimer of warranty constitutes an *
280 | * essential part of this License. No use of any Covered Software is *
281 | * authorized under this License except under this disclaimer. *
282 | * *
283 | ************************************************************************
284 |
285 | ************************************************************************
286 | * *
287 | * 7. Limitation of Liability *
288 | * -------------------------- *
289 | * *
290 | * Under no circumstances and under no legal theory, whether tort *
291 | * (including negligence), contract, or otherwise, shall any *
292 | * Contributor, or anyone who distributes Covered Software as *
293 | * permitted above, be liable to You for any direct, indirect, *
294 | * special, incidental, or consequential damages of any character *
295 | * including, without limitation, damages for lost profits, loss of *
296 | * goodwill, work stoppage, computer failure or malfunction, or any *
297 | * and all other commercial damages or losses, even if such party *
298 | * shall have been informed of the possibility of such damages. This *
299 | * limitation of liability shall not apply to liability for death or *
300 | * personal injury resulting from such party's negligence to the *
301 | * extent applicable law prohibits such limitation. Some *
302 | * jurisdictions do not allow the exclusion or limitation of *
303 | * incidental or consequential damages, so this exclusion and *
304 | * limitation may not apply to You. *
305 | * *
306 | ************************************************************************
307 |
308 | 8. Litigation
309 | -------------
310 |
311 | Any litigation relating to this License may be brought only in the
312 | courts of a jurisdiction where the defendant maintains its principal
313 | place of business and such litigation shall be governed by laws of that
314 | jurisdiction, without reference to its conflict-of-law provisions.
315 | Nothing in this Section shall prevent a party's ability to bring
316 | cross-claims or counter-claims.
317 |
318 | 9. Miscellaneous
319 | ----------------
320 |
321 | This License represents the complete agreement concerning the subject
322 | matter hereof. If any provision of this License is held to be
323 | unenforceable, such provision shall be reformed only to the extent
324 | necessary to make it enforceable. Any law or regulation which provides
325 | that the language of a contract shall be construed against the drafter
326 | shall not be used to construe this License against a Contributor.
327 |
328 | 10. Versions of the License
329 | ---------------------------
330 |
331 | 10.1. New Versions
332 |
333 | Mozilla Foundation is the license steward. Except as provided in Section
334 | 10.3, no one other than the license steward has the right to modify or
335 | publish new versions of this License. Each version will be given a
336 | distinguishing version number.
337 |
338 | 10.2. Effect of New Versions
339 |
340 | You may distribute the Covered Software under the terms of the version
341 | of the License under which You originally received the Covered Software,
342 | or under the terms of any subsequent version published by the license
343 | steward.
344 |
345 | 10.3. Modified Versions
346 |
347 | If you create software not governed by this License, and you want to
348 | create a new license for such software, you may create and use a
349 | modified version of this License if you rename the license and remove
350 | any references to the name of the license steward (except to note that
351 | such modified license differs from this License).
352 |
353 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
354 | Licenses
355 |
356 | If You choose to distribute Source Code Form that is Incompatible With
357 | Secondary Licenses under the terms of this version of the License, the
358 | notice described in Exhibit B of this License must be attached.
359 |
360 | Exhibit A - Source Code Form License Notice
361 | -------------------------------------------
362 |
363 | This Source Code Form is subject to the terms of the Mozilla Public
364 | License, v. 2.0. If a copy of the MPL was not distributed with this
365 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
366 |
367 | If it is not possible or desirable to put the notice in a particular
368 | file, then You may include the notice in a location (such as a LICENSE
369 | file in a relevant directory) where a recipient would be likely to look
370 | for such a notice.
371 |
372 | You may add additional accurate notices of copyright ownership.
373 |
374 | Exhibit B - "Incompatible With Secondary Licenses" Notice
375 | ---------------------------------------------------------
376 |
377 | This Source Code Form is "Incompatible With Secondary Licenses", as
378 | defined by the Mozilla Public License, v. 2.0.
379 |
--------------------------------------------------------------------------------
/addon/modules/mousetrap.js:
--------------------------------------------------------------------------------
1 | /*global define:false */
2 | /**
3 | * Copyright 2012-2017 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.3
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 | // Events originating from a shadow DOM are re-targetted and `e.target` is the shadow host,
986 | // not the initial event target in the shadow tree. Note that not all events cross the
987 | // shadow boundary.
988 | // For shadow trees with `mode: 'open'`, the initial event target is the first element in
989 | // the event’s composed path. For shadow trees with `mode: 'closed'`, the initial event
990 | // target cannot be obtained.
991 | if ('composedPath' in e && typeof e.composedPath === 'function') {
992 | // For open shadow trees, update `element` so that the following check works.
993 | var initialEventTarget = e.composedPath()[0];
994 | if (initialEventTarget !== e.target) {
995 | element = initialEventTarget;
996 | }
997 | }
998 |
999 | // stop for input, select, and textarea
1000 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
1001 | };
1002 |
1003 | /**
1004 | * exposes _handleKey publicly so it can be overwritten by extensions
1005 | */
1006 | Mousetrap.prototype.handleKey = function() {
1007 | var self = this;
1008 | return self._handleKey.apply(self, arguments);
1009 | };
1010 |
1011 | /**
1012 | * allow custom key mappings
1013 | */
1014 | Mousetrap.addKeycodes = function(object) {
1015 | for (var key in object) {
1016 | if (object.hasOwnProperty(key)) {
1017 | _MAP[key] = object[key];
1018 | }
1019 | }
1020 | _REVERSE_MAP = null;
1021 | };
1022 |
1023 | /**
1024 | * Init the global mousetrap functions
1025 | *
1026 | * This method is needed to allow the global mousetrap functions to work
1027 | * now that mousetrap is a constructor function.
1028 | */
1029 | Mousetrap.init = function() {
1030 | var documentMousetrap = Mousetrap(document);
1031 | for (var method in documentMousetrap) {
1032 | if (method.charAt(0) !== '_') {
1033 | Mousetrap[method] = (function(method) {
1034 | return function() {
1035 | return documentMousetrap[method].apply(documentMousetrap, arguments);
1036 | };
1037 | } (method));
1038 | }
1039 | }
1040 | };
1041 |
1042 | Mousetrap.init();
1043 |
1044 | // expose mousetrap to the global object
1045 | window.Mousetrap = Mousetrap;
1046 |
1047 | // expose as a common js module
1048 | if (typeof module !== 'undefined' && module.exports) {
1049 | module.exports = Mousetrap;
1050 | }
1051 |
1052 | // expose mousetrap as an AMD module
1053 | if (typeof define === 'function' && define.amd) {
1054 | define(function() {
1055 | return Mousetrap;
1056 | });
1057 | }
1058 | }) (typeof window !== 'undefined' ? window : null, typeof window !== 'undefined' ? document : null);
1059 |
--------------------------------------------------------------------------------