├── addon ├── .editorconfig ├── manifest.json ├── options.html ├── schema.json ├── background.js ├── options.js ├── implementation.js ├── LICENSE.md └── modules │ └── mousetrap.js ├── .prettierrc ├── .gitignore ├── .prettierignore ├── .github └── workflows │ └── lint.yml ├── package.json ├── eslint.config.mjs ├── Makefile ├── CHANGELOG.md ├── updates.json ├── CODE_OF_CONDUCT.md ├── README.md └── LICENSE.md /addon/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.js] 2 | indent_style = space 3 | indent_size = 4 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | build/ 4 | addon/chrome/locale/*/zutilo/README.html 5 | *.sw* 6 | *~ 7 | \#*# 8 | *.xpi 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | addon/LICENSE.md 2 | addon/modules/ 3 | build/ 4 | CODE_OF_CONDUCT.md 5 | LICENSE.md 6 | node_modules/ 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Check code for errors and style 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out repo 10 | uses: actions/checkout@v4 11 | - name: Setup node 12 | uses: actions/setup-node@v4 13 | with: 14 | cache: npm 15 | - name: Install dependencies 16 | run: npm ci 17 | - name: Run prettier 18 | run: npm run style 19 | - name: Run eslint 20 | run: npm run lint 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tbkeys", 3 | "version": "2.4.1", 4 | "description": "Thunderbird keys", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs" 8 | }, 9 | "scripts": { 10 | "lint": "eslint", 11 | "style": "prettier --check ." 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/wshanks/tbkeys.git" 16 | }, 17 | "keywords": [], 18 | "author": "Will Shanks", 19 | "license": "MPL-2.0", 20 | "bugs": { 21 | "url": "https://github.com/wshanks/tbkeys/issues" 22 | }, 23 | "homepage": "https://github.com/wshanks/tbkeys#readme", 24 | "devDependencies": { 25 | "eslint": "latest", 26 | "prettier": "latest" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import js from "@eslint/js"; 5 | import { FlatCompat } from "@eslint/eslintrc"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all, 13 | }); 14 | 15 | export default [ 16 | { 17 | ignores: ["**/build", "addon/modules"], 18 | }, 19 | ...compat.extends("eslint:recommended"), 20 | { 21 | languageOptions: { 22 | globals: { 23 | ...globals.browser, 24 | }, 25 | 26 | ecmaVersion: 12, 27 | sourceType: "module", 28 | }, 29 | 30 | rules: {}, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /addon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "browser_specific_settings": { 4 | "gecko": { 5 | "id": "tbkeys@addons.thunderbird.net", 6 | "strict_min_version": "128.0", 7 | "strict_max_version": "*", 8 | "update_url": "https://raw.githubusercontent.com/wshanks/tbkeys/main/updates.json" 9 | } 10 | }, 11 | "name": "tbkeys", 12 | "description": "Custom Thunderbird keybindings", 13 | "author": "Will Shanks", 14 | "version": "2.4.1", 15 | "background": { 16 | "scripts": ["background.js"] 17 | }, 18 | "options_ui": { 19 | "browser_style": true, 20 | "page": "options.html" 21 | }, 22 | "permissions": ["storage"], 23 | "experiment_apis": { 24 | "tbkeys": { 25 | "schema": "schema.json", 26 | "parent": { 27 | "scopes": ["addon_parent"], 28 | "paths": [["tbkeys"]], 29 | "script": "implementation.js" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /addon/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 26 | 29 | 30 | 31 | 34 | 42 | 43 |
18 | Settings 19 |
24 | 25 | 27 | 28 |
32 | 33 | 35 | 41 |
44 | 45 | 46 | 51 | 56 | 61 | 62 |
47 |
48 | 49 |
50 |
52 |
53 | 54 |
55 |
57 |
58 | 59 |
60 |
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 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](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 | --------------------------------------------------------------------------------