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");
46 * var snav = new SpatialNavigation(browser_element, optional_callback);
48 * optional_callback will be called when a new element is focused.
50 * function optional_callback(element) {}
55 var EXPORTED_SYMBOLS
= ["SpatialNavigation"];
57 function SpatialNavigation (browser
, callback
)
59 browser
.addEventListener("keypress", function (event
) { _onInputKeyPress(event
, callback
) }, true);
62 SpatialNavigation
.prototype = {
68 const Cc
= Components
.classes
;
69 const Ci
= Components
.interfaces
;
73 var console
= Cc
["@mozilla.org/consoleservice;1"].getService(Ci
.nsIConsoleService
);
74 console
.logStringMessage("*** SNAV: " + msg
);
77 var gDirectionalBias
= 10;
80 function _onInputKeyPress (event
, callback
) {
82 // If it isn't an arrow key, bail.
83 if (event
.keyCode
!= event
.DOM_VK_LEFT
&&
84 event
.keyCode
!= event
.DOM_VK_RIGHT
&&
85 event
.keyCode
!= event
.DOM_VK_UP
&&
86 event
.keyCode
!= event
.DOM_VK_DOWN
)
89 function snavfilter(node
) {
91 if (node
instanceof Ci
.nsIDOMHTMLLinkElement
||
92 node
instanceof Ci
.nsIDOMHTMLAnchorElement
) {
93 // if a anchor doesn't have a href, don't target it.
95 return Ci
.nsIDOMNodeFilter
.FILTER_SKIP
;
96 return Ci
.nsIDOMNodeFilter
.FILTER_ACCEPT
;
99 if (node
instanceof Ci
.nsIDOMHTMLInputElement
||
100 node
instanceof Ci
.nsIDOMHTMLSelectElement
||
101 node
instanceof Ci
.nsIDOMHTMLOptionElement
)
102 return Ci
.nsIDOMNodeFilter
.FILTER_ACCEPT
;
103 return Ci
.nsIDOMNodeFilter
.FILTER_SKIP
;
105 var bestElementToFocus
= null;
106 var distanceToBestElement
= Infinity
;
107 var focusedRect
= _inflateRect(event
.target
.getBoundingClientRect(),
109 var doc
= event
.target
.ownerDocument
;
111 var treeWalker
= doc
.createTreeWalker(doc
, Ci
.nsIDOMNodeFilter
.SHOW_ELEMENT
, snavfilter
, false);
114 while ((nextNode
= treeWalker
.nextNode())) {
116 if (nextNode
== event
.target
)
119 var nextRect
= _inflateRect(nextNode
.getBoundingClientRect(),
122 if (! _isRectInDirection(event
, focusedRect
, nextRect
))
125 var distance
= _spatialDistance(event
, focusedRect
, nextRect
);
127 if (distance
<= distanceToBestElement
&& distance
> 0) {
128 distanceToBestElement
= distance
;
129 bestElementToFocus
= nextNode
;
133 if (bestElementToFocus
!= null) {
134 // dump("focusing element " + bestElementToFocus.nodeName + " " + bestElementToFocus);
135 // Wishing we could do element.focus()
136 doc
.defaultView
.QueryInterface(Ci
.nsIInterfaceRequestor
).getInterface(Ci
.nsIDOMWindowUtils
).focus(bestElementToFocus
);
138 if (callback
!= undefined)
139 callback(bestElementToFocus
);
142 // couldn't find anything. just advance and hope.
143 // dump("advancing focus");
144 var windowMediator
= Cc
['@mozilla.org/appshell/window-mediator;1'].getService(Ci
.nsIWindowMediator
);
145 var window
= windowMediator
.getMostRecentWindow("navigator:browser");
146 window
.document
.commandDispatcher
.advanceFocus();
148 if (callback
!= undefined)
152 event
.preventDefault();
153 event
.stopPropagation();
156 function _isRectInDirection(event
, focusedRect
, anotherRect
)
158 if (event
.keyCode
== event
.DOM_VK_LEFT
) {
159 return (anotherRect
.left
< focusedRect
.left
);
162 if (event
.keyCode
== event
.DOM_VK_RIGHT
) {
163 return (anotherRect
.right
> focusedRect
.right
);
166 if (event
.keyCode
== event
.DOM_VK_UP
) {
167 return (anotherRect
.top
< focusedRect
.top
);
170 if (event
.keyCode
== event
.DOM_VK_DOWN
) {
171 return (anotherRect
.bottom
> focusedRect
.bottom
);
176 function _inflateRect(rect
, value
)
178 var newRect
= new Object();
180 newRect
.left
= rect
.left
- value
;
181 newRect
.top
= rect
.top
- value
;
182 newRect
.right
= rect
.right
+ value
;
183 newRect
.bottom
= rect
.bottom
+ value
;
187 function _containsRect(a
, b
)
189 return ( (b
.left
<= a
.right
) &&
190 (b
.right
>= a
.left
) &&
191 (b
.top
<= a
.bottom
) &&
192 (b
.bottom
>= a
.top
) );
195 function _spatialDistance(event
, a
, b
)
197 var inlineNavigation
= false;
200 if (event
.keyCode
== event
.DOM_VK_LEFT
) {
212 if (a
.top
> b
.bottom
) {
213 // the b rect is above a.
219 else if (a
.bottom
< b
.top
) {
220 // the b rect is below a.
232 } else if (event
.keyCode
== event
.DOM_VK_RIGHT
) {
244 if (a
.top
> b
.bottom
) {
245 // the b rect is above a.
251 else if (a
.bottom
< b
.top
) {
252 // the b rect is below a.
263 } else if (event
.keyCode
== event
.DOM_VK_UP
) {
272 if (a
.left
> b
.right
) {
273 // the b rect is to the left of a.
278 } else if (a
.right
< b
.left
) {
279 // the b rect is to the right of a
285 // both b and a share some common x's.
291 } else if (event
.keyCode
== event
.DOM_VK_DOWN
) {
300 if (a
.left
> b
.right
) {
301 // the b rect is to the left of a.
306 } else if (a
.right
< b
.left
) {
307 // the b rect is to the right of a
313 // both b and a share some common x's.
321 var scopedRect
= _inflateRect(a
, gRectFudge
);
323 if (event
.keyCode
== event
.DOM_VK_LEFT
||
324 event
.keyCode
== event
.DOM_VK_RIGHT
) {
326 scopedRect
.right
= Infinity
;
327 inlineNavigation
= _containsRect(scopedRect
, b
);
329 else if (event
.keyCode
== event
.DOM_VK_UP
||
330 event
.keyCode
== event
.DOM_VK_DOWN
) {
332 scopedRect
.bottom
= Infinity
;
333 inlineNavigation
= _containsRect(scopedRect
, b
);
336 var d
= Math
.pow((mx
-nx
), 2) + Math
.pow((my
-ny
), 2);
338 // prefer elements directly aligned with the focused element
339 if (inlineNavigation
)
340 d
/= gDirectionalBias
;