├── .gitignore ├── src ├── 16x16.png ├── 32x32.png ├── 48x48.png ├── background.html ├── background.js ├── manifest.json ├── options.js ├── options.html └── crossfire-chrome.js ├── README └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | *.zip 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /src/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/crossfire-chrome/master/src/16x16.png -------------------------------------------------------------------------------- /src/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/crossfire-chrome/master/src/32x32.png -------------------------------------------------------------------------------- /src/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozlima/crossfire-chrome/master/src/48x48.png -------------------------------------------------------------------------------- /src/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | (function(document){ 2 | chrome.extension.onRequest.addListener( 3 | function(request, sender, sendResponse) { 4 | switch (request.name) { 5 | case "getPreferences": 6 | var value = localStorage["mode"]; 7 | if (!value) { value = "default"; }; 8 | sendResponse({mode: value}); 9 | break; 10 | } 11 | } 12 | ); 13 | })(document); 14 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "content_security_policy": "script-src 'self'; object-src 'self'", 4 | "content_scripts": [ 5 | { 6 | "js": [ "crossfire-chrome.js" ], 7 | "run_at": "document_end", 8 | "matches": [ "http://*/*", "https://*/*" ] 9 | } 10 | ], 11 | "version": "0.2.6", 12 | "name": "CrossFire for Google Chrome™", 13 | "options_page": "options.html", 14 | "background": { 15 | "script" : "background.js", 16 | "page" : "background.html" 17 | }, 18 | "description": "CrossFire (Opera Spatial Navigation) for Chrome.", 19 | "icons": { "16": "16x16.png", 20 | "32": "32x32.png", 21 | "48": "48x48.png", 22 | "128": "48x48.png" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | (function(document) { 2 | document.addEventListener('DOMContentLoaded', function(e) { 3 | document.getElementById("button").addEventListener('click', function(e) { 4 | e.preventDefault(); 5 | save_options(); 6 | }, false); 7 | 8 | restore_options(); 9 | }, false); 10 | 11 | 12 | function save_options() 13 | { 14 | var select = document.getElementById("mode"); 15 | localStorage["mode"] = select.value; 16 | var status = document.getElementById("status"); 17 | status.innerHTML = "Settings have been saved."; 18 | setTimeout(function(e) { 19 | status.innerHTML = ""; 20 | }, 1500); 21 | } 22 | 23 | function restore_options() 24 | { 25 | var mode = localStorage["mode"]; 26 | if (!mode) { return; } 27 | var select = document.getElementById("mode"); 28 | select.value = mode; 29 | } 30 | })(document); 31 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | CrossFire for Google Chrome™ allows you to move to the next link with a keyboard. 2 | This method is called "Spatial Navigation" in Opera and "CrossFire" in Firefox Addons. 3 | 4 | Usage: 5 | Shift + Up : navigate up 6 | Shift + Down : navigate down 7 | Shift + Left : navigate left 8 | Shift + Right : navigate right 9 | Enter: navigate the focused link 10 | 11 | History: 12 | 0.2.6: 13 | add ignoring if event.defaultPrevented == true (thanks github.com/ijprest!) 14 | 0.2.5: 15 | add ignoring Content editable documents (thanks github.com/takuya!) 16 | 0.2.4: 17 | update manifest version 18 | 0.2.3: 19 | highlight by extension 20 | rename extension name 21 | 0.2.2: 22 | option page to change keybindings 23 | 0.2.1: 24 | work on https://* 25 | show the icon in chrome:extensions page 26 | 27 | Authors: 28 | mallowlabs 29 | Volker Eichhorn 30 | 31 | Repository: 32 | http://github.com/mallowlabs/crossfire-chrome/ 33 | This extension is an open source software (The MIT License). 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 mallowlabs (mallowlabs@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChrossFire - Options 4 | 32 | 33 | 34 | 35 | 38 |
39 |

Preferences

40 |
41 |
42 | 43 | 47 | 48 | 49 |
50 |
51 |
52 |
53 |

Repository

54 |
55 | This extension is an open source software.
56 | CrossFire on github 57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /src/crossfire-chrome.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name crossfire-chrome.js 3 | // @namespace http://d.hatena.ne.jp/mallowlabs/ 4 | // ==/UserScript== 5 | 6 | (function(document) { 7 | 8 | var defaultBindings = { DOWN: 40, UP: 38, LEFT: 37, RIGHT: 39 } 9 | var viBindings = { DOWN: 74, UP: 75, LEFT: 72, RIGHT: 76 } 10 | var KEY = {}; 11 | 12 | chrome.extension.sendRequest({name: "getPreferences"}, 13 | function(response) { 14 | var binding = response.mode; 15 | if (!binding) { binding = "default"; } 16 | switch(binding) { 17 | case "vi": 18 | KEY = viBindings; 19 | break; 20 | default: 21 | KEY = defaultBindings; 22 | } 23 | KEY.MODIFIER = 16; 24 | 25 | var xlinks = []; 26 | var ylinks = []; 27 | var modifierPressed = false; 28 | var CROSSFIRE_CHROME_FOCUS = "crossfire-chrome-focus"; 29 | 30 | addStyle(".crossfire-chrome-focus:focus {outline: 2px solid #6baee6;}"); 31 | 32 | function addStyle(css) { 33 | var heads = document.getElementsByTagName("head"); 34 | if (heads.length == 0) { return; } 35 | var node = document.createElement("style"); 36 | node.type = "text/css"; 37 | node.appendChild(document.createTextNode(css)); 38 | heads[0].appendChild(node); 39 | } 40 | 41 | function collectRects() { 42 | xlinks = [] 43 | ylinks = [] 44 | var ns = document.getElementsByTagName("a") // TODO use XPath 45 | for (var i = 0,l = ns.length; i < l; i++) { 46 | if (!(ns[i].hasAttribute("href") && isVisible(ns[i]))) { 47 | continue; // link which has no href or is not visible should be ignore 48 | } 49 | var rect = ns[i].getBoundingClientRect(); 50 | xlinks.push({dom: ns[i], rect: rect}); 51 | ylinks.push({dom: ns[i], rect: rect}); 52 | } 53 | xlinks.sort(function(a,b) { return getCenter(a.rect).x - getCenter(b.rect).x }) 54 | ylinks.sort(function(a,b) { return getCenter(a.rect).y - getCenter(b.rect).y }) 55 | } 56 | 57 | function getCenter(rect) { 58 | return {x: rect.left + (rect.width) / 2, y: rect.top + (rect.height) / 2}; 59 | } 60 | function isVisible(node) { 61 | var tmp = node; 62 | while (tmp.tagName != "HTML") { 63 | var style = document.defaultView.getComputedStyle(tmp, ""); 64 | if (style.display == "none" || style.visibility == "hidden") { 65 | return false; 66 | } 67 | tmp = tmp.parentNode; 68 | } 69 | return true; 70 | } 71 | 72 | /* FIXME too complex ... */ 73 | function isTarget(activeRect, targetRect, axis, direction) { 74 | if (axis == "x") { 75 | if (direction == 1 && activeRect.right < targetRect.right) { // right 76 | if (targetRect.bottom >= activeRect.top && targetRect.top <= activeRect.bottom) { 77 | return (targetRect.left - activeRect.right); 78 | } else if ( (targetRect.bottom < activeRect.top) && // up 79 | (targetRect.left - activeRect.right) > (activeRect.top - targetRect.bottom)) { 80 | return (targetRect.left - activeRect.right) + (activeRect.top - targetRect.bottom); 81 | } else if ( (targetRect.top > activeRect.bottom) && // down 82 | (targetRect.left - activeRect.right) > (targetRect.top - activeRect.bottom)) { 83 | return (targetRect.left - activeRect.right) + (targetRect.top - activeRect.bottom); 84 | } 85 | } else if (direction == -1 && targetRect.left < activeRect.left) { // left 86 | if (targetRect.bottom >= activeRect.top && targetRect.top <= activeRect.bottom) { 87 | return (activeRect.left - targetRect.right); 88 | } else if ( (targetRect.bottom < activeRect.top) && // up 89 | (activeRect.left - targetRect.right) > (activeRect.top - targetRect.bottom)) { 90 | return (activeRect.left - targetRect.right) + (activeRect.top - targetRect.bottom); 91 | } else if ( (targetRect.top > activeRect.bottom ) && // down 92 | (activeRect.left - targetRect.right) > (targetRect.top - activeRect.bottom)) { 93 | return (activeRect.left - targetRect.right) +(targetRect.top - activeRect.bottom); 94 | } 95 | } 96 | } else if (axis == "y") { 97 | if (direction == 1 && activeRect.bottom < targetRect.bottom) { // down 98 | if (activeRect.left <= targetRect.right && targetRect.left <= activeRect.right) { 99 | return (targetRect.top - activeRect.bottom); 100 | } else if ( (targetRect.right < activeRect.left) && // left 101 | (activeRect.left - targetRect.right) < (targetRect.top - activeRect.bottom)) { 102 | return (targetRect.top - activeRect.bottom) + (activeRect.left - targetRect.right); 103 | } else if ( (targetRect.left > activeRect.right) && // right 104 | (targetRect.left - activeRect.right) < (targetRect.top - activeRect.bottom)) { 105 | return (targetRect.top - activeRect.bottom) + (targetRect.left - activeRect.right); 106 | } 107 | }else if (direction == -1 && targetRect.top < activeRect.top) { // up 108 | if (targetRect.right >= activeRect.left && targetRect.left <= activeRect.right) { 109 | return (activeRect.top - targetRect.bottom); 110 | } else if ( (targetRect.right < activeRect.left) && // left 111 | (activeRect.left - targetRect.right) < (activeRect.top - targetRect.bottom)) { 112 | return (activeRect.top - targetRect.bottom) + (activeRect.left - targetRect.right); 113 | } else if ( (targetRect.left > activeRect.right) && // right 114 | (targetRect.left - activeRect.right) < (activeRect.top - targetRect.bottom)) { 115 | return (activeRect.top - targetRect.bottom) + (targetRect.left - activeRect.right) ; 116 | } 117 | } 118 | } 119 | return -1; 120 | } 121 | 122 | function navigateNext(links, axis, direction) { 123 | var active = document.activeElement; 124 | var ignore = false; 125 | var activeRect = {left:-100, right:-100, top:-200, bottom:-100}; 126 | if (active.tagName == "A") { 127 | ignore = true; 128 | activeRect = active.getBoundingClientRect(); 129 | } 130 | var start = (direction == 1) ? 0 : links.length - 1; 131 | var minDistance = -1; 132 | var nearestNode = null; 133 | for (var i = start,l = links.length; 0 <= i && i < l; i += direction) { 134 | if (!ignore) { 135 | var distance = isTarget(activeRect, links[i].rect, axis, direction); 136 | if (distance < 0) continue; 137 | if (minDistance < 0 || distance < minDistance) { 138 | minDistance = distance; 139 | nearestNode = links[i].dom; 140 | } 141 | } 142 | if (links[i].dom == document.activeElement) { //XXX want to use 'active' but not works ... 143 | ignore = false; 144 | } 145 | } 146 | if (nearestNode) { 147 | focus(nearestNode); 148 | } 149 | } 150 | 151 | function focus(node) { 152 | node.classList.add(CROSSFIRE_CHROME_FOCUS); 153 | var listener = node.addEventListener('blur', function(e) { 154 | node.classList.remove(CROSSFIRE_CHROME_FOCUS); 155 | node.removeEventListener(listener); 156 | }, false); 157 | node.focus(); 158 | } 159 | 160 | function navigateRight() { 161 | navigateNext(xlinks, "x", 1); 162 | } 163 | function navigateLeft() { 164 | navigateNext(xlinks, "x", -1); 165 | } 166 | function navigateDown() { 167 | navigateNext(ylinks, "y", 1); 168 | } 169 | function navigateUp() { 170 | navigateNext(ylinks, "y", -1); 171 | } 172 | document.addEventListener('keyup', function(e) { 173 | if (document.activeElement.tagName == "INPUT" 174 | || document.activeElement.tagName == "TEXTAREA" 175 | || document.activeElement.contentEditable == "true" ) { 176 | return; // ignore 177 | } 178 | switch(e.keyCode) { 179 | case KEY.MODIFIER: 180 | modifierPressed = false; 181 | break; 182 | } 183 | }, false); 184 | document.addEventListener('keydown', function(e) { 185 | if (document.activeElement.tagName == "INPUT" 186 | || document.activeElement.tagName == "TEXTAREA" 187 | || document.activeElement.contentEditable == "true" 188 | || e.defaultPrevented ) { 189 | return; // ignore 190 | } 191 | switch(e.keyCode) { 192 | case KEY.MODIFIER: 193 | modifierPressed = true; 194 | collectRects(); 195 | break; 196 | case KEY.DOWN: 197 | if (e.shiftKey) { 198 | navigateDown(); 199 | e.preventDefault(); 200 | } 201 | break; 202 | case KEY.UP: 203 | if (e.shiftKey) { 204 | navigateUp(); 205 | e.preventDefault(); 206 | } 207 | break; 208 | case KEY.RIGHT: 209 | if (e.shiftKey) { 210 | navigateRight(); 211 | e.preventDefault(); 212 | } 213 | break; 214 | case KEY.LEFT: 215 | if (e.shiftKey) { 216 | navigateLeft(); 217 | e.preventDefault(); 218 | } 219 | break; 220 | default: 221 | break; 222 | } 223 | }, false); 224 | document.addEventListener('scroll', function(e) { 225 | if(modifierPressed) { 226 | collectRects(); 227 | } 228 | }, false); 229 | }); 230 | })(document); 231 | 232 | --------------------------------------------------------------------------------