├── 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 | 
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 |
--------------------------------------------------------------------------------