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();