├── vimari.safariextension ├── Icon.png ├── vimari.tiff ├── injectedcss.css ├── Info.plist ├── keyboardUtils.js ├── global.html ├── Settings.plist ├── vimium-scripts.js ├── injected.js ├── linkHints.js └── mousetrap.js ├── manifest.plist ├── MIT-LICENSE.txt ├── CHANGELOG.markdown └── README.markdown /vimari.safariextension/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/vimari/master/vimari.safariextension/Icon.png -------------------------------------------------------------------------------- /vimari.safariextension/vimari.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonlong/vimari/master/vimari.safariextension/vimari.tiff -------------------------------------------------------------------------------- /manifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Extension Updates 6 | 7 | 8 | CFBundleIdentifier 9 | com.guyht.vimari 10 | Developer Identifier 11 | (357AUX7M8L) guyht@me.com 12 | CFBundleVersion 13 | 5 14 | CFBundleShortVersionString 15 | 1.0 16 | URL 17 | https://github.com/downloads/guyht/vimari/vimari.safariextz 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Phil Crosby, Ilya Sukhar. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /CHANGELOG.markdown: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ------------- 3 | 4 | **Note, changelog no longer updated :)** 5 | 6 | ### 1.1 (31/07/2011) 7 | * Updated to work with the new version of Safari on lion 8 | * Removed history forward / back 9 | * Changed directory structure to make it more developer friendly 10 | 11 | ### 1.0 (21/11/2010) 12 | * Changed the way vimari modifier keys work. ESC key depricated. Now use CTRL-modifierkey. 13 | 14 | ### 0.4 (17/11/2010) 15 | * First BETA release ! 16 | * Press ESC to enter a permanent state of 'non' insert mode. Clicking on any input then exits insert mode. This fixes several issues with google and facebook. 17 | 18 | ### 0.3 (16/11/2010) 19 | * Moved the extension startup code to be loaded before the browser page. Events can now be intercepted before they are passed to the browser page. 20 | * Created a manifest file, this allows automatic updates to take place. 21 | * Added insert mode. If the selected node can accept an input, the extension is disabled. This functionality still needs some work. 22 | * Ported the HUD from vimium. The hud displays information along the bottom of the screen. The hud has been ported but is not used for very much at the moment. 23 | 24 | ### 0.2 (14/11/2010) 25 | * Pressing ESC now removes focus from any input fields and activates modifiers 26 | 27 | ### 0.1 (14/11/1020) 28 | * First alpa release of vimari. Added basic features but still very buggy. 29 | -------------------------------------------------------------------------------- /vimari.safariextension/injectedcss.css: -------------------------------------------------------------------------------- 1 | .vimiumReset { 2 | background: none; 3 | border: none; 4 | bottom: auto; 5 | box-shadow: none; 6 | color: black; 7 | cursor: auto; 8 | display: inline; 9 | float: none; 10 | font-family : "Helvetica Neue", "Helvetica", "Arial", sans-serif; 11 | font-size: inherit; 12 | font-style: normal; 13 | font-variant: normal; 14 | font-weight: normal; 15 | height: auto; 16 | left: auto; 17 | letter-spacing: 0; 18 | line-height: 100%; 19 | margin: 0; 20 | max-height: none; 21 | max-width: none; 22 | min-height: 0; 23 | min-width: 0; 24 | opacity: 1; 25 | padding: 0; 26 | position: static; 27 | right: auto; 28 | text-align: left; 29 | text-decoration: none; 30 | text-indent: 0; 31 | text-shadow: none; 32 | text-transform: none; 33 | top: auto; 34 | vertical-align: baseline; 35 | white-space: normal; 36 | width: auto; 37 | z-index: 2147483647; /* Maximum value in Safari */ 38 | } 39 | 40 | div.internalVimiumHintMarker { 41 | position: absolute !important; 42 | display: block !important; 43 | top: -1px; 44 | left: -1px; 45 | white-space: nowrap !important; 46 | overflow: hidden !important; 47 | font-size: 11px !important; 48 | padding: 1px 3px 0px 3px !important; 49 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542)) !important; 50 | border: 1px solid #E3BE23 !important; 51 | border-radius: 3px !important; 52 | box-shadow: 0px 3px 7px 0px rgba(0, 0, 0, 0.3) !important; 53 | } 54 | 55 | div.internalVimiumHintMarker span { 56 | color: #000000; 57 | font-family: Helvetica, Arial, sans-serif; 58 | font-weight: bold; 59 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); 60 | } 61 | 62 | div.internalVimiumHintMarker > .matchingCharacter { 63 | color: #D4AC3A; 64 | } -------------------------------------------------------------------------------- /vimari.safariextension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | Various 7 | Builder Version 8 | 9537.76.4 9 | CFBundleDisplayName 10 | vimari 11 | CFBundleExecutable 12 | 13 | CFBundleIdentifier 14 | com.guyht.vimari 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleShortVersionString 18 | 1.11 19 | CFBundleVersion 20 | 21 21 | Chrome 22 | 23 | Global Page 24 | global.html 25 | 26 | Content 27 | 28 | Scripts 29 | 30 | Start 31 | 32 | keyboardUtils.js 33 | vimium-scripts.js 34 | linkHints.js 35 | mousetrap.js 36 | injected.js 37 | 38 | 39 | Stylesheets 40 | 41 | injectedcss.css 42 | 43 | 44 | Description 45 | Vim style shortcuts for safari 46 | ExtensionInfoDictionaryVersion 47 | 1.0 48 | Permissions 49 | 50 | Website Access 51 | 52 | Include Secure Pages 53 | 54 | Level 55 | All 56 | 57 | 58 | Update Manifest URL 59 | http://guyht.github.io/vimari/manifest.plist 60 | Website 61 | http://guyht.github.com/vimari/ 62 | 63 | 64 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | **NOTE: If you have a pre 1.2 version of Vimari, you need to manually update to the latest version as there is a bug in the auto-update code. Versions post 1.2 will automatically update to the lastest version.** 2 | 3 | Vimari - Keyboard Shortcuts extension for Safari 4 | ================================================ 5 | 6 | ### Releases 7 | [Release note for version 1.10](https://github.com/guyht/vimari/releases/tag/v1.10) 8 | 9 | [Release note for version 1.9](https://github.com/guyht/vimari/releases/tag/v1.9) 10 | 11 | 12 | Vimari is a Safari extension that provides keyboard based navigation. The code is heavily based on 'vimium', a chrome extension that provides much more extensive features. 13 | 14 | Vimari attempts to provide a lightweight port of vimium to Safari, taking the best components of vimium and adapting them to Safari. 15 | 16 | __Installation Instructions:__ 17 | 18 | Click the download link below: 19 | https://github.com/guyht/vimari/releases/download/v1.10/vimari-1.10.safariextz 20 | 21 | 22 | Screenshots 23 | ----------- 24 | 25 | ![Screenshot](https://github.com/guyht/vimari/raw/gh-pages/shot.png) 26 | 27 | 28 | Sounds awesome. Howto? 29 | ----------------------- 30 | 31 | Simply browse to a page and hold CTRL, then press 'f' to enter link hint mode to easily navigate, q or w to move between tabs ;). All these shortcuts are also configurable through Safari preferences. Enjoy. 32 | 33 | Link Hints 34 | ---------- 35 | 36 | The principle feature of Vimari and the principle component taken from vimium is the link hints feature. Press ESC to enter 'vimari shortcut mode' and then pressing 'f' enters link hint mode which displays a HUD over the page wih a code for each link. Simply type of code for the link to follow that link. Link codes are generated according to the home key buttons and so are always easiliy accessible. 37 | 38 | Keyboard Bindings 39 | ----------------- 40 | 41 | All bindings are configurable in the Preferences->Extensions options in Safari 42 | 43 | Currently: 44 | CTRL-f Link Hints (open in same tab) 45 | CTRL-SHIFT-f Link Hints (open in new tab) 46 | CTRL-q Previous Tab 47 | CTRL-w Next Tab 48 | 49 | 50 | 51 | License 52 | ------- 53 | Copyright (c) 2011 Guy Halford-Thompson. See MIT-LICENSE.txt for details. 54 | -------------------------------------------------------------------------------- /vimari.safariextension/keyboardUtils.js: -------------------------------------------------------------------------------- 1 | var keyCodes = { ESC: 27, backspace: 8, deleteKey: 46, enter: 13, space: 32, shiftKey: 16, f1: 112, f12: 123}; 2 | var keyNames = { 37: "left", 38: "up", 39: "right", 40: "down" } 3 | 4 | // This is a mapping of the incorrect keyIdentifiers generated by Webkit on Windows during keydown events to 5 | // the correct identifiers, which are correctly generated on Mac. We require this mapping to properly handle 6 | // these keys on Windows. See https://bugs.webkit.org/show_bug.cgi?id=19906 for more details. 7 | var keyIdentifierCorrectionMap = { 8 | "U+00C0": ["U+0060", "U+007E"], // `~ 9 | "U+00BD": ["U+002D", "U+005F"], // -_ 10 | "U+00BB": ["U+003D", "U+002B"], // =+ 11 | "U+00DB": ["U+005B", "U+007B"], // [{ 12 | "U+00DD": ["U+005D", "U+007D"], // ]} 13 | "U+00DC": ["U+005C", "U+007C"], // \| 14 | "U+00BA": ["U+003B", "U+003A"], // ;: 15 | "U+00DE": ["U+0027", "U+0022"], // '" 16 | "U+00BC": ["U+002C", "U+003C"], // ,< 17 | "U+00BE": ["U+002E", "U+003E"], // .> 18 | "U+00BF": ["U+002F", "U+003F"] // /? 19 | }; 20 | 21 | var platform; 22 | if (navigator.userAgent.indexOf("Mac") != -1) 23 | platform = "Mac"; 24 | else if (navigator.userAgent.indexOf("Linux") != -1) 25 | platform = "Linux"; 26 | else 27 | platform = "Windows"; 28 | 29 | function getKeyChar(event) { 30 | // Not a letter 31 | if (event.keyIdentifier.slice(0, 2) != "U+") { 32 | // Named key 33 | if (keyNames[event.keyCode]) { 34 | return keyNames[event.keyCode]; 35 | } 36 | // F-key 37 | if (event.keyCode >= keyCodes.f1 && event.keyCode <= keyCodes.f12) { 38 | return "f" + (1 + event.keyCode - keyCodes.f1); 39 | } 40 | return ""; 41 | } 42 | var keyIdentifier = event.keyIdentifier; 43 | // On Windows, the keyIdentifiers for non-letter keys are incorrect. See 44 | // https://bugs.webkit.org/show_bug.cgi?id=19906 for more details. 45 | if ((platform == "Windows" || platform == "Linux") && keyIdentifierCorrectionMap[keyIdentifier]) { 46 | correctedIdentifiers = keyIdentifierCorrectionMap[keyIdentifier]; 47 | keyIdentifier = event.shiftKey ? correctedIdentifiers[0] : correctedIdentifiers[1]; 48 | } 49 | var unicodeKeyInHex = "0x" + keyIdentifier.substring(2); 50 | return String.fromCharCode(parseInt(unicodeKeyInHex)).toLowerCase(); 51 | } 52 | 53 | function isPrimaryModifierKey(event) { 54 | if (platform == "Mac") 55 | return event.metaKey; 56 | else 57 | return event.ctrlKey; 58 | } 59 | 60 | function isEscape(event) { 61 | return event.keyCode == keyCodes.ESC || 62 | (event.ctrlKey && getKeyChar(event) == '['); // c-[ is mapped to ESC in Vim by default. 63 | } 64 | -------------------------------------------------------------------------------- /vimari.safariextension/global.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vimari global extension page 5 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /vimari.safariextension/Settings.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Title 7 | General 8 | Type 9 | Group 10 | 11 | 12 | DefaultValue 13 | ctrl 14 | Key 15 | modifier 16 | Title 17 | Command prefix (blank for none) 18 | Type 19 | TextField 20 | 21 | 22 | DefaultValue 23 | 24 | Key 25 | excludedUrls 26 | Title 27 | Excluded URLs (comma separated) 28 | Type 29 | TextField 30 | 31 | 32 | Title 33 | Link hinting 34 | Type 35 | Group 36 | 37 | 38 | DefaultValue 39 | f 40 | Key 41 | hintToggle 42 | Title 43 | Link Hint Toggle 44 | Type 45 | TextField 46 | 47 | 48 | DefaultValue 49 | shift+f 50 | Key 51 | newTabHintToggle 52 | Title 53 | Link Hint Toggle (open in new tab) 54 | Type 55 | TextField 56 | 57 | 58 | DefaultValue 59 | asdfjklqwerzxc 60 | Key 61 | linkHintCharacters 62 | Title 63 | Link hint characters 64 | Type 65 | TextField 66 | 67 | 68 | DefaultValue 69 | 70 | Key 71 | detectByCursorStyle 72 | Title 73 | Extra detection by cursor style 74 | Type 75 | CheckBox 76 | 77 | 78 | Title 79 | In-page navigation 80 | Type 81 | Group 82 | 83 | 84 | DefaultValue 85 | k 86 | Key 87 | scrollUp 88 | Title 89 | Scroll Up 90 | Type 91 | TextField 92 | 93 | 94 | DefaultValue 95 | j 96 | Key 97 | scrollDown 98 | Title 99 | Scroll Down 100 | Type 101 | TextField 102 | 103 | 104 | DefaultValue 105 | h 106 | Key 107 | scrollLeft 108 | Title 109 | Scroll Left 110 | Type 111 | TextField 112 | 113 | 114 | DefaultValue 115 | l 116 | Key 117 | scrollRight 118 | Title 119 | Scroll Right 120 | Type 121 | TextField 122 | 123 | 124 | DefaultValue 125 | 60 126 | Key 127 | scrollSize 128 | MaximumValue 129 | 200 130 | MinimumValue 131 | 20 132 | StepValue 133 | 20 134 | Title 135 | Scroll Size 136 | Type 137 | Slider 138 | 139 | 140 | DefaultValue 141 | u 142 | Key 143 | scrollUpHalfPage 144 | Title 145 | Scroll Up Half Page 146 | Type 147 | TextField 148 | 149 | 150 | DefaultValue 151 | d 152 | Key 153 | scrollDownHalfPage 154 | Title 155 | Scroll Down Half Page 156 | Type 157 | TextField 158 | 159 | 160 | DefaultValue 161 | g g 162 | Key 163 | goToPageTop 164 | Title 165 | Go to the top of the page 166 | Type 167 | TextField 168 | 169 | 170 | DefaultValue 171 | shift+g 172 | Key 173 | goToPageBottom 174 | Title 175 | Go to the bottom of the page 176 | Type 177 | TextField 178 | 179 | 180 | Title 181 | Page/Tab navigation 182 | Type 183 | Group 184 | 185 | 186 | DefaultValue 187 | shift+h 188 | Key 189 | goBack 190 | Title 191 | History Back 192 | Type 193 | TextField 194 | 195 | 196 | DefaultValue 197 | shift+l 198 | Key 199 | goForward 200 | Title 201 | History Forward 202 | Type 203 | TextField 204 | 205 | 206 | DefaultValue 207 | r 208 | Key 209 | reload 210 | Title 211 | Reload 212 | Type 213 | TextField 214 | 215 | 216 | DefaultValue 217 | w 218 | Key 219 | tabForward 220 | Title 221 | Next Tab 222 | Type 223 | TextField 224 | 225 | 226 | DefaultValue 227 | q 228 | Key 229 | tabBack 230 | Title 231 | Previous Tab 232 | Type 233 | TextField 234 | 235 | 236 | DefaultValue 237 | x 238 | Key 239 | closeTab 240 | Title 241 | Close Current Tab (open left tab) 242 | Type 243 | TextField 244 | 245 | 246 | DefaultValue 247 | shift+x 248 | Key 249 | closeTabReverse 250 | Title 251 | Close Current Tab (open right tab) 252 | Type 253 | TextField 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /vimari.safariextension/vimium-scripts.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Code in this file is taken directly from vimium 3 | */ 4 | 5 | 6 | /* 7 | * A heads-up-display (HUD) for showing Vimium page operations. 8 | * Note: you cannot interact with the HUD until document.body is available. 9 | */ 10 | HUD = { 11 | _tweenId: -1, 12 | _displayElement: null, 13 | _upgradeNotificationElement: null, 14 | 15 | // This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html" 16 | // test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that 17 | // it doesn't sit on top of horizontal scrollbars like Chrome's HUD does. 18 | _hudCss: 19 | ".vimiumHUD, .vimiumHUD * {" + 20 | "line-height: 100%;" + 21 | "font-size: 11px;" + 22 | "font-weight: normal;" + 23 | "}" + 24 | ".vimiumHUD {" + 25 | "position: fixed;" + 26 | "bottom: 0px;" + 27 | "color: black;" + 28 | "height: 13px;" + 29 | "max-width: 400px;" + 30 | "min-width: 150px;" + 31 | "text-align: left;" + 32 | "background-color: #ebebeb;" + 33 | "padding: 3px 3px 2px 3px;" + 34 | "border: 1px solid #b3b3b3;" + 35 | "border-radius: 4px 4px 0 0;" + 36 | "font-family: Lucida Grande, Arial, Sans;" + 37 | // One less than vimium's hint markers, so link hints can be shown e.g. for the panel's close button. 38 | "z-index: 99999998;" + 39 | "text-shadow: 0px 1px 2px #FFF;" + 40 | "line-height: 1.0;" + 41 | "opacity: 0;" + 42 | "}" + 43 | ".vimiumHUD a, .vimiumHUD a:hover {" + 44 | "background: transparent;" + 45 | "color: blue;" + 46 | "text-decoration: underline;" + 47 | "}" + 48 | ".vimiumHUD a.close-button {" + 49 | "float:right;" + 50 | "font-family:courier new;" + 51 | "font-weight:bold;" + 52 | "color:#9C9A9A;" + 53 | "text-decoration:none;" + 54 | "padding-left:10px;" + 55 | "margin-top:-1px;" + 56 | "font-size:14px;" + 57 | "}" + 58 | ".vimiumHUD a.close-button:hover {" + 59 | "color:#333333;" + 60 | "cursor:default;" + 61 | "-webkit-user-select:none;" + 62 | "}", 63 | 64 | _cssHasBeenAdded: false, 65 | 66 | showForDuration: function(text, duration) { 67 | HUD.show(text); 68 | HUD._showForDurationTimerId = setTimeout(function() { HUD.hide(); }, duration); 69 | }, 70 | 71 | show: function(text) { 72 | clearTimeout(HUD._showForDurationTimerId); 73 | HUD.displayElement().innerHTML = text; 74 | clearInterval(HUD._tweenId); 75 | HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150); 76 | HUD.displayElement().style.display = ""; 77 | }, 78 | 79 | showUpgradeNotification: function(version) { 80 | HUD.upgradeNotificationElement().innerHTML = "Vimium has been updated to " + 81 | "" + 82 | version + ".x"; 83 | var links = HUD.upgradeNotificationElement().getElementsByTagName("a"); 84 | links[0].addEventListener("click", HUD.onUpdateLinkClicked, false); 85 | links[1].addEventListener("click", function(event) { 86 | event.preventDefault(); 87 | HUD.onUpdateLinkClicked(); 88 | }); 89 | Tween.fade(HUD.upgradeNotificationElement(), 1.0, 150); 90 | }, 91 | 92 | onUpdateLinkClicked: function(event) { 93 | HUD.hideUpgradeNotification(); 94 | chrome.extension.sendRequest({ handler: "upgradeNotificationClosed" }); 95 | }, 96 | 97 | hideUpgradeNotification: function(clickEvent) { 98 | Tween.fade(HUD.upgradeNotificationElement(), 0, 150, 99 | function() { HUD.upgradeNotificationElement().style.display = "none"; }); 100 | }, 101 | 102 | updatePageZoomLevel: function(pageZoomLevel) { 103 | // Since the chrome HUD does not scale with the page's zoom level, neither will this HUD. 104 | var inverseZoomLevel = (100.0 / pageZoomLevel) * 100; 105 | if (HUD._displayElement) 106 | HUD.displayElement().style.zoom = inverseZoomLevel + "%"; 107 | if (HUD._upgradeNotificationElement) 108 | HUD.upgradeNotificationElement().style.zoom = inverseZoomLevel + "%"; 109 | }, 110 | 111 | /* 112 | * Retrieves the HUD HTML element. 113 | */ 114 | displayElement: function() { 115 | if (!HUD._displayElement) { 116 | HUD._displayElement = HUD.createHudElement(); 117 | // Keep this far enough to the right so that it doesn't collide with the "popups blocked" chrome HUD. 118 | HUD._displayElement.style.right = "150px"; 119 | HUD.updatePageZoomLevel(currentZoomLevel); 120 | } 121 | return HUD._displayElement; 122 | }, 123 | 124 | upgradeNotificationElement: function() { 125 | if (!HUD._upgradeNotificationElement) { 126 | HUD._upgradeNotificationElement = HUD.createHudElement(); 127 | // Position this just to the left of our normal HUD. 128 | HUD._upgradeNotificationElement.style.right = "315px"; 129 | HUD.updatePageZoomLevel(currentZoomLevel); 130 | } 131 | return HUD._upgradeNotificationElement; 132 | }, 133 | 134 | createHudElement: function() { 135 | if (!HUD._cssHasBeenAdded) { 136 | addCssToPage(HUD._hudCss); 137 | HUD._cssHasBeenAdded = true; 138 | } 139 | var element = document.createElement("div"); 140 | element.className = "vimiumHUD"; 141 | document.body.appendChild(element); 142 | return element; 143 | }, 144 | 145 | hide: function() { 146 | clearInterval(HUD._tweenId); 147 | HUD._tweenId = Tween.fade(HUD.displayElement(), 0, 150, 148 | function() { HUD.displayElement().style.display = "none"; }); 149 | }, 150 | 151 | isReady: function() { return document.body != null; } 152 | }; 153 | 154 | 155 | 156 | 157 | Tween = { 158 | /* 159 | * Fades an element's alpha. Returns a timer ID which can be used to stop the tween via clearInterval. 160 | */ 161 | fade: function(element, toAlpha, duration, onComplete) { 162 | var state = {}; 163 | state.duration = duration; 164 | state.startTime = (new Date()).getTime(); 165 | state.from = parseInt(element.style.opacity) || 0; 166 | state.to = toAlpha; 167 | state.onUpdate = function(value) { 168 | element.style.opacity = value; 169 | if (value == state.to && onComplete) 170 | onComplete(); 171 | }; 172 | state.timerId = setInterval(function() { Tween.performTweenStep(state); }, 50); 173 | return state.timerId; 174 | }, 175 | 176 | performTweenStep: function(state) { 177 | var elapsed = (new Date()).getTime() - state.startTime; 178 | if (elapsed >= state.duration) { 179 | clearInterval(state.timerId); 180 | state.onUpdate(state.to) 181 | } else { 182 | var value = (elapsed / state.duration) * (state.to - state.from) + state.from; 183 | state.onUpdate(value); 184 | } 185 | } 186 | }; 187 | 188 | 189 | 190 | 191 | 192 | /* 193 | * Adds the given CSS to the page. 194 | */ 195 | function addCssToPage(css) { 196 | var head = document.getElementsByTagName("head")[0]; 197 | if (!head) { 198 | console.log("Warning: unable to add CSS to the page."); 199 | return; 200 | } 201 | var style = document.createElement("style"); 202 | style.type = "text/css"; 203 | style.appendChild(document.createTextNode(css)); 204 | head.appendChild(style); 205 | } 206 | -------------------------------------------------------------------------------- /vimari.safariextension/injected.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Vimari injected script. 3 | * 4 | * This script is called before the requested page is loaded. This allows us 5 | * to intercept events before they are passed to the requested pages code and 6 | * therefore we can stop certain pages (google) stealing the focus. 7 | */ 8 | 9 | 10 | /* 11 | * Global vars 12 | * 13 | * topWindow - true if top window, false if iframe 14 | * settings - stores user settings 15 | * currentZoomLevel - required for vimium scripts to run correctly 16 | * linkHintCss - required from vimium scripts 17 | * extensionActive - is the extension currently enabled (should only be true when tab is active) 18 | * shiftKeyToggle - is shift key currently toggled 19 | */ 20 | 21 | var topWindow = (window.top === window), 22 | settings = {}, 23 | currentZoomLevel = 100, 24 | linkHintCss = {}, 25 | extensionActive = true, 26 | insertMode = false, 27 | shiftKeyToggle = false; 28 | 29 | var actionMap = { 30 | 'hintToggle' : function() { 31 | HUD.show('Link hints mode'); 32 | activateLinkHintsMode(false, false); }, 33 | 34 | 'newTabHintToggle' : function() { 35 | HUD.show('Link hints mode'); 36 | activateLinkHintsMode(true, false); }, 37 | 38 | 'tabForward' : function() { 39 | safari.self.tab.dispatchMessage('changeTab', 1); }, 40 | 41 | 'tabBack' : function() { 42 | safari.self.tab.dispatchMessage('changeTab', 0); }, 43 | 44 | 'scrollDown' : 45 | function() { window.scrollBy(0, settings.scrollSize); }, 46 | 47 | 'scrollUp' : 48 | function() { window.scrollBy(0, -settings.scrollSize); }, 49 | 50 | 'scrollLeft' : 51 | function() { window.scrollBy(-settings.scrollSize, 0); }, 52 | 53 | 'scrollRight' : 54 | function() { window.scrollBy(settings.scrollSize, 0); }, 55 | 56 | 'goBack' : 57 | function() { window.history.back(); }, 58 | 59 | 'goForward' : 60 | function() { window.history.forward(); }, 61 | 62 | 'reload' : 63 | function() { window.location.reload(); }, 64 | 65 | 'closeTab' : 66 | function() { safari.self.tab.dispatchMessage('closeTab', 0); }, 67 | 68 | 'closeTabReverse' : 69 | function() { safari.self.tab.dispatchMessage('closeTab', 1); }, 70 | 71 | 'scrollDownHalfPage' : 72 | function() { window.scrollBy(0, window.innerHeight / 2); }, 73 | 74 | 'scrollUpHalfPage' : 75 | function() { window.scrollBy(0, window.innerHeight / -2); }, 76 | 77 | 'goToPageBottom' : 78 | function() { window.scrollBy(0, document.body.scrollHeight); }, 79 | 80 | 'goToPageTop' : 81 | function() { window.scrollBy(0, -document.body.scrollHeight); } 82 | }; 83 | 84 | // Meant to be overridden, but still has to be copy/pasted from the original... 85 | Mousetrap.stopCallback = function(e, element, combo) { 86 | // Escape key is special, no need to stop. Vimari-specific. 87 | if (combo === 'esc' || combo === 'ctrl+[') { return false; } 88 | 89 | // Preserve the behavior of allowing ex. ctrl-j in an input 90 | if (settings.modifier) { return false; } 91 | 92 | // if the element has the class "mousetrap" then no need to stop 93 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 94 | return false; 95 | } 96 | 97 | // stop for input, select, and textarea 98 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true'); 99 | } 100 | 101 | // Set up key codes to event handlers 102 | function bindKeyCodesToActions() { 103 | // Only add if topWindow... not iframe 104 | if (topWindow && isEnabledForUrl() ) { 105 | Mousetrap.reset(); 106 | Mousetrap.bind('esc', enterNormalMode); 107 | Mousetrap.bind('ctrl+[', enterNormalMode); 108 | Mousetrap.bind('i', enterInsertMode); 109 | for (var actionName in actionMap) { 110 | if (actionMap.hasOwnProperty(actionName)) { 111 | var keyCode = getKeyCode(actionName); 112 | Mousetrap.bind(keyCode, executeAction(actionName), 'keydown'); 113 | } 114 | } 115 | } 116 | } 117 | 118 | function enterNormalMode() { 119 | // Clear input focus 120 | document.activeElement.blur(); 121 | 122 | // Clear link hints (if any) 123 | deactivateLinkHintsMode(); 124 | 125 | // Re-enable if in insert mode 126 | insertMode = false; 127 | Mousetrap.bind('i', enterInsertMode); 128 | } 129 | 130 | // Calling it 'insert mode', but it's really just a user-triggered 131 | // off switch for the actions. 132 | function enterInsertMode() { 133 | insertMode = true; 134 | Mousetrap.unbind('i'); 135 | } 136 | 137 | function executeAction(actionName) { 138 | return function() { 139 | // don't do anything if we're not supposed to 140 | if (linkHintsModeActivated || !extensionActive || insertMode) 141 | return; 142 | 143 | //Call the action function 144 | actionMap[actionName](); 145 | 146 | // Tell mousetrap to stop propagation 147 | return false; 148 | } 149 | } 150 | 151 | function unbindKeyCodes() { 152 | Mousetrap.reset(); 153 | } 154 | 155 | // Adds an optional modifier to the configured key code for the action 156 | function getKeyCode(actionName) { 157 | var keyCode = ''; 158 | if(settings.modifier) { 159 | keyCode += settings.modifier + '+'; 160 | } 161 | return keyCode + settings[actionName]; 162 | } 163 | 164 | 165 | 166 | /* 167 | * Adds the given CSS to the page. 168 | * This function is required by vimium but depracated for vimari as the 169 | * css is pre loaded into the page. 170 | */ 171 | function addCssToPage(css) { 172 | return; 173 | } 174 | 175 | 176 | 177 | /* 178 | * Input or text elements are considered focusable and able to receieve their own keyboard events, 179 | * and will enter enter mode if focused. Also note that the "contentEditable" attribute can be set on 180 | * any element which makes it a rich text editor, like the notes on jjot.com. 181 | * Note: we used to discriminate for text-only inputs, but this is not accurate since all input fields 182 | * can be controlled via the keyboard, particuarlly SELECT combo boxes. 183 | */ 184 | function isEditable(target) { 185 | if (target.getAttribute("contentEditable") == "true") 186 | return true; 187 | var focusableInputs = ["input", "textarea", "select", "button"]; 188 | return focusableInputs.indexOf(target.tagName.toLowerCase()) >= 0; 189 | } 190 | 191 | 192 | 193 | /* 194 | * Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically 195 | * unfocused. 196 | */ 197 | function isEmbed(element) { return ["EMBED", "OBJECT"].indexOf(element.tagName) > 0; } 198 | 199 | 200 | 201 | 202 | // ========================== 203 | // Message handling functions 204 | // ========================== 205 | 206 | /* 207 | * All messages are handled by this function 208 | */ 209 | function handleMessage(msg) { 210 | // Attempt to call a function with the same name as the message name 211 | switch(msg.name) { 212 | case 'setSettings': 213 | setSettings(msg.message); 214 | break; 215 | case 'setActive': 216 | setActive(msg.message); 217 | break; 218 | } 219 | } 220 | 221 | /* 222 | * Callback to pass settings to injected script 223 | */ 224 | function setSettings(msg) { 225 | settings = msg; 226 | bindKeyCodesToActions(); 227 | } 228 | 229 | /* 230 | * Enable or disable the extension on this tab 231 | */ 232 | function setActive(msg) { 233 | 234 | extensionActive = msg; 235 | if(msg) { 236 | bindKeyCodesToActions(); 237 | } else { 238 | unbindKeyCodes(); 239 | } 240 | } 241 | 242 | /* 243 | * Check to see if the current url is in the blacklist 244 | */ 245 | function isEnabledForUrl() { 246 | var excludedUrls, isEnabled, regexp, url, _i, _len; 247 | excludedUrls = settings.excludedUrls.split(","); 248 | for (_i = 0, _len = excludedUrls.length; _i < _len; _i++) { 249 | url = excludedUrls[_i]; 250 | regexp = new RegExp("^" + url.replace(/\*/g, ".*") + "$"); 251 | if (document.URL.match(regexp)) { 252 | return false; 253 | } 254 | } 255 | return true; 256 | }; 257 | 258 | // Add event listener 259 | safari.self.addEventListener("message", handleMessage, false); 260 | // Retrieve settings 261 | safari.self.tab.dispatchMessage('getSettings', ''); 262 | -------------------------------------------------------------------------------- /vimari.safariextension/linkHints.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This implements link hinting. Typing "F" will enter link-hinting mode, where all clickable items on 3 | * the page have a hint marker displayed containing a sequence of letters. Typing those letters will select 4 | * a link. 5 | * 6 | * The characters we use to show link hints are a user-configurable option. By default they're the home row. 7 | * The CSS which is used on the link hints is also a configurable option. 8 | */ 9 | 10 | var hintMarkers = []; 11 | var hintMarkerContainingDiv = null; 12 | // The characters that were typed in while in "link hints" mode. 13 | var hintKeystrokeQueue = []; 14 | var linkHintsModeActivated = false; 15 | var shouldOpenLinkHintInNewTab = false; 16 | var shouldOpenLinkHintWithQueue = false; 17 | // Whether link hint's "open in current/new tab" setting is currently toggled 18 | var openLinkModeToggle = false; 19 | // Whether we have added to the page the CSS needed to display link hints. 20 | var linkHintsCssAdded = false; 21 | 22 | // We need this as a top-level function because our command system doesn't yet support arguments. 23 | function activateLinkHintsModeToOpenInNewTab() { activateLinkHintsMode(true, false); } 24 | 25 | function activateLinkHintsModeWithQueue() { activateLinkHintsMode(true, true); } 26 | 27 | function activateLinkHintsMode(openInNewTab, withQueue) { 28 | if (!linkHintsCssAdded) 29 | addCssToPage(linkHintCss); // linkHintCss is declared by vimiumFrontend.js 30 | linkHintCssAdded = true; 31 | linkHintsModeActivated = true; 32 | setOpenLinkMode(openInNewTab, withQueue); 33 | buildLinkHints(); 34 | document.addEventListener("keydown", onKeyDownInLinkHintsMode, true); 35 | document.addEventListener("keyup", onKeyUpInLinkHintsMode, true); 36 | } 37 | 38 | function setOpenLinkMode(openInNewTab, withQueue) { 39 | shouldOpenLinkHintInNewTab = openInNewTab; 40 | shouldOpenLinkHintWithQueue = withQueue; 41 | return; 42 | /* 43 | if (shouldOpenLinkHintWithQueue) { 44 | HUD.show("Open multiple links in a new tab"); 45 | } else { 46 | if (shouldOpenLinkHintInNewTab) 47 | HUD.show("Open link in new tab"); 48 | else 49 | HUD.show("Open link in current tab"); 50 | }*/ 51 | } 52 | 53 | /* 54 | * Builds and displays link hints for every visible clickable item on the page. 55 | */ 56 | function buildLinkHints() { 57 | var visibleElements = getVisibleClickableElements(); 58 | 59 | // Initialize the number used to generate the character hints to be as many digits as we need to 60 | // highlight all the links on the page; we don't want some link hints to have more chars than others. 61 | var digitsNeeded = Math.ceil(logXOfBase(visibleElements.length, settings.linkHintCharacters.length)); 62 | var linkHintNumber = 0; 63 | for (var i = 0; i < visibleElements.length; i++) { 64 | hintMarkers.push(createMarkerFor(visibleElements[i], linkHintNumber, digitsNeeded)); 65 | linkHintNumber++; 66 | } 67 | // Note(philc): Append these markers as top level children instead of as child nodes to the link itself, 68 | // because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat 69 | // that if you scroll the page and the link has position=fixed, the marker will not stay fixed. 70 | // Also note that adding these nodes to document.body all at once is significantly faster than one-by-one. 71 | hintMarkerContainingDiv = document.createElement("div"); 72 | hintMarkerContainingDiv.id = "vimiumHintMarkerContainer"; 73 | hintMarkerContainingDiv.className = "vimiumReset"; 74 | for (var i = 0; i < hintMarkers.length; i++) 75 | hintMarkerContainingDiv.appendChild(hintMarkers[i]); 76 | document.body.appendChild(hintMarkerContainingDiv); 77 | } 78 | 79 | function logXOfBase(x, base) { return Math.log(x) / Math.log(base); } 80 | 81 | /* 82 | * Returns all clickable elements that are not hidden and are in the current viewport. 83 | * We prune invisible elements partly for performance reasons, but moreso it's to decrease the number 84 | * of digits needed to enumerate all of the links on screen. 85 | */ 86 | function getVisibleClickableElements() { 87 | // Get all clickable elements. 88 | var elements = getClickableElements(); 89 | 90 | // Get those that are visible too. 91 | var visibleElements = []; 92 | 93 | for (var i = 0; i < elements.length; i++) { 94 | var element = elements[i]; 95 | 96 | var selectedRect = getFirstVisibleRect(element); 97 | if (selectedRect) { 98 | visibleElements.push(selectedRect); 99 | } 100 | } 101 | 102 | return visibleElements; 103 | } 104 | 105 | function getClickableElements() { 106 | var elements = document.getElementsByTagName('*'); 107 | var clickableElements = []; 108 | for (var i = 0; i < elements.length; i++) { 109 | var element = elements[i]; 110 | if (isClickable(element)) 111 | clickableElements.push(element); 112 | } 113 | return clickableElements; 114 | } 115 | 116 | function isClickable(element) { 117 | var name = element.nodeName.toLowerCase(); 118 | var role = element.getAttribute('role'); 119 | 120 | return ( 121 | // normal html elements that can be clicked 122 | name == 'a' || 123 | name == 'button' || 124 | name == 'input' && element.getAttribute('type') != 'hidden' || 125 | name == 'select' || 126 | name == 'textarea' || 127 | // elements having an ARIA role implying clickability 128 | // (see http://www.w3.org/TR/wai-aria/roles#widget_roles) 129 | role == 'button' || 130 | role == 'checkbox' || 131 | role == 'combobox' || 132 | role == 'link' || 133 | role == 'menuitem' || 134 | role == 'menuitemcheckbox' || 135 | role == 'menuitemradio' || 136 | role == 'radio' || 137 | role == 'tab' || 138 | role == 'textbox' || 139 | // other ways by which we can know an element is clickable 140 | element.hasAttribute('onclick') || 141 | settings.detectByCursorStyle && window.getComputedStyle(element).cursor == 'pointer' && 142 | (!element.parentNode || 143 | window.getComputedStyle(element.parentNode).cursor != 'pointer') 144 | ); 145 | } 146 | 147 | /* 148 | * Get firs visible rect under an element. 149 | * 150 | * Inline elements can have more than one rect. 151 | * Block elemens only have one rect. 152 | * So, in general, add element's first visible rect, if any. 153 | * If element does not have any visible rect, 154 | * it can still be wrapping other visible children. 155 | * So, in that case, recurse to get the first visible rect 156 | * of the first child that has one. 157 | */ 158 | function getFirstVisibleRect(element) { 159 | // find visible clientRect of element itself 160 | var clientRects = element.getClientRects(); 161 | for (var i = 0; i < clientRects.length; i++) { 162 | var clientRect = clientRects[i]; 163 | if (isVisible(element, clientRect)) { 164 | return {element: element, rect: clientRect}; 165 | } 166 | } 167 | // find visible clientRect of child 168 | for (var j = 0; j < element.children.length; j++) { 169 | var childClientRect = getFirstVisibleRect(element.children[j]); 170 | if (childClientRect) { 171 | return childClientRect; 172 | } 173 | } 174 | return null; 175 | } 176 | 177 | /* 178 | * Returns true if element is visible. 179 | */ 180 | function isVisible(element, clientRect) { 181 | // Exclude links which have just a few pixels on screen, because the link hints won't show for them anyway. 182 | var zoomFactor = currentZoomLevel / 100.0; 183 | if (!clientRect || clientRect.top < 0 || clientRect.top * zoomFactor >= window.innerHeight - 4 || 184 | clientRect.left < 0 || clientRect.left * zoomFactor >= window.innerWidth - 4) 185 | return false; 186 | 187 | if (clientRect.width < 3 || clientRect.height < 3) 188 | return false; 189 | 190 | // eliminate invisible elements (see test_harnesses/visibility_test.html) 191 | var computedStyle = window.getComputedStyle(element, null); 192 | if (computedStyle.getPropertyValue('visibility') != 'visible' || 193 | computedStyle.getPropertyValue('display') == 'none') 194 | return false; 195 | 196 | // Eliminate elements hidden by another overlapping element. 197 | // To do that, get topmost element at some offset from upper-left corner of clientRect 198 | // and check whether it is the element itself or one of its descendants. 199 | // The offset is needed to account for coordinates truncation and elements with rounded borders. 200 | // 201 | // Coordinates truncation occcurs when using zoom. In that case, clientRect coords should be float, 202 | // but we get integers instead. That makes so that elementFromPoint(clientRect.left, clientRect.top) 203 | // sometimes returns an element different from the one clientRect was obtained from. 204 | // So we introduce an offset to make sure elementFromPoint hits the right element. 205 | // 206 | // For elements with a rounded topleft border, the upper left corner lies outside the element. 207 | // Then, we need an offset to get to the point nearest to the upper left corner, but within border. 208 | var coordTruncationOffset = 2, // A value of 1 has been observed not to be enough, 209 | // so we heuristically choose 2, which seems to work well. 210 | // We know a value of 2 is still safe (lies within the element) because, 211 | // from the code above, widht & height are >= 3. 212 | radius = parseFloat(computedStyle.borderTopLeftRadius), 213 | roundedBorderOffset = Math.ceil(radius * (1 - Math.sin(Math.PI / 4))), 214 | offset = Math.max(coordTruncationOffset, roundedBorderOffset); 215 | if (offset >= clientRect.width || offset >= clientRect.height) 216 | return false; 217 | var el = document.elementFromPoint(clientRect.left + offset, clientRect.top + offset); 218 | while (el && el != element) 219 | el = el.parentNode; 220 | if (!el) 221 | return false; 222 | 223 | return true; 224 | } 225 | 226 | function onKeyDownInLinkHintsMode(event) { 227 | console.log("Key Down"); 228 | if (event.keyCode == keyCodes.shiftKey && !openLinkModeToggle) { 229 | // Toggle whether to open link in a new or current tab. 230 | setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue); 231 | openLinkModeToggle = true; 232 | } 233 | 234 | var keyChar = getKeyChar(event); 235 | if (!keyChar) 236 | return; 237 | 238 | // TODO(philc): Ignore keys that have modifiers. 239 | if (isEscape(event)) { 240 | deactivateLinkHintsMode(); 241 | } else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) { 242 | if (hintKeystrokeQueue.length == 0) { 243 | deactivateLinkHintsMode(); 244 | } else { 245 | hintKeystrokeQueue.pop(); 246 | updateLinkHints(); 247 | } 248 | } else if (settings.linkHintCharacters.indexOf(keyChar) >= 0) { 249 | hintKeystrokeQueue.push(keyChar); 250 | updateLinkHints(); 251 | } else { 252 | return; 253 | } 254 | 255 | event.stopPropagation(); 256 | event.preventDefault(); 257 | } 258 | 259 | function onKeyUpInLinkHintsMode(event) { 260 | if (event.keyCode == keyCodes.shiftKey && openLinkModeToggle) { 261 | // Revert toggle on whether to open link in new or current tab. 262 | setOpenLinkMode(!shouldOpenLinkHintInNewTab, shouldOpenLinkHintWithQueue); 263 | openLinkModeToggle = false; 264 | } 265 | event.stopPropagation(); 266 | event.preventDefault(); 267 | } 268 | 269 | /* 270 | * Updates the visibility of link hints on screen based on the keystrokes typed thus far. If only one 271 | * link hint remains, click on that link and exit link hints mode. 272 | */ 273 | function updateLinkHints() { 274 | var matchString = hintKeystrokeQueue.join(""); 275 | var linksMatched = highlightLinkMatches(matchString); 276 | if (linksMatched.length == 0) 277 | deactivateLinkHintsMode(); 278 | else if (linksMatched.length == 1) { 279 | var matchedLink = linksMatched[0]; 280 | if (isSelectable(matchedLink)) { 281 | matchedLink.focus(); 282 | // When focusing a textbox, put the selection caret at the end of the textbox's contents. 283 | matchedLink.setSelectionRange(matchedLink.value.length, matchedLink.value.length); 284 | deactivateLinkHintsMode(); 285 | } else { 286 | // When we're opening the link in the current tab, don't navigate to the selected link immediately; 287 | // we want to give the user some feedback depicting which link they've selected by focusing it. 288 | if (shouldOpenLinkHintWithQueue) { 289 | simulateClick(matchedLink); 290 | resetLinkHintsMode(); 291 | } else if (shouldOpenLinkHintInNewTab) { 292 | simulateClick(matchedLink); 293 | matchedLink.focus(); 294 | deactivateLinkHintsMode(); 295 | } else { 296 | setTimeout(function() { simulateClick(matchedLink); }, 400); 297 | matchedLink.focus(); 298 | deactivateLinkHintsMode(); 299 | } 300 | } 301 | } 302 | } 303 | 304 | /* 305 | * Selectable means the element has a text caret; this is not the same as "focusable". 306 | */ 307 | function isSelectable(element) { 308 | var selectableTypes = ["search", "text", "password"]; 309 | return (element.tagName == "INPUT" && selectableTypes.indexOf(element.type) >= 0) || 310 | element.tagName == "TEXTAREA"; 311 | } 312 | 313 | /* 314 | * Hides link hints which do not match the given search string. To allow the backspace key to work, this 315 | * will also show link hints which do match but were previously hidden. 316 | */ 317 | function highlightLinkMatches(searchString) { 318 | var linksMatched = []; 319 | for (var i = 0; i < hintMarkers.length; i++) { 320 | var linkMarker = hintMarkers[i]; 321 | if (linkMarker.getAttribute("hintString").indexOf(searchString) == 0) { 322 | if (linkMarker.style.display == "none") 323 | linkMarker.style.display = ""; 324 | for (var j = 0; j < linkMarker.childNodes.length; j++) 325 | linkMarker.childNodes[j].className = (j >= searchString.length) ? "" : "matchingCharacter"; 326 | linksMatched.push(linkMarker.clickableItem); 327 | } else { 328 | linkMarker.style.display = "none"; 329 | } 330 | } 331 | return linksMatched; 332 | } 333 | 334 | /* 335 | * Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of 336 | * the hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits. 337 | */ 338 | function numberToHintString(number, numHintDigits) { 339 | var base = settings.linkHintCharacters.length; 340 | var hintString = []; 341 | var remainder = 0; 342 | do { 343 | remainder = number % base; 344 | hintString.unshift(settings.linkHintCharacters[remainder]); 345 | number -= remainder; 346 | number /= Math.floor(base); 347 | } while (number > 0); 348 | 349 | // Pad the hint string we're returning so that it matches numHintDigits. 350 | var hintStringLength = hintString.length; 351 | for (var i = 0; i < numHintDigits - hintStringLength; i++) 352 | hintString.unshift(settings.linkHintCharacters[0]); 353 | return hintString.join(""); 354 | } 355 | 356 | function simulateClick(link) { 357 | // Configure events with appropriate meta key (CMD on Mac, CTRL on windows) 358 | // to open links in new tabs if necessary. 359 | var metaKey = (platform == "Mac" && shouldOpenLinkHintInNewTab); 360 | var ctrlKey = (platform != "Mac" && shouldOpenLinkHintInNewTab); 361 | 362 | // A full click will be simulated by the sequence: 363 | // focus --> mouseDown --> mouseUp --> click 364 | // The focus step is there because Safari has been observed to do so. 365 | 366 | link.focus(); 367 | 368 | var mouseDownEvent = document.createEvent("MouseEvents"); 369 | mouseDownEvent.initMouseEvent("mousedown", true, true, window, 1, 0, 0, 0, 0, ctrlKey, false, false, metaKey, 0, null); 370 | link.dispatchEvent(mouseDownEvent) 371 | 372 | var mouseUpEvent = document.createEvent("MouseEvents"); 373 | mouseUpEvent.initMouseEvent("mouseup", true, true, window, 1, 0, 0, 0, 0, ctrlKey, false, false, metaKey, 0, null); 374 | link.dispatchEvent(mouseUpEvent); 375 | 376 | var clickEvent = document.createEvent("MouseEvents"); 377 | clickEvent.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, ctrlKey, false, false, metaKey, 0, null); 378 | link.dispatchEvent(clickEvent); 379 | 380 | // On click event dispatch, Firefox will not execute the link's default action, but Webkit will. 381 | // This is a Safari extension, so that's ok for now, if no easy cross-browser solution is available. 382 | } 383 | 384 | function deactivateLinkHintsMode() { 385 | if (hintMarkerContainingDiv) 386 | hintMarkerContainingDiv.parentNode.removeChild(hintMarkerContainingDiv); 387 | hintMarkerContainingDiv = null; 388 | hintMarkers = []; 389 | hintKeystrokeQueue = []; 390 | document.removeEventListener("keydown", onKeyDownInLinkHintsMode, true); 391 | document.removeEventListener("keyup", onKeyUpInLinkHintsMode, true); 392 | linkHintsModeActivated = false; 393 | //HUD.hide(); 394 | } 395 | 396 | function resetLinkHintsMode() { 397 | deactivateLinkHintsMode(); 398 | activateLinkHintsModeWithQueue(); 399 | } 400 | 401 | /* 402 | * Creates a link marker for the given link. 403 | */ 404 | function createMarkerFor(link, linkHintNumber, linkHintDigits) { 405 | var hintString = numberToHintString(linkHintNumber, linkHintDigits); 406 | var marker = document.createElement("div"); 407 | marker.className = "internalVimiumHintMarker vimiumReset"; 408 | var innerHTML = []; 409 | // Make each hint character a span, so that we can highlight the typed characters as you type them. 410 | for (var i = 0; i < hintString.length; i++) 411 | innerHTML.push('' + hintString[i].toUpperCase() + ''); 412 | marker.innerHTML = innerHTML.join(""); 413 | marker.setAttribute("hintString", hintString); 414 | 415 | // Note: this call will be expensive if we modify the DOM in between calls. 416 | var clientRect = link.rect; 417 | // The coordinates given by the window do not have the zoom factor included since the zoom is set only on 418 | // the document node. 419 | var zoomFactor = currentZoomLevel / 100.0; 420 | marker.style.left = clientRect.left + window.scrollX / zoomFactor + "px"; 421 | marker.style.top = clientRect.top + window.scrollY / zoomFactor + "px"; 422 | 423 | marker.clickableItem = link.element; 424 | return marker; 425 | } 426 | -------------------------------------------------------------------------------- /vimari.safariextension/mousetrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Craig Campbell 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Mousetrap is a simple keyboard shortcut library for Javascript with 17 | * no external dependencies 18 | * 19 | * @version 1.3.0 20 | * @url craig.is/killing/mice 21 | */ 22 | (function() { 23 | 24 | /** 25 | * mapping of special keycodes to their corresponding keys 26 | * 27 | * everything in this dictionary cannot use keypress events 28 | * so it has to be here to map to the correct keycodes for 29 | * keyup/keydown events 30 | * 31 | * @type {Object} 32 | */ 33 | var _MAP = { 34 | 8: 'backspace', 35 | 9: 'tab', 36 | 13: 'enter', 37 | 16: 'shift', 38 | 17: 'ctrl', 39 | 18: 'alt', 40 | 20: 'capslock', 41 | 27: 'esc', 42 | 32: 'space', 43 | 33: 'pageup', 44 | 34: 'pagedown', 45 | 35: 'end', 46 | 36: 'home', 47 | 37: 'left', 48 | 38: 'up', 49 | 39: 'right', 50 | 40: 'down', 51 | 45: 'ins', 52 | 46: 'del', 53 | 91: 'meta', 54 | 93: 'meta', 55 | 224: 'meta' 56 | }, 57 | 58 | /** 59 | * mapping for special characters so they can support 60 | * 61 | * this dictionary is only used incase you want to bind a 62 | * keyup or keydown event to one of these keys 63 | * 64 | * @type {Object} 65 | */ 66 | _KEYCODE_MAP = { 67 | 106: '*', 68 | 107: '+', 69 | 109: '-', 70 | 110: '.', 71 | 111 : '/', 72 | 186: ';', 73 | 187: '=', 74 | 188: ',', 75 | 189: '-', 76 | 190: '.', 77 | 191: '/', 78 | 192: '`', 79 | 219: '[', 80 | 220: '\\', 81 | 221: ']', 82 | 222: '\'' 83 | }, 84 | 85 | /** 86 | * this is a mapping of keys that require shift on a US keypad 87 | * back to the non shift equivelents 88 | * 89 | * this is so you can use keyup events with these keys 90 | * 91 | * note that this will only work reliably on US keyboards 92 | * 93 | * @type {Object} 94 | */ 95 | _SHIFT_MAP = { 96 | '~': '`', 97 | '!': '1', 98 | '@': '2', 99 | '#': '3', 100 | '$': '4', 101 | '%': '5', 102 | '^': '6', 103 | '&': '7', 104 | '*': '8', 105 | '(': '9', 106 | ')': '0', 107 | '_': '-', 108 | '+': '=', 109 | ':': ';', 110 | '\"': '\'', 111 | '<': ',', 112 | '>': '.', 113 | '?': '/', 114 | '|': '\\' 115 | }, 116 | 117 | /** 118 | * this is a list of special strings you can use to map 119 | * to modifier keys when you specify your keyboard shortcuts 120 | * 121 | * @type {Object} 122 | */ 123 | _SPECIAL_ALIASES = { 124 | 'option': 'alt', 125 | 'command': 'meta', 126 | 'return': 'enter', 127 | 'escape': 'esc' 128 | }, 129 | 130 | /** 131 | * variable to store the flipped version of _MAP from above 132 | * needed to check if we should use keypress or not when no action 133 | * is specified 134 | * 135 | * @type {Object|undefined} 136 | */ 137 | _REVERSE_MAP, 138 | 139 | /** 140 | * a list of all the callbacks setup via Mousetrap.bind() 141 | * 142 | * @type {Object} 143 | */ 144 | _callbacks = {}, 145 | 146 | /** 147 | * direct map of string combinations to callbacks used for trigger() 148 | * 149 | * @type {Object} 150 | */ 151 | _directMap = {}, 152 | 153 | /** 154 | * keeps track of what level each sequence is at since multiple 155 | * sequences can start out with the same sequence 156 | * 157 | * @type {Object} 158 | */ 159 | _sequenceLevels = {}, 160 | 161 | /** 162 | * variable to store the setTimeout call 163 | * 164 | * @type {null|number} 165 | */ 166 | _resetTimer, 167 | 168 | /** 169 | * temporary state where we will ignore the next keyup 170 | * 171 | * @type {boolean|string} 172 | */ 173 | _ignoreNextKeyup = false, 174 | 175 | /** 176 | * are we currently inside of a sequence? 177 | * type of action ("keyup" or "keydown" or "keypress") or false 178 | * 179 | * @type {boolean|string} 180 | */ 181 | _sequenceType = false; 182 | 183 | /** 184 | * loop through the f keys, f1 to f19 and add them to the map 185 | * programatically 186 | */ 187 | for (var i = 1; i < 20; ++i) { 188 | _MAP[111 + i] = 'f' + i; 189 | } 190 | 191 | /** 192 | * loop through to map numbers on the numeric keypad 193 | */ 194 | for (i = 0; i <= 9; ++i) { 195 | _MAP[i + 96] = i; 196 | } 197 | 198 | /** 199 | * cross browser add event method 200 | * 201 | * @param {Element|HTMLDocument} object 202 | * @param {string} type 203 | * @param {Function} callback 204 | * @returns void 205 | */ 206 | function _addEvent(object, type, callback) { 207 | if (object.addEventListener) { 208 | object.addEventListener(type, callback, false); 209 | return; 210 | } 211 | 212 | object.attachEvent('on' + type, callback); 213 | } 214 | 215 | /** 216 | * takes the event and returns the key character 217 | * 218 | * @param {Event} e 219 | * @return {string} 220 | */ 221 | function _characterFromEvent(e) { 222 | 223 | // for keypress events we should return the character as is 224 | if (e.type == 'keypress') { 225 | return String.fromCharCode(e.which); 226 | } 227 | 228 | // for non keypress events the special maps are needed 229 | if (_MAP[e.which]) { 230 | return _MAP[e.which]; 231 | } 232 | 233 | if (_KEYCODE_MAP[e.which]) { 234 | return _KEYCODE_MAP[e.which]; 235 | } 236 | 237 | // if it is not in the special map 238 | return String.fromCharCode(e.which).toLowerCase(); 239 | } 240 | 241 | /** 242 | * checks if two arrays are equal 243 | * 244 | * @param {Array} modifiers1 245 | * @param {Array} modifiers2 246 | * @returns {boolean} 247 | */ 248 | function _modifiersMatch(modifiers1, modifiers2) { 249 | return modifiers1.sort().join(',') === modifiers2.sort().join(','); 250 | } 251 | 252 | /** 253 | * resets all sequence counters except for the ones passed in 254 | * 255 | * @param {Object} doNotReset 256 | * @returns void 257 | */ 258 | function _resetSequences(doNotReset, maxLevel) { 259 | doNotReset = doNotReset || {}; 260 | 261 | var activeSequences = false, 262 | key; 263 | 264 | for (key in _sequenceLevels) { 265 | if (doNotReset[key] && _sequenceLevels[key] > maxLevel) { 266 | activeSequences = true; 267 | continue; 268 | } 269 | _sequenceLevels[key] = 0; 270 | } 271 | 272 | if (!activeSequences) { 273 | _sequenceType = false; 274 | } 275 | } 276 | 277 | /** 278 | * finds all callbacks that match based on the keycode, modifiers, 279 | * and action 280 | * 281 | * @param {string} character 282 | * @param {Array} modifiers 283 | * @param {Event|Object} e 284 | * @param {boolean=} remove - should we remove any matches 285 | * @param {string=} combination 286 | * @returns {Array} 287 | */ 288 | function _getMatches(character, modifiers, e, remove, combination) { 289 | var i, 290 | callback, 291 | matches = [], 292 | action = e.type; 293 | 294 | // if there are no events related to this keycode 295 | if (!_callbacks[character]) { 296 | return []; 297 | } 298 | 299 | // if a modifier key is coming up on its own we should allow it 300 | if (action == 'keyup' && _isModifier(character)) { 301 | modifiers = [character]; 302 | } 303 | 304 | // loop through all callbacks for the key that was pressed 305 | // and see if any of them match 306 | for (i = 0; i < _callbacks[character].length; ++i) { 307 | callback = _callbacks[character][i]; 308 | 309 | // if this is a sequence but it is not at the right level 310 | // then move onto the next match 311 | if (callback.seq && _sequenceLevels[callback.seq] != callback.level) { 312 | continue; 313 | } 314 | 315 | // if the action we are looking for doesn't match the action we got 316 | // then we should keep going 317 | if (action != callback.action) { 318 | continue; 319 | } 320 | 321 | // if this is a keypress event and the meta key and control key 322 | // are not pressed that means that we need to only look at the 323 | // character, otherwise check the modifiers as well 324 | // 325 | // chrome will not fire a keypress if meta or control is down 326 | // safari will fire a keypress if meta or meta+shift is down 327 | // firefox will fire a keypress if meta or control is down 328 | if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { 329 | 330 | // remove is used so if you change your mind and call bind a 331 | // second time with a new function the first one is overwritten 332 | if (remove && callback.combo == combination) { 333 | _callbacks[character].splice(i, 1); 334 | } 335 | 336 | matches.push(callback); 337 | } 338 | } 339 | 340 | return matches; 341 | } 342 | 343 | /** 344 | * takes a key event and figures out what the modifiers are 345 | * 346 | * @param {Event} e 347 | * @returns {Array} 348 | */ 349 | function _eventModifiers(e) { 350 | var modifiers = []; 351 | 352 | if (e.shiftKey) { 353 | modifiers.push('shift'); 354 | } 355 | 356 | if (e.altKey) { 357 | modifiers.push('alt'); 358 | } 359 | 360 | if (e.ctrlKey) { 361 | modifiers.push('ctrl'); 362 | } 363 | 364 | if (e.metaKey) { 365 | modifiers.push('meta'); 366 | } 367 | 368 | return modifiers; 369 | } 370 | 371 | /** 372 | * actually calls the callback function 373 | * 374 | * if your callback function returns false this will use the jquery 375 | * convention - prevent default and stop propogation on the event 376 | * 377 | * @param {Function} callback 378 | * @param {Event} e 379 | * @returns void 380 | */ 381 | function _fireCallback(callback, e, combo) { 382 | 383 | // if this event should not happen stop here 384 | if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo)) { 385 | return; 386 | } 387 | 388 | if (callback(e, combo) === false) { 389 | if (e.preventDefault) { 390 | e.preventDefault(); 391 | } 392 | 393 | if (e.stopPropagation) { 394 | e.stopPropagation(); 395 | } 396 | 397 | e.returnValue = false; 398 | e.cancelBubble = true; 399 | } 400 | } 401 | 402 | /** 403 | * handles a character key event 404 | * 405 | * @param {string} character 406 | * @param {Event} e 407 | * @returns void 408 | */ 409 | function _handleCharacter(character, e) { 410 | var callbacks = _getMatches(character, _eventModifiers(e), e), 411 | i, 412 | doNotReset = {}, 413 | maxLevel = 0, 414 | processedSequenceCallback = false; 415 | 416 | // loop through matching callbacks for this key event 417 | for (i = 0; i < callbacks.length; ++i) { 418 | 419 | // fire for all sequence callbacks 420 | // this is because if for example you have multiple sequences 421 | // bound such as "g i" and "g t" they both need to fire the 422 | // callback for matching g cause otherwise you can only ever 423 | // match the first one 424 | if (callbacks[i].seq) { 425 | processedSequenceCallback = true; 426 | 427 | // as we loop through keep track of the max 428 | // any sequence at a lower level will be discarded 429 | maxLevel = Math.max(maxLevel, callbacks[i].level); 430 | 431 | // keep a list of which sequences were matches for later 432 | doNotReset[callbacks[i].seq] = 1; 433 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); 434 | continue; 435 | } 436 | 437 | // if there were no sequence matches but we are still here 438 | // that means this is a regular match so we should fire that 439 | if (!processedSequenceCallback && !_sequenceType) { 440 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); 441 | } 442 | } 443 | 444 | // if you are inside of a sequence and the key you are pressing 445 | // is not a modifier key then we should reset all sequences 446 | // that were not matched by this key event 447 | if (e.type == _sequenceType && !_isModifier(character)) { 448 | _resetSequences(doNotReset, maxLevel); 449 | } 450 | } 451 | 452 | /** 453 | * handles a keydown event 454 | * 455 | * @param {Event} e 456 | * @returns void 457 | */ 458 | function _handleKey(e) { 459 | 460 | // normalize e.which for key events 461 | // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion 462 | if (typeof e.which !== 'number') { 463 | e.which = e.keyCode; 464 | } 465 | 466 | var character = _characterFromEvent(e); 467 | 468 | // no character found then stop 469 | if (!character) { 470 | return; 471 | } 472 | 473 | if (e.type == 'keyup' && _ignoreNextKeyup == character) { 474 | _ignoreNextKeyup = false; 475 | return; 476 | } 477 | 478 | _handleCharacter(character, e); 479 | } 480 | 481 | /** 482 | * determines if the keycode specified is a modifier key or not 483 | * 484 | * @param {string} key 485 | * @returns {boolean} 486 | */ 487 | function _isModifier(key) { 488 | return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; 489 | } 490 | 491 | /** 492 | * called to set a 1 second timeout on the specified sequence 493 | * 494 | * this is so after each key press in the sequence you have 1 second 495 | * to press the next key before you have to start over 496 | * 497 | * @returns void 498 | */ 499 | function _resetSequenceTimer() { 500 | clearTimeout(_resetTimer); 501 | _resetTimer = setTimeout(_resetSequences, 1000); 502 | } 503 | 504 | /** 505 | * reverses the map lookup so that we can look for specific keys 506 | * to see what can and can't use keypress 507 | * 508 | * @return {Object} 509 | */ 510 | function _getReverseMap() { 511 | if (!_REVERSE_MAP) { 512 | _REVERSE_MAP = {}; 513 | for (var key in _MAP) { 514 | 515 | // pull out the numeric keypad from here cause keypress should 516 | // be able to detect the keys from the character 517 | if (key > 95 && key < 112) { 518 | continue; 519 | } 520 | 521 | if (_MAP.hasOwnProperty(key)) { 522 | _REVERSE_MAP[_MAP[key]] = key; 523 | } 524 | } 525 | } 526 | return _REVERSE_MAP; 527 | } 528 | 529 | /** 530 | * picks the best action based on the key combination 531 | * 532 | * @param {string} key - character for key 533 | * @param {Array} modifiers 534 | * @param {string=} action passed in 535 | */ 536 | function _pickBestAction(key, modifiers, action) { 537 | 538 | // if no action was picked in we should try to pick the one 539 | // that we think would work best for this key 540 | if (!action) { 541 | action = _getReverseMap()[key] ? 'keydown' : 'keypress'; 542 | } 543 | 544 | // modifier keys don't work as expected with keypress, 545 | // switch to keydown 546 | if (action == 'keypress' && modifiers.length) { 547 | action = 'keydown'; 548 | } 549 | 550 | return action; 551 | } 552 | 553 | /** 554 | * binds a key sequence to an event 555 | * 556 | * @param {string} combo - combo specified in bind call 557 | * @param {Array} keys 558 | * @param {Function} callback 559 | * @param {string=} action 560 | * @returns void 561 | */ 562 | function _bindSequence(combo, keys, callback, action) { 563 | 564 | // start off by adding a sequence level record for this combination 565 | // and setting the level to 0 566 | _sequenceLevels[combo] = 0; 567 | 568 | // if there is no action pick the best one for the first key 569 | // in the sequence 570 | if (!action) { 571 | action = _pickBestAction(keys[0], []); 572 | } 573 | 574 | /** 575 | * callback to increase the sequence level for this sequence and reset 576 | * all other sequences that were active 577 | * 578 | * @param {Event} e 579 | * @returns void 580 | */ 581 | var _increaseSequence = function(e) { 582 | _sequenceType = action; 583 | ++_sequenceLevels[combo]; 584 | _resetSequenceTimer(); 585 | }, 586 | 587 | /** 588 | * wraps the specified callback inside of another function in order 589 | * to reset all sequence counters as soon as this sequence is done 590 | * 591 | * @param {Event} e 592 | * @returns void 593 | */ 594 | _callbackAndReset = function(e) { 595 | _fireCallback(callback, e, combo); 596 | 597 | // we should ignore the next key up if the action is key down 598 | // or keypress. this is so if you finish a sequence and 599 | // release the key the final key will not trigger a keyup 600 | if (action !== 'keyup') { 601 | _ignoreNextKeyup = _characterFromEvent(e); 602 | } 603 | 604 | // weird race condition if a sequence ends with the key 605 | // another sequence begins with 606 | setTimeout(_resetSequences, 10); 607 | }, 608 | i; 609 | 610 | // loop through keys one at a time and bind the appropriate callback 611 | // function. for any key leading up to the final one it should 612 | // increase the sequence. after the final, it should reset all sequences 613 | for (i = 0; i < keys.length; ++i) { 614 | _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i); 615 | } 616 | } 617 | 618 | /** 619 | * binds a single keyboard combination 620 | * 621 | * @param {string} combination 622 | * @param {Function} callback 623 | * @param {string=} action 624 | * @param {string=} sequenceName - name of sequence if part of sequence 625 | * @param {number=} level - what part of the sequence the command is 626 | * @returns void 627 | */ 628 | function _bindSingle(combination, callback, action, sequenceName, level) { 629 | 630 | // store a direct mapped reference for use with Mousetrap.trigger 631 | _directMap[combination + ':' + action] = callback; 632 | 633 | // make sure multiple spaces in a row become a single space 634 | combination = combination.replace(/\s+/g, ' '); 635 | 636 | var sequence = combination.split(' '), 637 | i, 638 | key, 639 | keys, 640 | modifiers = []; 641 | 642 | // if this pattern is a sequence of keys then run through this method 643 | // to reprocess each pattern one key at a time 644 | if (sequence.length > 1) { 645 | _bindSequence(combination, sequence, callback, action); 646 | return; 647 | } 648 | 649 | // take the keys from this pattern and figure out what the actual 650 | // pattern is all about 651 | keys = combination === '+' ? ['+'] : combination.split('+'); 652 | 653 | for (i = 0; i < keys.length; ++i) { 654 | key = keys[i]; 655 | 656 | // normalize key names 657 | if (_SPECIAL_ALIASES[key]) { 658 | key = _SPECIAL_ALIASES[key]; 659 | } 660 | 661 | // if this is not a keypress event then we should 662 | // be smart about using shift keys 663 | // this will only work for US keyboards however 664 | if (action && action != 'keypress' && _SHIFT_MAP[key]) { 665 | key = _SHIFT_MAP[key]; 666 | modifiers.push('shift'); 667 | } 668 | 669 | // if this key is a modifier then add it to the list of modifiers 670 | if (_isModifier(key)) { 671 | modifiers.push(key); 672 | } 673 | } 674 | 675 | // depending on what the key combination is 676 | // we will try to pick the best event for it 677 | action = _pickBestAction(key, modifiers, action); 678 | 679 | // make sure to initialize array if this is the first time 680 | // a callback is added for this key 681 | if (!_callbacks[key]) { 682 | _callbacks[key] = []; 683 | } 684 | 685 | // remove an existing match if there is one 686 | _getMatches(key, modifiers, {type: action}, !sequenceName, combination); 687 | 688 | // add this call back to the array 689 | // if it is a sequence put it at the beginning 690 | // if not put it at the end 691 | // 692 | // this is important because the way these are processed expects 693 | // the sequence ones to come first 694 | _callbacks[key][sequenceName ? 'unshift' : 'push']({ 695 | callback: callback, 696 | modifiers: modifiers, 697 | action: action, 698 | seq: sequenceName, 699 | level: level, 700 | combo: combination 701 | }); 702 | } 703 | 704 | /** 705 | * binds multiple combinations to the same callback 706 | * 707 | * @param {Array} combinations 708 | * @param {Function} callback 709 | * @param {string|undefined} action 710 | * @returns void 711 | */ 712 | function _bindMultiple(combinations, callback, action) { 713 | for (var i = 0; i < combinations.length; ++i) { 714 | _bindSingle(combinations[i], callback, action); 715 | } 716 | } 717 | 718 | // start! 719 | _addEvent(document, 'keypress', _handleKey); 720 | _addEvent(document, 'keydown', _handleKey); 721 | _addEvent(document, 'keyup', _handleKey); 722 | 723 | var Mousetrap = { 724 | 725 | /** 726 | * binds an event to mousetrap 727 | * 728 | * can be a single key, a combination of keys separated with +, 729 | * an array of keys, or a sequence of keys separated by spaces 730 | * 731 | * be sure to list the modifier keys first to make sure that the 732 | * correct key ends up getting bound (the last key in the pattern) 733 | * 734 | * @param {string|Array} keys 735 | * @param {Function} callback 736 | * @param {string=} action - 'keypress', 'keydown', or 'keyup' 737 | * @returns void 738 | */ 739 | bind: function(keys, callback, action) { 740 | keys = keys instanceof Array ? keys : [keys]; 741 | _bindMultiple(keys, callback, action); 742 | return this; 743 | }, 744 | 745 | /** 746 | * unbinds an event to mousetrap 747 | * 748 | * the unbinding sets the callback function of the specified key combo 749 | * to an empty function and deletes the corresponding key in the 750 | * _directMap dict. 751 | * 752 | * TODO: actually remove this from the _callbacks dictionary instead 753 | * of binding an empty function 754 | * 755 | * the keycombo+action has to be exactly the same as 756 | * it was defined in the bind method 757 | * 758 | * @param {string|Array} keys 759 | * @param {string} action 760 | * @returns void 761 | */ 762 | unbind: function(keys, action) { 763 | return Mousetrap.bind(keys, function() {}, action); 764 | }, 765 | 766 | /** 767 | * triggers an event that has already been bound 768 | * 769 | * @param {string} keys 770 | * @param {string=} action 771 | * @returns void 772 | */ 773 | trigger: function(keys, action) { 774 | if (_directMap[keys + ':' + action]) { 775 | _directMap[keys + ':' + action](); 776 | } 777 | return this; 778 | }, 779 | 780 | /** 781 | * resets the library back to its initial state. this is useful 782 | * if you want to clear out the current keyboard shortcuts and bind 783 | * new ones - for example if you switch to another page 784 | * 785 | * @returns void 786 | */ 787 | reset: function() { 788 | _callbacks = {}; 789 | _directMap = {}; 790 | return this; 791 | }, 792 | 793 | /** 794 | * should we stop this event before firing off callbacks 795 | * 796 | * @param {Event} e 797 | * @param {Element} element 798 | * @return {boolean} 799 | */ 800 | stopCallback: function(e, element, combo) { 801 | 802 | // if the element has the class "mousetrap" then no need to stop 803 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { 804 | return false; 805 | } 806 | 807 | // stop for input, select, and textarea 808 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true'); 809 | } 810 | }; 811 | 812 | // expose mousetrap to the global object 813 | window.Mousetrap = Mousetrap; 814 | 815 | // expose mousetrap as an AMD module 816 | if (typeof define === 'function' && define.amd) { 817 | define(Mousetrap); 818 | } 819 | }) (); 820 | --------------------------------------------------------------------------------