1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
14 * The Original Code is Spatial Navigation.
16 * The Initial Developer of the Original Code is Mozilla Corporation
17 * Portions created by the Initial Developer are Copyright (C) 2008
18 * the Initial Developer. All Rights Reserved.
21 * Doug Turner <dougt@meer.net> (Original Author)
23 * Alternatively, the contents of this file may be used under the terms of
24 * either the GNU General Public License Version 2 or later (the "GPL"), or
25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 * in which case the provisions of the GPL or the LGPL are applicable instead
27 * of those above. If you wish to allow use of your version of this file only
28 * under the terms of either the GPL or the LGPL, and not to allow others to
29 * use your version of this file under the terms of the MPL, indicate your
30 * decision by deleting the provisions above and replace them with the notice
31 * and other provisions required by the GPL or the LGPL. If you do not delete
32 * the provisions above, a recipient may use your version of this file under
33 * the terms of any one of the MPL, the GPL or the LGPL.
35 * ***** END LICENSE BLOCK ***** */
39 * Import this module through
41 * Components.utils.import("resource://gre/modules/SpatialNavigation.js");
43 * Usage: (Literal class)
45 * SpatialNavigation(browser_element, optional_callback);
47 * optional_callback will be called when a new element is focused.
49 * function optional_callback(element) {}
54 var EXPORTED_SYMBOLS = ["SpatialNavigation"];
56 var SpatialNavigation = {
58 init: function(browser, callback) {
59 browser.addEventListener("keypress", function (event) { _onInputKeyPress(event, callback) }, true);
69 const Cc = Components.classes;
70 const Ci = Components.interfaces;
74 var console = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
75 console.logStringMessage("*** SNAV: " + msg);
78 var gDirectionalBias = 10;
83 const kShift = "shift";
87 function _onInputKeyPress (event, callback) {
89 // Use whatever key value is available (either keyCode or charCode).
90 // It might be useful for addons or whoever wants to set different
91 // key to be used here (e.g. "a", "F1", "arrowUp", ...).
92 var key = event.which || event.keyCode;
94 // If it isn't enabled, bail.
95 if (!PrefObserver['enabled'])
98 if (key != PrefObserver['keyCodeDown'] &&
99 key != PrefObserver['keyCodeRight'] &&
100 key != PrefObserver['keyCodeUp'] &&
101 key != PrefObserver['keyCodeLeft'])
104 // If it is not using the modifiers it should, bail.
105 if (!event.altKey && PrefObserver['modifierAlt'])
108 if (!event.shiftKey && PrefObserver['modifierShift'])
111 if (!event.crtlKey && PrefObserver['modifierCtrl'])
114 var target = event.target;
116 var doc = target.ownerDocument;
118 // If it is XUL content (e.g. about:config), bail.
119 if (!PrefObserver['xulContentEnabled'] && doc instanceof Ci.nsIDOMXULDocument)
122 // check to see if we are in a textarea or text input element, and if so,
123 // ensure that we let the arrow keys work properly.
124 if (target instanceof Ci.nsIDOMHTMLHtmlElement) {
125 _focusNextUsingCmdDispatcher(key, callback);
129 if ((target instanceof Ci.nsIDOMHTMLInputElement && (target.type == "text" || target.type == "password")) ||
130 target instanceof Ci.nsIDOMHTMLTextAreaElement ) {
132 // if there is any selection at all, just ignore
133 if (target.selectionEnd - target.selectionStart > 0)
136 // if there is no text, there is nothing special to do.
137 if (target.textLength > 0) {
138 if (key == PrefObserver['keyCodeRight'] ||
139 key == PrefObserver['keyCodeDown'] ) {
140 // we are moving forward into the document
141 if (target.textLength != target.selectionEnd)
146 // we are at the start of the text, okay to move
147 if (target.selectionStart != 0)
153 // Check to see if we are in a select
154 if (target instanceof Ci.nsIDOMHTMLSelectElement)
156 if (key == PrefObserver['keyCodeDown']) {
157 if (target.selectedIndex + 1 < target.length)
161 if (key == PrefObserver['keyCodeUp']) {
162 if (target.selectedIndex > 0)
167 function snavfilter(node) {
169 if (node instanceof Ci.nsIDOMHTMLLinkElement ||
170 node instanceof Ci.nsIDOMHTMLAnchorElement) {
171 // if a anchor doesn't have a href, don't target it.
173 return Ci.nsIDOMNodeFilter.FILTER_SKIP;
174 return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
177 if ((node instanceof Ci.nsIDOMHTMLButtonElement ||
178 node instanceof Ci.nsIDOMHTMLInputElement ||
179 node instanceof Ci.nsIDOMHTMLLinkElement ||
180 node instanceof Ci.nsIDOMHTMLOptGroupElement ||
181 node instanceof Ci.nsIDOMHTMLSelectElement ||
182 node instanceof Ci.nsIDOMHTMLTextAreaElement) &&
183 node.disabled == false)
184 return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
186 return Ci.nsIDOMNodeFilter.FILTER_SKIP;
189 var bestElementToFocus = null;
190 var distanceToBestElement = Infinity;
191 var focusedRect = _inflateRect(target.getBoundingClientRect(),
194 var treeWalker = doc.createTreeWalker(doc, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, snavfilter, false);
197 while ((nextNode = treeWalker.nextNode())) {
199 if (nextNode == target)
202 var nextRect = _inflateRect(nextNode.getBoundingClientRect(),
205 if (! _isRectInDirection(key, focusedRect, nextRect))
208 var distance = _spatialDistance(key, focusedRect, nextRect);
210 //dump("looking at: " + nextNode + " " + distance);
212 if (distance <= distanceToBestElement && distance > 0) {
213 distanceToBestElement = distance;
214 bestElementToFocus = nextNode;
218 if (bestElementToFocus != null) {
219 //dump("focusing element " + bestElementToFocus.nodeName + " " + bestElementToFocus) + "id=" + bestElementToFocus.getAttribute("id");
221 // Wishing we could do element.focus()
222 doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).focus(bestElementToFocus);
224 // if it is a text element, select all.
225 if((bestElementToFocus instanceof Ci.nsIDOMHTMLInputElement && (bestElementToFocus.type == "text" || bestElementToFocus.type == "password")) ||
226 bestElementToFocus instanceof Ci.nsIDOMHTMLTextAreaElement ) {
227 bestElementToFocus.selectionStart = 0;
228 bestElementToFocus.selectionEnd = bestElementToFocus.textLength;
231 if (callback != undefined)
232 callback(bestElementToFocus);
235 // couldn't find anything. just advance and hope.
236 _focusNextUsingCmdDispatcher(key, callback);
239 event.preventDefault();
240 event.stopPropagation();
243 function _focusNextUsingCmdDispatcher(key, callback) {
245 var windowMediator = Cc['@mozilla.org/appshell/window-mediator;1'].getService(Ci.nsIWindowMediator);
246 var window = windowMediator.getMostRecentWindow("navigator:browser");
248 if (key == PrefObserver['keyCodeRight'] || key == PrefObserver['keyCodeDown']) {
249 window.document.commandDispatcher.advanceFocus();
251 window.document.commandDispatcher.rewindFocus();
254 if (callback != undefined)
258 function _isRectInDirection(key, focusedRect, anotherRect)
260 if (key == PrefObserver['keyCodeLeft']) {
261 return (anotherRect.left < focusedRect.left);
264 if (key == PrefObserver['keyCodeRight']) {
265 return (anotherRect.right > focusedRect.right);
268 if (key == PrefObserver['keyCodeUp']) {
269 return (anotherRect.top < focusedRect.top);
272 if (key == PrefObserver['keyCodeDown']) {
273 return (anotherRect.bottom > focusedRect.bottom);
278 function _inflateRect(rect, value)
280 var newRect = new Object();
282 newRect.left = rect.left - value;
283 newRect.top = rect.top - value;
284 newRect.right = rect.right + value;
285 newRect.bottom = rect.bottom + value;
289 function _containsRect(a, b)
291 return ( (b.left <= a.right) &&
292 (b.right >= a.left) &&
293 (b.top <= a.bottom) &&
294 (b.bottom >= a.top) );
297 function _spatialDistance(key, a, b)
299 var inlineNavigation = false;
302 if (key == PrefObserver['keyCodeLeft']) {
314 if (a.top > b.bottom) {
315 // the b rect is above a.
321 else if (a.bottom < b.top) {
322 // the b rect is below a.
334 } else if (key == PrefObserver['keyCodeRight']) {
346 if (a.top > b.bottom) {
347 // the b rect is above a.
353 else if (a.bottom < b.top) {
354 // the b rect is below a.
365 } else if (key == PrefObserver['keyCodeUp']) {
374 if (a.left > b.right) {
375 // the b rect is to the left of a.
380 } else if (a.right < b.left) {
381 // the b rect is to the right of a
387 // both b and a share some common x's.
393 } else if (key == PrefObserver['keyCodeDown']) {
402 if (a.left > b.right) {
403 // the b rect is to the left of a.
408 } else if (a.right < b.left) {
409 // the b rect is to the right of a
415 // both b and a share some common x's.
423 var scopedRect = _inflateRect(a, gRectFudge);
425 if (key == PrefObserver['keyCodeLeft'] ||
426 key == PrefObserver['keyCodeRight']) {
428 scopedRect.right = Infinity;
429 inlineNavigation = _containsRect(scopedRect, b);
431 else if (key == PrefObserver['keyCodeUp'] ||
432 key == PrefObserver['keyCodeDown']) {
434 scopedRect.bottom = Infinity;
435 inlineNavigation = _containsRect(scopedRect, b);
438 var d = Math.pow((mx-nx), 2) + Math.pow((my-ny), 2);
440 // prefer elements directly aligned with the focused element
441 if (inlineNavigation)
442 d /= gDirectionalBias;
447 // Snav preference observer
453 this.prefService = Cc["@mozilla.org/preferences-service;1"]
454 .getService(Ci.nsIPrefService);
456 this._branch = this.prefService.getBranch("snav.");
457 this._branch.QueryInterface(Ci.nsIPrefBranch2);
458 this._branch.addObserver("", this, false);
460 // set current or default pref values
461 this.observe(null, "nsPref:changed", "enabled");
462 this.observe(null, "nsPref:changed", "xulContentEnabled");
463 this.observe(null, "nsPref:changed", "keyCode.modifier");
464 this.observe(null, "nsPref:changed", "keyCode.right");
465 this.observe(null, "nsPref:changed", "keyCode.up");
466 this.observe(null, "nsPref:changed", "keyCode.down");
467 this.observe(null, "nsPref:changed", "keyCode.left");
470 observe: function(aSubject, aTopic, aData)
472 if(aTopic != "nsPref:changed")
475 // aSubject is the nsIPrefBranch we're observing (after appropriate QI)
476 // aData is the name of the pref that's been changed (relative to aSubject)
480 this.enabled = this._branch.getBoolPref("enabled");
482 this.enabled = false;
485 case "xulContentEnabled":
487 this.xulContentEnabled = this._branch.getBoolPref("xulContentEnabled");
489 this.xulContentEnabled = false;
493 case "keyCode.modifier":
495 this.keyCodeModifier = this._branch.getCharPref("keyCode.modifier");
497 // resetting modifiers
498 this.modifierAlt = false;
499 this.modifierShift = false;
500 this.modifierCtrl = false;
502 if (this.keyCodeModifier != this.kNone)
504 // use are using '+' as a separator in about:config.
505 var mods = this.keyCodeModifier.split(/\++/);
506 for (var i = 0; i < mods.length; i++) {
507 var mod = mods[i].toLowerCase();
510 else if (mod == kAlt)
511 this.modifierAlt = true;
512 else if (mod == kShift)
513 this.modifierShift = true;
514 else if (mod == kCtrl)
515 this.modifierCtrl = true;
517 this.keyCodeModifier = kNone;
523 this.keyCodeModifier = kNone;
528 this.keyCodeUp = this._branch.getIntPref("keyCode.up");
530 this.keyCodeUp = Ci.nsIDOMKeyEvent.DOM_VK_UP;
535 this.keyCodeDown = this._branch.getIntPref("keyCode.down");
537 this.keyCodeDown = Ci.nsIDOMKeyEvent.DOM_VK_DOWN;
542 this.keyCodeLeft = this._branch.getIntPref("keyCode.left");
544 this.keyCodeLeft = Ci.nsIDOMKeyEvent.DOM_VK_LEFT;
547 case "keyCode.right":
549 this.keyCodeRight = this._branch.getIntPref("keyCode.right");
551 this.keyCodeRight = Ci.nsIDOMKeyEvent.DOM_VK_RIGHT;
558 PrefObserver.register();