├── .gitignore ├── icon.ai ├── promo.png ├── extension ├── icon.png ├── _locales │ ├── es │ │ └── messages.json │ └── en │ │ └── messages.json ├── vendor │ ├── mousetrap-global-bind.js │ └── mousetrap.js ├── manifest.json ├── background.js ├── content.js ├── content.css └── dark-mode.css ├── promo-1400x560.png ├── promo-920x680.png ├── chrome-screenshot-1.png ├── chrome-screenshot-2.png ├── firefox-screenshot-1.png ├── firefox-screenshot-2.png ├── .gitattributes ├── .editorconfig ├── package.json ├── license └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | build 4 | -------------------------------------------------------------------------------- /icon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/refined-wikipedia/HEAD/icon.ai -------------------------------------------------------------------------------- /promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/refined-wikipedia/HEAD/promo.png -------------------------------------------------------------------------------- /extension/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/refined-wikipedia/HEAD/extension/icon.png -------------------------------------------------------------------------------- /promo-1400x560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/refined-wikipedia/HEAD/promo-1400x560.png -------------------------------------------------------------------------------- /promo-920x680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/refined-wikipedia/HEAD/promo-920x680.png -------------------------------------------------------------------------------- /chrome-screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/refined-wikipedia/HEAD/chrome-screenshot-1.png -------------------------------------------------------------------------------- /chrome-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/refined-wikipedia/HEAD/chrome-screenshot-2.png -------------------------------------------------------------------------------- /firefox-screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/refined-wikipedia/HEAD/firefox-screenshot-1.png -------------------------------------------------------------------------------- /firefox-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ismamz/refined-wikipedia/HEAD/firefox-screenshot-2.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.ai binary 3 | readme.md merge=union 4 | extension/content.css merge=union 5 | -------------------------------------------------------------------------------- /extension/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "message": "Fuerza la versión web móvil de Wikipedia y mejora su interfaz.", 4 | "description": "Descripción de la extensión." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /extension/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "message": "Enforces the mobile web version of Wikipedia and improves its interface.", 4 | "description": "Description of the extension." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /extension/vendor/mousetrap-global-bind.js: -------------------------------------------------------------------------------- 1 | (function(a){var c={},d=a.prototype.stopCallback;a.prototype.stopCallback=function(e,b,a,f){return this.paused?!0:c[a]||c[f]?!1:d.call(this,e,b,a)};a.prototype.bindGlobal=function(a,b,d){this.bind(a,b,d);if(a instanceof Array)for(b=0;b { 6 | if (req.method === 'getMode') { 7 | respond({darkMode: localStorage.darkMode}); 8 | } 9 | 10 | if (req.method === 'setMode') { 11 | localStorage.darkMode = req.darkMode; 12 | respond({darkMode: localStorage.darkMode}); 13 | } 14 | }); 15 | 16 | browser.webRequest.onBeforeRequest.addListener(details => { 17 | if (details.method !== 'GET') { 18 | return; 19 | } 20 | 21 | const url = new URL(details.url); 22 | 23 | if (url.host.includes('.m.')) { 24 | return; 25 | } 26 | 27 | let lang = url.hostname.split('.')[0]; 28 | 29 | if (lang === 'www') { 30 | return; 31 | } 32 | 33 | if (lang === 'm' || lang === 'www' || lang === 'wikipedia') { 34 | lang = 'www'; 35 | } else { 36 | lang += '.m'; 37 | } 38 | 39 | url.hostname = lang + '.wikipedia.org'; 40 | 41 | return { 42 | redirectUrl: url.href 43 | }; 44 | }, { 45 | urls: [ 46 | 'https://wikipedia.org/*', 47 | 'https://*.wikipedia.org/*' 48 | ], 49 | types: [ 50 | 'main_frame' 51 | ] 52 | }, [ 53 | 'blocking' 54 | ]); 55 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Ismael Martínez (isma.uy) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Refined Wikipedia 2 | 3 | > This Chrome and Firefox extension enforce a mobile version of [Wikipedia](https://wikipedia.org) and improve its interface for a better experience on desktop. It's strongly inspired by [Refined Twitter](https://github.com/sindresorhus/refined-twitter) and [Refined GitHub](https://github.com/sindresorhus/refined-github) extensions created by [Sindre Sorhus](https://github.com/sindresorhus). 4 | 5 | New features, interface tweaks or more keyboard shortcuts are welcome. You can send a [Pull Request](https://github.com/ismamz/refined-wikipedia/pull/new/master). 6 | 7 | It's available in all languages. If you find an error, please notify. 8 | 9 | 10 | #### Chrome 11 | 12 | 13 | 14 | 15 | --- 16 | 17 | 18 | #### Firefox 19 | 20 | 21 | 22 | 23 | 24 | ## Highlights 25 | 26 | - Dark Mode 🕶 27 | - Increase font size 👓 28 | - Resize width container 29 | - Adjust Table of Contents 30 | - Many [keybord shortcuts](#keyboard-shortcuts)! ⌨️ 31 | 32 | 33 | ## Install 34 | 35 | ### Google Chrome Google Chrome 36 | 37 | Install it from the [Chrome Web Store](https://chrome.google.com/webstore/detail/refined-wikipedia/cnmnmlclbofploblcanilidpmklleppe) or [manually](http://superuser.com/a/247654/6877). 38 | 39 | ### Mozilla Firefox Firefox 40 | 41 | Install it from [Firefox Add-ons](https://addons.mozilla.org/es/firefox/addon/wikipedia-refined/). 42 | 43 | 44 | ## Keyboard shortcuts 45 | 46 | - Go to Search: f 47 | - Go to Home: g h 48 | - Go to Top (scroll): g t 49 | - Go to Back in History: backspace or shift+ 50 | - Go to Next page in History: shift+ 51 | - Toggle Dark Mode: d 52 | - Open/Close Main Menu: shift+m 53 | - Open/Close Language Selector: shift+l 54 | - Select Suggested Language: 1 (after open Language Selector) 55 | - Focus on First Search Result: 1 (after search) 56 | - Scroll Up: w 57 | - Scroll Down: s 58 | 59 | 60 | ## Related 61 | 62 | - [Refined GitHub](https://github.com/sindresorhus/refined-github) - Like this, but for GitHub 63 | - [Refined Twitter](https://github.com/sindresorhus/refined-twitter) - Like this, but for Twitter 64 | - [Simplify Gmail](https://chrome.google.com/webstore/detail/simplify-gmail/pbmlfaiicoikhdbjagjbglnbfcbcojpj) - Simplifies Gmail interface to the bare minimum 65 | 66 | [Upvote on Product Hunt 👍](https://www.producthunt.com/posts/refined-wikipedia) 67 | 68 | ## License 69 | 70 | MIT © [Ismael Martínez](https://isma.uy) 71 | -------------------------------------------------------------------------------- /extension/content.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* globals Mousetrap */ 4 | const $ = document.querySelector.bind(document); 5 | 6 | function registerShortcuts() { 7 | // Go to search 8 | Mousetrap.bindGlobal('f', () => { 9 | const searchInputFaux = $('input#searchInput'); 10 | const overlayEnabled = $('html.overlay-enabled'); 11 | 12 | if (!overlayEnabled) { 13 | if (searchInputFaux) { 14 | searchInputFaux.click(); 15 | return false; 16 | } 17 | } 18 | }); 19 | 20 | // Go home 21 | Mousetrap.bind('g h', () => { 22 | $('a[data-event-name="home"]').click(); 23 | }); 24 | 25 | // Toggle dark mode 26 | Mousetrap.bind('d', () => { 27 | toggleDarkMode(); 28 | }); 29 | 30 | // Open / Close main menu 31 | Mousetrap.bindGlobal('shift+m', () => { 32 | const btn = $('a#mw-mf-main-menu-button'); 33 | 34 | if (btn) { 35 | btn.click(); 36 | } 37 | }); 38 | 39 | // Open / Close language selector 40 | Mousetrap.bindGlobal('shift+l', () => { 41 | const btn = $('.language-selector'); 42 | 43 | if (btn) { 44 | btn.click(); 45 | } 46 | }); 47 | 48 | // Change to first suggested language after open language selector 49 | Mousetrap.bindGlobal('1', () => { 50 | const suggestedLang = $('.suggested-languages li:first-child a'); 51 | 52 | if (suggestedLang) { 53 | suggestedLang.click(); 54 | } 55 | 56 | const results = $('.overlay-enabled .results'); 57 | 58 | if (results) { 59 | $('.page-list li a').focus(); 60 | } 61 | }); 62 | 63 | // Close search and language selector overlay 64 | Mousetrap.bindGlobal('esc', () => { 65 | const cancel = $('button.cancel'); 66 | 67 | if (cancel) { 68 | cancel.click(); 69 | } 70 | }); 71 | 72 | // Go top 73 | Mousetrap.bind('g t', () => { 74 | window.scrollTo(0, 0); 75 | }); 76 | 77 | // Scroll up 78 | Mousetrap.bind('w', () => { 79 | window.scrollBy(0, -400); 80 | }); 81 | 82 | // Scroll down 83 | Mousetrap.bind('s', () => { 84 | window.scrollBy(0, 400); 85 | }); 86 | 87 | // Enable backspace to go back (and shift+left) 88 | // NOTE: Backspace on search input? 89 | Mousetrap.bindGlobal(['shift+left'], () => { 90 | window.history.back(); 91 | }); 92 | 93 | // Go to next in browser history 94 | Mousetrap.bindGlobal('shift+right', () => { 95 | window.history.go(1); 96 | }); 97 | } 98 | 99 | // Toggle Dark Mode 100 | // ============================================================================ 101 | 102 | function getMode() { 103 | return new Promise(resolve => { 104 | chrome.runtime.sendMessage({method: 'getMode'}, res => { 105 | // Values are being passed back as strings, this converts to accurate boolean 106 | resolve(res.darkMode === 'true'); 107 | }); 108 | }); 109 | } 110 | 111 | function setMode(newMode) { 112 | return new Promise(resolve => { 113 | chrome.runtime.sendMessage({ 114 | method: 'setMode', 115 | darkMode: newMode 116 | }, res => { 117 | // Values are being passed back as strings, this converts to accurate boolean 118 | resolve(res.darkMode === 'true'); 119 | }); 120 | }); 121 | } 122 | 123 | function toggleDarkMode() { 124 | getMode().then(current => { 125 | setMode(!current); 126 | }).then(applyMode()); 127 | } 128 | 129 | function applyMode(isDark) { 130 | document.documentElement.classList.toggle('dark-mode', isDark); 131 | } 132 | 133 | // Initialization 134 | // ============================================================================ 135 | 136 | function init() { 137 | registerShortcuts(); 138 | 139 | // Apply dark mode with local storage value 140 | getMode().then(applyMode); 141 | } 142 | 143 | document.addEventListener('DOMContentLoaded', () => { 144 | init(); 145 | }); 146 | -------------------------------------------------------------------------------- /extension/content.css: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Refined Wikipedia 3 | https://github.com/ismamz/refined-wikipedia 4 | ========================================================================== */ 5 | 6 | /* Global Styles 7 | ========================================================================== */ 8 | 9 | html { 10 | scroll-behavior: smooth !important; 11 | } 12 | 13 | /* global font-size */ 14 | #mw-mf-viewport { 15 | font-size: 18px !important; 16 | } 17 | 18 | /* global max-width */ 19 | #mw-mf-page-center .mw-mf-banner, .banner-container, .header, .page-header-bar, .content-header, .overlay-header, .content, .overlay-content, .content-unstyled, .pre-content, .post-content, .content-overlay, #mw-mf-page-center .pointer-overlay { 20 | max-width: 45em !important; 21 | } 22 | 23 | .language-overlay .panel-body, .language-overlay .overlay-content-body { 24 | max-width: 45em !important; 25 | } 26 | 27 | .feature-footer-v2, 28 | .feature-footer-v2 #mw-mf-page-center { 29 | background-color: transparent !important; 30 | } 31 | 32 | 33 | 34 | /* Main Title 35 | ========================================================================== */ 36 | 37 | /* increase vertical spacing */ 38 | .pre-content { 39 | margin: 1em auto !important; 40 | } 41 | 42 | .feature-page-action-bar-v2 .heading-holder { 43 | padding-bottom: 1.5em !important; 44 | } 45 | 46 | /* increase font size */ 47 | h1#section_0 { 48 | font-size: 2.5em !important; 49 | } 50 | 51 | 52 | 53 | /* TOC 54 | ========================================================================== */ 55 | 56 | /* toc box */ 57 | .toc-mobile { 58 | width: 100% !important; 59 | } 60 | 61 | .toc-mobile #toc-collapsible-block-0 > ul { 62 | font-weight: bold !important; 63 | list-style: disc !important; 64 | } 65 | 66 | .toc-mobile #toc-collapsible-block-0 > ul > li > ul { 67 | font-weight: normal !important; 68 | list-style: circle !important; 69 | } 70 | 71 | .toc-mobile #toc-collapsible-block-0 > ul > li > ul > li > ul { 72 | font-style: italic !important; 73 | list-style: square !important; 74 | } 75 | 76 | .toc-mobile ul ul ul { 77 | margin-top: 0 !important; 78 | margin-bottom: 0 !important; 79 | } 80 | 81 | 82 | 83 | /* Misc 84 | ========================================================================== */ 85 | /* thumb caption font size */ 86 | .thumbcaption { 87 | font-size: 0.75em !important; 88 | } 89 | 90 | /* add margin bottom to lists of sections */ 91 | @media (min-width: 720px) { 92 | [class^="mf-section-"] ul { 93 | margin-bottom: 1.5em !important; 94 | } 95 | } 96 | 97 | /* make wider tables */ 98 | .content table.wikitable { 99 | width: 1080px !important; 100 | margin-left: -135px !important; /* 1080 - 810 / 2 */ 101 | } 102 | 103 | /* adjust flags in table */ 104 | /* NOTE: Review several cases */ 105 | .content table .flagicon { 106 | float: left !important; 107 | margin-right: 10px !important; 108 | } 109 | 110 | 111 | 112 | /* Info Boxes 113 | ========================================================================== */ 114 | 115 | @media (min-width: 720px) { 116 | /* improve style for infobox table */ 117 | .mf-section-0 table.infobox { 118 | font-size: 13px !important; 119 | } 120 | 121 | .mf-section-0 table.infobox tr th[scope="row"], 122 | .mf-section-0 table.infobox tr td:first-child:not([colspan="3"]) { 123 | text-align: right !important; 124 | background-color: #f3f3f3 !important; 125 | line-height: 1.35 !important; 126 | } 127 | 128 | .mf-section-0 table.infobox tr th, 129 | .mf-section-0 table.infobox tr td { 130 | padding: 0.5em !important; 131 | } 132 | } 133 | 134 | html blockquote.citado { 135 | font-size: 110% !important; 136 | } 137 | 138 | html .references { 139 | font-size: 15px; 140 | } 141 | -------------------------------------------------------------------------------- /extension/dark-mode.css: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Refined Wikipedia (Dark Mode) 3 | https://github.com/ismamz/refined-wikipedia 4 | ========================================================================== */ 5 | 6 | /** 7 | * Color Pallete 8 | * Based on https://github.com/sindresorhus/hyperterm-snazzy 9 | */ 10 | 11 | html.dark-mode { 12 | --background-color: #282a36; 13 | --background-alt-color: hsla(231, 15%, 16%, 1); 14 | --foreground-color: #eff0eb; 15 | --link-color: #57c7ff; 16 | --new-link-color: #ff6ac1; 17 | --yellow: #f3f99d; 18 | --border-color: #222430; 19 | --cursor-color: #97979b; 20 | --icon-filter: contrast(0) sepia(100%) hue-rotate(0) brightness(20) saturate(1); 21 | --green: #5af78e; 22 | --red: #ff5c57; 23 | } 24 | 25 | /* Global 26 | ========================================================================== */ 27 | 28 | /* improved text rendering */ 29 | html.dark-mode * { 30 | -webkit-font-smoothing: antialiased !important; 31 | } 32 | 33 | html .mw-body { 34 | transition: background-color .2s ease-in; 35 | } 36 | 37 | 38 | /* background and foreground colors */ 39 | html.dark-mode .mw-body, 40 | html.dark-mode body, 41 | html.dark-mode #mw-mf-page-center { 42 | background-color: var(--background-color) !important; 43 | color: var(--foreground-color) !important; 44 | } 45 | 46 | /* links */ 47 | html.dark-mode #mw-mf-page-center a, 48 | html.dark-mode .overlay-content a { 49 | color: var(--link-color) !important; 50 | } 51 | 52 | /* header style */ 53 | html.dark-mode .header { 54 | border-top: 0 !important; 55 | } 56 | 57 | html.dark-mode .branding-box { 58 | filter: var(--icon-filter); 59 | } 60 | 61 | html.dark-mode .header-container { 62 | background-color: var(--background-alt-color) !important; 63 | border-bottom: 0; 64 | } 65 | 66 | /* toc */ 67 | html.dark-mode .toc-mobile { 68 | background-color: var(--border-color); 69 | border: 1px solid var(--cursor-color); 70 | } 71 | 72 | /* thumbcaption color */ 73 | html.dark-mode .thumbcaption { 74 | color: var(--cursor-color); 75 | } 76 | 77 | /* math equations */ 78 | html.dark-mode .mwe-math-fallback-image-inline { 79 | background-color: #fff !important; 80 | padding: 2px !important; 81 | border-radius: 2px !important; 82 | } 83 | 84 | html.dark-mode .wikidata-description { 85 | color: var(--cursor-color); 86 | } 87 | 88 | /* images */ 89 | html.dark-mode .thumbimage { 90 | background-color: #fff !important; 91 | padding: 2px !important; 92 | border-radius: 2px !important; 93 | } 94 | 95 | html.dark-mode .hatnote, 96 | html.dark-mode .dablink, 97 | html.dark-mode .rellink { 98 | color: var(--yellow); 99 | background-color: var(--border-color); 100 | } 101 | 102 | 103 | 104 | /* Misc 105 | ========================================================================== */ 106 | 107 | /* buttons */ 108 | html.dark-mode #mw-mf-page-center a.mw-ui-button, 109 | html.dark-mode .mw-ui-button { 110 | background-color: var(--border-color); 111 | color: #fff !important; 112 | } 113 | 114 | /* replace inline gray for yellow */ 115 | html.dark-mode span[style="color:#555;"] { 116 | color: var(--yellow) !important; 117 | } 118 | 119 | /* overlay on menu open */ 120 | html.dark-mode body.navigation-enabled .transparent-shield { 121 | background: rgba(0, 0, 0, .7); 122 | } 123 | 124 | html.dark-mode #mw-mf-page-left { 125 | background: var(--background-color); 126 | } 127 | 128 | html.dark-mode #mw-mf-page-left ul li { 129 | background: transparent; 130 | border-top: none; 131 | } 132 | 133 | #mw-mf-page-left ul.hlist li a { 134 | color: var(--link-color) !important; 135 | } 136 | 137 | /* invert colours of UI icons */ 138 | html.dark-mode .mw-ui-icon, 139 | html.dark-mode a.external { 140 | filter: var(--icon-filter); 141 | } 142 | 143 | /* code blocks */ 144 | html.dark-mode .mw-highlight { 145 | background-color: var(--background-color); 146 | } 147 | 148 | /* inline gallery */ 149 | html.dark-mode .mw-parser-output .mod-gallery .gallerybox div { 150 | background-color: var(--background-color) !important; 151 | } 152 | 153 | html.dark-mode li.gallerybox div.thumb { 154 | background: var(--background-alt-color) !important; 155 | border: none !important; 156 | } 157 | 158 | /* overlay */ 159 | html.dark-mode .overlay-header-container { 160 | background-color: var(--background-alt-color); 161 | } 162 | 163 | html.dark-mode .overlay.visible { 164 | background-color: var(--background-color); 165 | } 166 | 167 | 168 | /* Search Results 169 | ========================================================================== */ 170 | 171 | /* search overlay */ 172 | html.dark-mode .search-overlay, 173 | html.dark-mode .overlay-content, 174 | html.dark-mode .overlay-content-body { 175 | background-color: var(--background-color); 176 | color: var(--foreground-color); 177 | } 178 | 179 | /* search results data */ 180 | html.dark-mode .mw-search-result-data { 181 | color: #9aedfe; 182 | } 183 | 184 | /* background search result */ 185 | html.dark-mode .page-list { 186 | background-color: var(--border-color); 187 | } 188 | 189 | /* text for search results */ 190 | html.dark-mode.page-summary h2, 191 | html.dark-mode .page-summary h3 { 192 | color: var(--link-color); 193 | } 194 | 195 | /* Search Did You Mean */ 196 | html.dark-mode .searchdidyoumean { 197 | color: #ff5c57; 198 | } 199 | 200 | html.dark-mode .site-link-list a { 201 | background-color: var(--border-color); 202 | color: var(--foreground-color); 203 | } 204 | 205 | html.dark-mode .site-link-list a .autonym { 206 | color: #fff; 207 | } 208 | 209 | 210 | /* Infobox 211 | ========================================================================== */ 212 | 213 | /* preserve original colors */ 214 | html.dark-mode table.infobox { 215 | color: #000 !important; 216 | } 217 | 218 | /* preserve original colors for links */ 219 | html.dark-mode #mw-mf-page-center table.infobox a { 220 | color: #0645ad !important; 221 | } 222 | 223 | /* tables inside content */ 224 | html.dark-mode .content table.wikitable > tr > th, 225 | html.dark-mode .content table.wikitable > * > tr > th { 226 | background-color: #222430; 227 | } 228 | 229 | 230 | /* Article Meta Boxes 231 | ========================================================================== */ 232 | 233 | html.dark-mode .ambox-learn-more { 234 | color: var(--link-color) !important; 235 | } 236 | 237 | html.dark-mode .ambox, 238 | html.dark-mode .mbox-small { 239 | background: var(--background-alt-color) !important; 240 | color: var(--foreground-color) !important; 241 | } 242 | 243 | html.dark-mode .ambox-learn-more:before { 244 | background-image: linear-gradient(to right, rgba(255, 255, 255, 0), var(--background-alt-color) 50%) !important; 245 | } 246 | 247 | 248 | /* Footer 249 | ========================================================================== */ 250 | 251 | /* external links on article footer */ 252 | /* html.dark-mode table.mbox-small { 253 | background-color: var(--background-color) !important; 254 | } */ 255 | 256 | /* logo */ 257 | html.dark-mode .read-more-container + h2 img { 258 | filter: var(--icon-filter); 259 | } 260 | 261 | /* main container */ 262 | html.dark-mode .minerva-footer { 263 | background-color: var(--background-alt-color) !important; 264 | border-top: solid 1px hsla(230, 15%, 8%, 1) 265 | } 266 | 267 | /* last modified */ 268 | html.dark-mode .last-modified-bar { 269 | background-color: var(--background-alt-color) !important; 270 | border-bottom: solid 1px hsla(230, 15%, 8%, 1); 271 | } 272 | 273 | /* footer cards */ 274 | html.dark-mode .ext-related-articles-card { 275 | background-color: var(--background-color) !important; 276 | border: 1px solid hsla(0, 0%, 0%, 1) !important; 277 | } 278 | 279 | html.dark-mode .ext-related-articles-card-detail h3:after { 280 | background-image: linear-gradient(to right, rgba(255, 255, 255, 0), var(--background-color) 50%); 281 | } 282 | 283 | /* */ 284 | html.dark-mode .heading-holder .tagline { 285 | color: var(--foreground-color) !important; 286 | opacity: .85; 287 | } 288 | 289 | html.dark-mode blockquote { 290 | border-left-color: var(--yellow) !important; 291 | } 292 | 293 | html.dark-mode .content .mw-parser-output > h2, 294 | html.dark-mode .content .section-heading { 295 | border-bottom-color: hsla(230, 15%, 50%, .6); 296 | } 297 | 298 | html.dark-mode .mw-parser-output .mw-authority-control .mw-mf-linked-projects { 299 | border: 1px solid var(--border-color); 300 | background-color: var(--background-alt-color); 301 | color: var(--foreground-color); 302 | } 303 | 304 | html.dark-mode .page-actions-menu { 305 | border-top: 1px solid hsla(230, 15%, 50%, .6); 306 | border-bottom: 1px solid hsla(230, 15%, 50%, .6); 307 | } 308 | 309 | 310 | html.dark-mode .drawer { 311 | background-color: var(--background-alt-color); 312 | } 313 | 314 | html.dark-mode a:not([href]) { 315 | color: var(--foreground-color); 316 | } 317 | 318 | 319 | html.dark-mode #mw-mf-page-center a.new, 320 | html.dark-mode #mw-mf-page-center a.new:visited, 321 | html.dark-mode #mw-mf-page-center a.new:hover { 322 | color: var(--new-link-color) !important; 323 | } 324 | 325 | 326 | /* message box */ 327 | html.dark-mode #bodyContent .panel .content { 328 | background-color: var(--background-color); 329 | color: var(--foreground-color); 330 | } 331 | 332 | html.dark-mode table:not(.infobox)[style] { 333 | background-color: transparent !important; 334 | } 335 | -------------------------------------------------------------------------------- /extension/vendor/mousetrap.js: -------------------------------------------------------------------------------- 1 | /*global define:false */ 2 | /** 3 | * Copyright 2015 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.5.3 21 | * @url craig.is/killing/mice 22 | */ 23 | (function(window, document, undefined) { 24 | 25 | /** 26 | * mapping of special keycodes to their corresponding keys 27 | * 28 | * everything in this dictionary cannot use keypress events 29 | * so it has to be here to map to the correct keycodes for 30 | * keyup/keydown events 31 | * 32 | * @type {Object} 33 | */ 34 | var _MAP = { 35 | 8: 'backspace', 36 | 9: 'tab', 37 | 13: 'enter', 38 | 16: 'shift', 39 | 17: 'ctrl', 40 | 18: 'alt', 41 | 20: 'capslock', 42 | 27: 'esc', 43 | 32: 'space', 44 | 33: 'pageup', 45 | 34: 'pagedown', 46 | 35: 'end', 47 | 36: 'home', 48 | 37: 'left', 49 | 38: 'up', 50 | 39: 'right', 51 | 40: 'down', 52 | 45: 'ins', 53 | 46: 'del', 54 | 91: 'meta', 55 | 93: 'meta', 56 | 224: 'meta' 57 | }; 58 | 59 | /** 60 | * mapping for special characters so they can support 61 | * 62 | * this dictionary is only used incase you want to bind a 63 | * keyup or keydown event to one of these keys 64 | * 65 | * @type {Object} 66 | */ 67 | var _KEYCODE_MAP = { 68 | 106: '*', 69 | 107: '+', 70 | 109: '-', 71 | 110: '.', 72 | 111 : '/', 73 | 186: ';', 74 | 187: '=', 75 | 188: ',', 76 | 189: '-', 77 | 190: '.', 78 | 191: '/', 79 | 192: '`', 80 | 219: '[', 81 | 220: '\\', 82 | 221: ']', 83 | 222: '\'' 84 | }; 85 | 86 | /** 87 | * this is a mapping of keys that require shift on a US keypad 88 | * back to the non shift equivelents 89 | * 90 | * this is so you can use keyup events with these keys 91 | * 92 | * note that this will only work reliably on US keyboards 93 | * 94 | * @type {Object} 95 | */ 96 | var _SHIFT_MAP = { 97 | '~': '`', 98 | '!': '1', 99 | '@': '2', 100 | '#': '3', 101 | '$': '4', 102 | '%': '5', 103 | '^': '6', 104 | '&': '7', 105 | '*': '8', 106 | '(': '9', 107 | ')': '0', 108 | '_': '-', 109 | '+': '=', 110 | ':': ';', 111 | '\"': '\'', 112 | '<': ',', 113 | '>': '.', 114 | '?': '/', 115 | '|': '\\' 116 | }; 117 | 118 | /** 119 | * this is a list of special strings you can use to map 120 | * to modifier keys when you specify your keyboard shortcuts 121 | * 122 | * @type {Object} 123 | */ 124 | var _SPECIAL_ALIASES = { 125 | 'option': 'alt', 126 | 'command': 'meta', 127 | 'return': 'enter', 128 | 'escape': 'esc', 129 | 'plus': '+', 130 | 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl' 131 | }; 132 | 133 | /** 134 | * variable to store the flipped version of _MAP from above 135 | * needed to check if we should use keypress or not when no action 136 | * is specified 137 | * 138 | * @type {Object|undefined} 139 | */ 140 | var _REVERSE_MAP; 141 | 142 | /** 143 | * loop through the f keys, f1 to f19 and add them to the map 144 | * programatically 145 | */ 146 | for (var i = 1; i < 20; ++i) { 147 | _MAP[111 + i] = 'f' + i; 148 | } 149 | 150 | /** 151 | * loop through to map numbers on the numeric keypad 152 | */ 153 | for (i = 0; i <= 9; ++i) { 154 | _MAP[i + 96] = i; 155 | } 156 | 157 | /** 158 | * cross browser add event method 159 | * 160 | * @param {Element|HTMLDocument} object 161 | * @param {string} type 162 | * @param {Function} callback 163 | * @returns void 164 | */ 165 | function _addEvent(object, type, callback) { 166 | if (object.addEventListener) { 167 | object.addEventListener(type, callback, false); 168 | return; 169 | } 170 | 171 | object.attachEvent('on' + type, callback); 172 | } 173 | 174 | /** 175 | * takes the event and returns the key character 176 | * 177 | * @param {Event} e 178 | * @return {string} 179 | */ 180 | function _characterFromEvent(e) { 181 | 182 | // for keypress events we should return the character as is 183 | if (e.type == 'keypress') { 184 | var character = String.fromCharCode(e.which); 185 | 186 | // if the shift key is not pressed then it is safe to assume 187 | // that we want the character to be lowercase. this means if 188 | // you accidentally have caps lock on then your key bindings 189 | // will continue to work 190 | // 191 | // the only side effect that might not be desired is if you 192 | // bind something like 'A' cause you want to trigger an 193 | // event when capital A is pressed caps lock will no longer 194 | // trigger the event. shift+a will though. 195 | if (!e.shiftKey) { 196 | character = character.toLowerCase(); 197 | } 198 | 199 | return character; 200 | } 201 | 202 | // for non keypress events the special maps are needed 203 | if (_MAP[e.which]) { 204 | return _MAP[e.which]; 205 | } 206 | 207 | if (_KEYCODE_MAP[e.which]) { 208 | return _KEYCODE_MAP[e.which]; 209 | } 210 | 211 | // if it is not in the special map 212 | 213 | // with keydown and keyup events the character seems to always 214 | // come in as an uppercase character whether you are pressing shift 215 | // or not. we should make sure it is always lowercase for comparisons 216 | return String.fromCharCode(e.which).toLowerCase(); 217 | } 218 | 219 | /** 220 | * checks if two arrays are equal 221 | * 222 | * @param {Array} modifiers1 223 | * @param {Array} modifiers2 224 | * @returns {boolean} 225 | */ 226 | function _modifiersMatch(modifiers1, modifiers2) { 227 | return modifiers1.sort().join(',') === modifiers2.sort().join(','); 228 | } 229 | 230 | /** 231 | * takes a key event and figures out what the modifiers are 232 | * 233 | * @param {Event} e 234 | * @returns {Array} 235 | */ 236 | function _eventModifiers(e) { 237 | var modifiers = []; 238 | 239 | if (e.shiftKey) { 240 | modifiers.push('shift'); 241 | } 242 | 243 | if (e.altKey) { 244 | modifiers.push('alt'); 245 | } 246 | 247 | if (e.ctrlKey) { 248 | modifiers.push('ctrl'); 249 | } 250 | 251 | if (e.metaKey) { 252 | modifiers.push('meta'); 253 | } 254 | 255 | return modifiers; 256 | } 257 | 258 | /** 259 | * prevents default for this event 260 | * 261 | * @param {Event} e 262 | * @returns void 263 | */ 264 | function _preventDefault(e) { 265 | if (e.preventDefault) { 266 | e.preventDefault(); 267 | return; 268 | } 269 | 270 | e.returnValue = false; 271 | } 272 | 273 | /** 274 | * stops propogation for this event 275 | * 276 | * @param {Event} e 277 | * @returns void 278 | */ 279 | function _stopPropagation(e) { 280 | if (e.stopPropagation) { 281 | e.stopPropagation(); 282 | return; 283 | } 284 | 285 | e.cancelBubble = true; 286 | } 287 | 288 | /** 289 | * determines if the keycode specified is a modifier key or not 290 | * 291 | * @param {string} key 292 | * @returns {boolean} 293 | */ 294 | function _isModifier(key) { 295 | return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; 296 | } 297 | 298 | /** 299 | * reverses the map lookup so that we can look for specific keys 300 | * to see what can and can't use keypress 301 | * 302 | * @return {Object} 303 | */ 304 | function _getReverseMap() { 305 | if (!_REVERSE_MAP) { 306 | _REVERSE_MAP = {}; 307 | for (var key in _MAP) { 308 | 309 | // pull out the numeric keypad from here cause keypress should 310 | // be able to detect the keys from the character 311 | if (key > 95 && key < 112) { 312 | continue; 313 | } 314 | 315 | if (_MAP.hasOwnProperty(key)) { 316 | _REVERSE_MAP[_MAP[key]] = key; 317 | } 318 | } 319 | } 320 | return _REVERSE_MAP; 321 | } 322 | 323 | /** 324 | * picks the best action based on the key combination 325 | * 326 | * @param {string} key - character for key 327 | * @param {Array} modifiers 328 | * @param {string=} action passed in 329 | */ 330 | function _pickBestAction(key, modifiers, action) { 331 | 332 | // if no action was picked in we should try to pick the one 333 | // that we think would work best for this key 334 | if (!action) { 335 | action = _getReverseMap()[key] ? 'keydown' : 'keypress'; 336 | } 337 | 338 | // modifier keys don't work as expected with keypress, 339 | // switch to keydown 340 | if (action == 'keypress' && modifiers.length) { 341 | action = 'keydown'; 342 | } 343 | 344 | return action; 345 | } 346 | 347 | /** 348 | * Converts from a string key combination to an array 349 | * 350 | * @param {string} combination like "command+shift+l" 351 | * @return {Array} 352 | */ 353 | function _keysFromString(combination) { 354 | if (combination === '+') { 355 | return ['+']; 356 | } 357 | 358 | combination = combination.replace(/\+{2}/g, '+plus'); 359 | return combination.split('+'); 360 | } 361 | 362 | /** 363 | * Gets info for a specific key combination 364 | * 365 | * @param {string} combination key combination ("command+s" or "a" or "*") 366 | * @param {string=} action 367 | * @returns {Object} 368 | */ 369 | function _getKeyInfo(combination, action) { 370 | var keys; 371 | var key; 372 | var i; 373 | var modifiers = []; 374 | 375 | // take the keys from this pattern and figure out what the actual 376 | // pattern is all about 377 | keys = _keysFromString(combination); 378 | 379 | for (i = 0; i < keys.length; ++i) { 380 | key = keys[i]; 381 | 382 | // normalize key names 383 | if (_SPECIAL_ALIASES[key]) { 384 | key = _SPECIAL_ALIASES[key]; 385 | } 386 | 387 | // if this is not a keypress event then we should 388 | // be smart about using shift keys 389 | // this will only work for US keyboards however 390 | if (action && action != 'keypress' && _SHIFT_MAP[key]) { 391 | key = _SHIFT_MAP[key]; 392 | modifiers.push('shift'); 393 | } 394 | 395 | // if this key is a modifier then add it to the list of modifiers 396 | if (_isModifier(key)) { 397 | modifiers.push(key); 398 | } 399 | } 400 | 401 | // depending on what the key combination is 402 | // we will try to pick the best event for it 403 | action = _pickBestAction(key, modifiers, action); 404 | 405 | return { 406 | key: key, 407 | modifiers: modifiers, 408 | action: action 409 | }; 410 | } 411 | 412 | function _belongsTo(element, ancestor) { 413 | if (element === null || element === document) { 414 | return false; 415 | } 416 | 417 | if (element === ancestor) { 418 | return true; 419 | } 420 | 421 | return _belongsTo(element.parentNode, ancestor); 422 | } 423 | 424 | function Mousetrap(targetElement) { 425 | var self = this; 426 | 427 | targetElement = targetElement || document; 428 | 429 | if (!(self instanceof Mousetrap)) { 430 | return new Mousetrap(targetElement); 431 | } 432 | 433 | /** 434 | * element to attach key events to 435 | * 436 | * @type {Element} 437 | */ 438 | self.target = targetElement; 439 | 440 | /** 441 | * a list of all the callbacks setup via Mousetrap.bind() 442 | * 443 | * @type {Object} 444 | */ 445 | self._callbacks = {}; 446 | 447 | /** 448 | * direct map of string combinations to callbacks used for trigger() 449 | * 450 | * @type {Object} 451 | */ 452 | self._directMap = {}; 453 | 454 | /** 455 | * keeps track of what level each sequence is at since multiple 456 | * sequences can start out with the same sequence 457 | * 458 | * @type {Object} 459 | */ 460 | var _sequenceLevels = {}; 461 | 462 | /** 463 | * variable to store the setTimeout call 464 | * 465 | * @type {null|number} 466 | */ 467 | var _resetTimer; 468 | 469 | /** 470 | * temporary state where we will ignore the next keyup 471 | * 472 | * @type {boolean|string} 473 | */ 474 | var _ignoreNextKeyup = false; 475 | 476 | /** 477 | * temporary state where we will ignore the next keypress 478 | * 479 | * @type {boolean} 480 | */ 481 | var _ignoreNextKeypress = false; 482 | 483 | /** 484 | * are we currently inside of a sequence? 485 | * type of action ("keyup" or "keydown" or "keypress") or false 486 | * 487 | * @type {boolean|string} 488 | */ 489 | var _nextExpectedAction = false; 490 | 491 | /** 492 | * resets all sequence counters except for the ones passed in 493 | * 494 | * @param {Object} doNotReset 495 | * @returns void 496 | */ 497 | function _resetSequences(doNotReset) { 498 | doNotReset = doNotReset || {}; 499 | 500 | var activeSequences = false, 501 | key; 502 | 503 | for (key in _sequenceLevels) { 504 | if (doNotReset[key]) { 505 | activeSequences = true; 506 | continue; 507 | } 508 | _sequenceLevels[key] = 0; 509 | } 510 | 511 | if (!activeSequences) { 512 | _nextExpectedAction = false; 513 | } 514 | } 515 | 516 | /** 517 | * finds all callbacks that match based on the keycode, modifiers, 518 | * and action 519 | * 520 | * @param {string} character 521 | * @param {Array} modifiers 522 | * @param {Event|Object} e 523 | * @param {string=} sequenceName - name of the sequence we are looking for 524 | * @param {string=} combination 525 | * @param {number=} level 526 | * @returns {Array} 527 | */ 528 | function _getMatches(character, modifiers, e, sequenceName, combination, level) { 529 | var i; 530 | var callback; 531 | var matches = []; 532 | var action = e.type; 533 | 534 | // if there are no events related to this keycode 535 | if (!self._callbacks[character]) { 536 | return []; 537 | } 538 | 539 | // if a modifier key is coming up on its own we should allow it 540 | if (action == 'keyup' && _isModifier(character)) { 541 | modifiers = [character]; 542 | } 543 | 544 | // loop through all callbacks for the key that was pressed 545 | // and see if any of them match 546 | for (i = 0; i < self._callbacks[character].length; ++i) { 547 | callback = self._callbacks[character][i]; 548 | 549 | // if a sequence name is not specified, but this is a sequence at 550 | // the wrong level then move onto the next match 551 | if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) { 552 | continue; 553 | } 554 | 555 | // if the action we are looking for doesn't match the action we got 556 | // then we should keep going 557 | if (action != callback.action) { 558 | continue; 559 | } 560 | 561 | // if this is a keypress event and the meta key and control key 562 | // are not pressed that means that we need to only look at the 563 | // character, otherwise check the modifiers as well 564 | // 565 | // chrome will not fire a keypress if meta or control is down 566 | // safari will fire a keypress if meta or meta+shift is down 567 | // firefox will fire a keypress if meta or control is down 568 | if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { 569 | 570 | // when you bind a combination or sequence a second time it 571 | // should overwrite the first one. if a sequenceName or 572 | // combination is specified in this call it does just that 573 | // 574 | // @todo make deleting its own method? 575 | var deleteCombo = !sequenceName && callback.combo == combination; 576 | var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level; 577 | if (deleteCombo || deleteSequence) { 578 | self._callbacks[character].splice(i, 1); 579 | } 580 | 581 | matches.push(callback); 582 | } 583 | } 584 | 585 | return matches; 586 | } 587 | 588 | /** 589 | * actually calls the callback function 590 | * 591 | * if your callback function returns false this will use the jquery 592 | * convention - prevent default and stop propogation on the event 593 | * 594 | * @param {Function} callback 595 | * @param {Event} e 596 | * @returns void 597 | */ 598 | function _fireCallback(callback, e, combo, sequence) { 599 | 600 | // if this event should not happen stop here 601 | if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) { 602 | return; 603 | } 604 | 605 | if (callback(e, combo) === false) { 606 | _preventDefault(e); 607 | _stopPropagation(e); 608 | } 609 | } 610 | 611 | /** 612 | * handles a character key event 613 | * 614 | * @param {string} character 615 | * @param {Array} modifiers 616 | * @param {Event} e 617 | * @returns void 618 | */ 619 | self._handleKey = function(character, modifiers, e) { 620 | var callbacks = _getMatches(character, modifiers, e); 621 | var i; 622 | var doNotReset = {}; 623 | var maxLevel = 0; 624 | var processedSequenceCallback = false; 625 | 626 | // Calculate the maxLevel for sequences so we can only execute the longest callback sequence 627 | for (i = 0; i < callbacks.length; ++i) { 628 | if (callbacks[i].seq) { 629 | maxLevel = Math.max(maxLevel, callbacks[i].level); 630 | } 631 | } 632 | 633 | // loop through matching callbacks for this key event 634 | for (i = 0; i < callbacks.length; ++i) { 635 | 636 | // fire for all sequence callbacks 637 | // this is because if for example you have multiple sequences 638 | // bound such as "g i" and "g t" they both need to fire the 639 | // callback for matching g cause otherwise you can only ever 640 | // match the first one 641 | if (callbacks[i].seq) { 642 | 643 | // only fire callbacks for the maxLevel to prevent 644 | // subsequences from also firing 645 | // 646 | // for example 'a option b' should not cause 'option b' to fire 647 | // even though 'option b' is part of the other sequence 648 | // 649 | // any sequences that do not match here will be discarded 650 | // below by the _resetSequences call 651 | if (callbacks[i].level != maxLevel) { 652 | continue; 653 | } 654 | 655 | processedSequenceCallback = true; 656 | 657 | // keep a list of which sequences were matches for later 658 | doNotReset[callbacks[i].seq] = 1; 659 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq); 660 | continue; 661 | } 662 | 663 | // if there were no sequence matches but we are still here 664 | // that means this is a regular match so we should fire that 665 | if (!processedSequenceCallback) { 666 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); 667 | } 668 | } 669 | 670 | // if the key you pressed matches the type of sequence without 671 | // being a modifier (ie "keyup" or "keypress") then we should 672 | // reset all sequences that were not matched by this event 673 | // 674 | // this is so, for example, if you have the sequence "h a t" and you 675 | // type "h e a r t" it does not match. in this case the "e" will 676 | // cause the sequence to reset 677 | // 678 | // modifier keys are ignored because you can have a sequence 679 | // that contains modifiers such as "enter ctrl+space" and in most 680 | // cases the modifier key will be pressed before the next key 681 | // 682 | // also if you have a sequence such as "ctrl+b a" then pressing the 683 | // "b" key will trigger a "keypress" and a "keydown" 684 | // 685 | // the "keydown" is expected when there is a modifier, but the 686 | // "keypress" ends up matching the _nextExpectedAction since it occurs 687 | // after and that causes the sequence to reset 688 | // 689 | // we ignore keypresses in a sequence that directly follow a keydown 690 | // for the same character 691 | var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress; 692 | if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) { 693 | _resetSequences(doNotReset); 694 | } 695 | 696 | _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown'; 697 | }; 698 | 699 | /** 700 | * handles a keydown event 701 | * 702 | * @param {Event} e 703 | * @returns void 704 | */ 705 | function _handleKeyEvent(e) { 706 | 707 | // normalize e.which for key events 708 | // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion 709 | if (typeof e.which !== 'number') { 710 | e.which = e.keyCode; 711 | } 712 | 713 | var character = _characterFromEvent(e); 714 | 715 | // no character found then stop 716 | if (!character) { 717 | return; 718 | } 719 | 720 | // need to use === for the character check because the character can be 0 721 | if (e.type == 'keyup' && _ignoreNextKeyup === character) { 722 | _ignoreNextKeyup = false; 723 | return; 724 | } 725 | 726 | self.handleKey(character, _eventModifiers(e), e); 727 | } 728 | 729 | /** 730 | * called to set a 1 second timeout on the specified sequence 731 | * 732 | * this is so after each key press in the sequence you have 1 second 733 | * to press the next key before you have to start over 734 | * 735 | * @returns void 736 | */ 737 | function _resetSequenceTimer() { 738 | clearTimeout(_resetTimer); 739 | _resetTimer = setTimeout(_resetSequences, 1000); 740 | } 741 | 742 | /** 743 | * binds a key sequence to an event 744 | * 745 | * @param {string} combo - combo specified in bind call 746 | * @param {Array} keys 747 | * @param {Function} callback 748 | * @param {string=} action 749 | * @returns void 750 | */ 751 | function _bindSequence(combo, keys, callback, action) { 752 | 753 | // start off by adding a sequence level record for this combination 754 | // and setting the level to 0 755 | _sequenceLevels[combo] = 0; 756 | 757 | /** 758 | * callback to increase the sequence level for this sequence and reset 759 | * all other sequences that were active 760 | * 761 | * @param {string} nextAction 762 | * @returns {Function} 763 | */ 764 | function _increaseSequence(nextAction) { 765 | return function() { 766 | _nextExpectedAction = nextAction; 767 | ++_sequenceLevels[combo]; 768 | _resetSequenceTimer(); 769 | }; 770 | } 771 | 772 | /** 773 | * wraps the specified callback inside of another function in order 774 | * to reset all sequence counters as soon as this sequence is done 775 | * 776 | * @param {Event} e 777 | * @returns void 778 | */ 779 | function _callbackAndReset(e) { 780 | _fireCallback(callback, e, combo); 781 | 782 | // we should ignore the next key up if the action is key down 783 | // or keypress. this is so if you finish a sequence and 784 | // release the key the final key will not trigger a keyup 785 | if (action !== 'keyup') { 786 | _ignoreNextKeyup = _characterFromEvent(e); 787 | } 788 | 789 | // weird race condition if a sequence ends with the key 790 | // another sequence begins with 791 | setTimeout(_resetSequences, 10); 792 | } 793 | 794 | // loop through keys one at a time and bind the appropriate callback 795 | // function. for any key leading up to the final one it should 796 | // increase the sequence. after the final, it should reset all sequences 797 | // 798 | // if an action is specified in the original bind call then that will 799 | // be used throughout. otherwise we will pass the action that the 800 | // next key in the sequence should match. this allows a sequence 801 | // to mix and match keypress and keydown events depending on which 802 | // ones are better suited to the key provided 803 | for (var i = 0; i < keys.length; ++i) { 804 | var isFinal = i + 1 === keys.length; 805 | var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action); 806 | _bindSingle(keys[i], wrappedCallback, action, combo, i); 807 | } 808 | } 809 | 810 | /** 811 | * binds a single keyboard combination 812 | * 813 | * @param {string} combination 814 | * @param {Function} callback 815 | * @param {string=} action 816 | * @param {string=} sequenceName - name of sequence if part of sequence 817 | * @param {number=} level - what part of the sequence the command is 818 | * @returns void 819 | */ 820 | function _bindSingle(combination, callback, action, sequenceName, level) { 821 | 822 | // store a direct mapped reference for use with Mousetrap.trigger 823 | self._directMap[combination + ':' + action] = callback; 824 | 825 | // make sure multiple spaces in a row become a single space 826 | combination = combination.replace(/\s+/g, ' '); 827 | 828 | var sequence = combination.split(' '); 829 | var info; 830 | 831 | // if this pattern is a sequence of keys then run through this method 832 | // to reprocess each pattern one key at a time 833 | if (sequence.length > 1) { 834 | _bindSequence(combination, sequence, callback, action); 835 | return; 836 | } 837 | 838 | info = _getKeyInfo(combination, action); 839 | 840 | // make sure to initialize array if this is the first time 841 | // a callback is added for this key 842 | self._callbacks[info.key] = self._callbacks[info.key] || []; 843 | 844 | // remove an existing match if there is one 845 | _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level); 846 | 847 | // add this call back to the array 848 | // if it is a sequence put it at the beginning 849 | // if not put it at the end 850 | // 851 | // this is important because the way these are processed expects 852 | // the sequence ones to come first 853 | self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({ 854 | callback: callback, 855 | modifiers: info.modifiers, 856 | action: info.action, 857 | seq: sequenceName, 858 | level: level, 859 | combo: combination 860 | }); 861 | } 862 | 863 | /** 864 | * binds multiple combinations to the same callback 865 | * 866 | * @param {Array} combinations 867 | * @param {Function} callback 868 | * @param {string|undefined} action 869 | * @returns void 870 | */ 871 | self._bindMultiple = function(combinations, callback, action) { 872 | for (var i = 0; i < combinations.length; ++i) { 873 | _bindSingle(combinations[i], callback, action); 874 | } 875 | }; 876 | 877 | // start! 878 | _addEvent(targetElement, 'keypress', _handleKeyEvent); 879 | _addEvent(targetElement, 'keydown', _handleKeyEvent); 880 | _addEvent(targetElement, 'keyup', _handleKeyEvent); 881 | } 882 | 883 | /** 884 | * binds an event to mousetrap 885 | * 886 | * can be a single key, a combination of keys separated with +, 887 | * an array of keys, or a sequence of keys separated by spaces 888 | * 889 | * be sure to list the modifier keys first to make sure that the 890 | * correct key ends up getting bound (the last key in the pattern) 891 | * 892 | * @param {string|Array} keys 893 | * @param {Function} callback 894 | * @param {string=} action - 'keypress', 'keydown', or 'keyup' 895 | * @returns void 896 | */ 897 | Mousetrap.prototype.bind = function(keys, callback, action) { 898 | var self = this; 899 | keys = keys instanceof Array ? keys : [keys]; 900 | self._bindMultiple.call(self, keys, callback, action); 901 | return self; 902 | }; 903 | 904 | /** 905 | * unbinds an event to mousetrap 906 | * 907 | * the unbinding sets the callback function of the specified key combo 908 | * to an empty function and deletes the corresponding key in the 909 | * _directMap dict. 910 | * 911 | * TODO: actually remove this from the _callbacks dictionary instead 912 | * of binding an empty function 913 | * 914 | * the keycombo+action has to be exactly the same as 915 | * it was defined in the bind method 916 | * 917 | * @param {string|Array} keys 918 | * @param {string} action 919 | * @returns void 920 | */ 921 | Mousetrap.prototype.unbind = function(keys, action) { 922 | var self = this; 923 | return self.bind.call(self, keys, function() {}, action); 924 | }; 925 | 926 | /** 927 | * triggers an event that has already been bound 928 | * 929 | * @param {string} keys 930 | * @param {string=} action 931 | * @returns void 932 | */ 933 | Mousetrap.prototype.trigger = function(keys, action) { 934 | var self = this; 935 | if (self._directMap[keys + ':' + action]) { 936 | self._directMap[keys + ':' + action]({}, keys); 937 | } 938 | return self; 939 | }; 940 | 941 | /** 942 | * resets the library back to its initial state. this is useful 943 | * if you want to clear out the current keyboard shortcuts and bind 944 | * new ones - for example if you switch to another page 945 | * 946 | * @returns void 947 | */ 948 | Mousetrap.prototype.reset = function() { 949 | var self = this; 950 | self._callbacks = {}; 951 | self._directMap = {}; 952 | return self; 953 | }; 954 | 955 | /** 956 | * should we stop this event before firing off callbacks 957 | * 958 | * @param {Event} e 959 | * @param {Element} element 960 | * @return {boolean} 961 | */ 962 | Mousetrap.prototype.stopCallback = function(e, element) { 963 | var self = this; 964 | 965 | // if the element has the class "mousetrap" then no need to stop 966 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 967 | return false; 968 | } 969 | 970 | if (_belongsTo(element, self.target)) { 971 | return false; 972 | } 973 | 974 | // stop for input, select, and textarea 975 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable; 976 | }; 977 | 978 | /** 979 | * exposes _handleKey publicly so it can be overwritten by extensions 980 | */ 981 | Mousetrap.prototype.handleKey = function() { 982 | var self = this; 983 | return self._handleKey.apply(self, arguments); 984 | }; 985 | 986 | /** 987 | * Init the global mousetrap functions 988 | * 989 | * This method is needed to allow the global mousetrap functions to work 990 | * now that mousetrap is a constructor function. 991 | */ 992 | Mousetrap.init = function() { 993 | var documentMousetrap = Mousetrap(document); 994 | for (var method in documentMousetrap) { 995 | if (method.charAt(0) !== '_') { 996 | Mousetrap[method] = (function(method) { 997 | return function() { 998 | return documentMousetrap[method].apply(documentMousetrap, arguments); 999 | }; 1000 | } (method)); 1001 | } 1002 | } 1003 | }; 1004 | 1005 | Mousetrap.init(); 1006 | 1007 | // expose mousetrap to the global object 1008 | window.Mousetrap = Mousetrap; 1009 | 1010 | // expose as a common js module 1011 | if (typeof module !== 'undefined' && module.exports) { 1012 | module.exports = Mousetrap; 1013 | } 1014 | 1015 | // expose mousetrap as an AMD module 1016 | if (typeof define === 'function' && define.amd) { 1017 | define(function() { 1018 | return Mousetrap; 1019 | }); 1020 | } 1021 | }) (window, document); 1022 | --------------------------------------------------------------------------------