]> git.wh0rd.org - tt-rss.git/blame - lib/dijit/focus.js.uncompressed.js
update dojo to 1.7.3
[tt-rss.git] / lib / dijit / focus.js.uncompressed.js
CommitLineData
1354d172
AD
1define("dijit/focus", [
2 "dojo/aspect",
3 "dojo/_base/declare", // declare
4 "dojo/dom", // domAttr.get dom.isDescendant
5 "dojo/dom-attr", // domAttr.get dom.isDescendant
6 "dojo/dom-construct", // connect to domConstruct.empty, domConstruct.destroy
7 "dojo/Evented",
8 "dojo/_base/lang", // lang.hitch
9 "dojo/on",
10 "dojo/ready",
11 "dojo/_base/sniff", // has("ie")
12 "dojo/Stateful",
13 "dojo/_base/unload", // unload.addOnWindowUnload
14 "dojo/_base/window", // win.body
15 "dojo/window", // winUtils.get
16 "./a11y", // a11y.isTabNavigable
17 "./registry", // registry.byId
18 "." // to set dijit.focus
19], function(aspect, declare, dom, domAttr, domConstruct, Evented, lang, on, ready, has, Stateful, unload, win, winUtils,
20 a11y, registry, dijit){
21
22 // module:
23 // dijit/focus
24 // summary:
25 // Returns a singleton that tracks the currently focused node, and which widgets are currently "active".
26
27/*=====
28 dijit.focus = {
29 // summary:
30 // Tracks the currently focused node, and which widgets are currently "active".
31 // Access via require(["dijit/focus"], function(focus){ ... }).
32 //
33 // A widget is considered active if it or a descendant widget has focus,
34 // or if a non-focusable node of this widget or a descendant was recently clicked.
35 //
36 // Call focus.watch("curNode", callback) to track the current focused DOMNode,
37 // or focus.watch("activeStack", callback) to track the currently focused stack of widgets.
38 //
39 // Call focus.on("widget-blur", func) or focus.on("widget-focus", ...) to monitor when
40 // when widgets become active/inactive
41 //
42 // Finally, focus(node) will focus a node, suppressing errors if the node doesn't exist.
43
44 // curNode: DomNode
45 // Currently focused item on screen
46 curNode: null,
47
48 // activeStack: dijit._Widget[]
49 // List of currently active widgets (focused widget and it's ancestors)
50 activeStack: [],
51
52 registerIframe: function(iframe){
53 // summary:
54 // Registers listeners on the specified iframe so that any click
55 // or focus event on that iframe (or anything in it) is reported
56 // as a focus/click event on the <iframe> itself.
57 // description:
58 // Currently only used by editor.
59 // returns:
60 // Handle with remove() method to deregister.
61 },
62
63 registerWin: function(targetWindow, effectiveNode){
64 // summary:
65 // Registers listeners on the specified window (either the main
66 // window or an iframe's window) to detect when the user has clicked somewhere
67 // or focused somewhere.
68 // description:
69 // Users should call registerIframe() instead of this method.
70 // targetWindow: Window?
71 // If specified this is the window associated with the iframe,
72 // i.e. iframe.contentWindow.
73 // effectiveNode: DOMNode?
74 // If specified, report any focus events inside targetWindow as
75 // an event on effectiveNode, rather than on evt.target.
76 // returns:
77 // Handle with remove() method to deregister.
78 }
79 };
80=====*/
81
82 var FocusManager = declare([Stateful, Evented], {
83 // curNode: DomNode
84 // Currently focused item on screen
85 curNode: null,
86
87 // activeStack: dijit._Widget[]
88 // List of currently active widgets (focused widget and it's ancestors)
89 activeStack: [],
90
91 constructor: function(){
92 // Don't leave curNode/prevNode pointing to bogus elements
93 var check = lang.hitch(this, function(node){
94 if(dom.isDescendant(this.curNode, node)){
95 this.set("curNode", null);
96 }
97 if(dom.isDescendant(this.prevNode, node)){
98 this.set("prevNode", null);
99 }
100 });
101 aspect.before(domConstruct, "empty", check);
102 aspect.before(domConstruct, "destroy", check);
103 },
104
105 registerIframe: function(/*DomNode*/ iframe){
106 // summary:
107 // Registers listeners on the specified iframe so that any click
108 // or focus event on that iframe (or anything in it) is reported
109 // as a focus/click event on the <iframe> itself.
110 // description:
111 // Currently only used by editor.
112 // returns:
113 // Handle with remove() method to deregister.
114 return this.registerWin(iframe.contentWindow, iframe);
115 },
116
117 registerWin: function(/*Window?*/targetWindow, /*DomNode?*/ effectiveNode){
118 // summary:
119 // Registers listeners on the specified window (either the main
120 // window or an iframe's window) to detect when the user has clicked somewhere
121 // or focused somewhere.
122 // description:
123 // Users should call registerIframe() instead of this method.
124 // targetWindow:
125 // If specified this is the window associated with the iframe,
126 // i.e. iframe.contentWindow.
127 // effectiveNode:
128 // If specified, report any focus events inside targetWindow as
129 // an event on effectiveNode, rather than on evt.target.
130 // returns:
131 // Handle with remove() method to deregister.
132
133 // TODO: make this function private in 2.0; Editor/users should call registerIframe(),
134
135 var _this = this;
136 var mousedownListener = function(evt){
137 _this._justMouseDowned = true;
138 setTimeout(function(){ _this._justMouseDowned = false; }, 0);
139
140 // workaround weird IE bug where the click is on an orphaned node
141 // (first time clicking a Select/DropDownButton inside a TooltipDialog)
142 if(has("ie") && evt && evt.srcElement && evt.srcElement.parentNode == null){
143 return;
144 }
145
146 _this._onTouchNode(effectiveNode || evt.target || evt.srcElement, "mouse");
147 };
148
149 // Listen for blur and focus events on targetWindow's document.
150 // IIRC, I'm using attachEvent() rather than dojo.connect() because focus/blur events don't bubble
151 // through dojo.connect(), and also maybe to catch the focus events early, before onfocus handlers
152 // fire.
153 // Connect to <html> (rather than document) on IE to avoid memory leaks, but document on other browsers because
154 // (at least for FF) the focus event doesn't fire on <html> or <body>.
155 var doc = has("ie") ? targetWindow.document.documentElement : targetWindow.document;
156 if(doc){
157 if(has("ie")){
158 targetWindow.document.body.attachEvent('onmousedown', mousedownListener);
159 var activateListener = function(evt){
160 // IE reports that nodes like <body> have gotten focus, even though they have tabIndex=-1,
161 // ignore those events
162 var tag = evt.srcElement.tagName.toLowerCase();
163 if(tag == "#document" || tag == "body"){ return; }
164
165 // Previous code called _onTouchNode() for any activate event on a non-focusable node. Can
166 // probably just ignore such an event as it will be handled by onmousedown handler above, but
167 // leaving the code for now.
168 if(a11y.isTabNavigable(evt.srcElement)){
169 _this._onFocusNode(effectiveNode || evt.srcElement);
170 }else{
171 _this._onTouchNode(effectiveNode || evt.srcElement);
172 }
173 };
174 doc.attachEvent('onactivate', activateListener);
175 var deactivateListener = function(evt){
176 _this._onBlurNode(effectiveNode || evt.srcElement);
177 };
178 doc.attachEvent('ondeactivate', deactivateListener);
179
180 return {
181 remove: function(){
182 targetWindow.document.detachEvent('onmousedown', mousedownListener);
183 doc.detachEvent('onactivate', activateListener);
184 doc.detachEvent('ondeactivate', deactivateListener);
185 doc = null; // prevent memory leak (apparent circular reference via closure)
186 }
187 };
188 }else{
189 doc.body.addEventListener('mousedown', mousedownListener, true);
190 doc.body.addEventListener('touchstart', mousedownListener, true);
191 var focusListener = function(evt){
192 _this._onFocusNode(effectiveNode || evt.target);
193 };
194 doc.addEventListener('focus', focusListener, true);
195 var blurListener = function(evt){
196 _this._onBlurNode(effectiveNode || evt.target);
197 };
198 doc.addEventListener('blur', blurListener, true);
199
200 return {
201 remove: function(){
202 doc.body.removeEventListener('mousedown', mousedownListener, true);
203 doc.body.removeEventListener('touchstart', mousedownListener, true);
204 doc.removeEventListener('focus', focusListener, true);
205 doc.removeEventListener('blur', blurListener, true);
206 doc = null; // prevent memory leak (apparent circular reference via closure)
207 }
208 };
209 }
210 }
211 },
212
213 _onBlurNode: function(/*DomNode*/ /*===== node =====*/){
214 // summary:
215 // Called when focus leaves a node.
216 // Usually ignored, _unless_ it *isn't* followed by touching another node,
217 // which indicates that we tabbed off the last field on the page,
218 // in which case every widget is marked inactive
219 this.set("prevNode", this.curNode);
220 this.set("curNode", null);
221
222 if(this._justMouseDowned){
223 // the mouse down caused a new widget to be marked as active; this blur event
224 // is coming late, so ignore it.
225 return;
226 }
227
228 // if the blur event isn't followed by a focus event then mark all widgets as inactive.
229 if(this._clearActiveWidgetsTimer){
230 clearTimeout(this._clearActiveWidgetsTimer);
231 }
232 this._clearActiveWidgetsTimer = setTimeout(lang.hitch(this, function(){
233 delete this._clearActiveWidgetsTimer;
234 this._setStack([]);
235 this.prevNode = null;
236 }), 100);
237 },
238
239 _onTouchNode: function(/*DomNode*/ node, /*String*/ by){
240 // summary:
241 // Callback when node is focused or mouse-downed
242 // node:
243 // The node that was touched.
244 // by:
245 // "mouse" if the focus/touch was caused by a mouse down event
246
247 // ignore the recent blurNode event
248 if(this._clearActiveWidgetsTimer){
249 clearTimeout(this._clearActiveWidgetsTimer);
250 delete this._clearActiveWidgetsTimer;
251 }
252
253 // compute stack of active widgets (ex: ComboButton --> Menu --> MenuItem)
254 var newStack=[];
255 try{
256 while(node){
257 var popupParent = domAttr.get(node, "dijitPopupParent");
258 if(popupParent){
259 node=registry.byId(popupParent).domNode;
260 }else if(node.tagName && node.tagName.toLowerCase() == "body"){
261 // is this the root of the document or just the root of an iframe?
262 if(node === win.body()){
263 // node is the root of the main document
264 break;
265 }
266 // otherwise, find the iframe this node refers to (can't access it via parentNode,
267 // need to do this trick instead). window.frameElement is supported in IE/FF/Webkit
268 node=winUtils.get(node.ownerDocument).frameElement;
269 }else{
270 // if this node is the root node of a widget, then add widget id to stack,
271 // except ignore clicks on disabled widgets (actually focusing a disabled widget still works,
272 // to support MenuItem)
273 var id = node.getAttribute && node.getAttribute("widgetId"),
274 widget = id && registry.byId(id);
275 if(widget && !(by == "mouse" && widget.get("disabled"))){
276 newStack.unshift(id);
277 }
278 node=node.parentNode;
279 }
280 }
281 }catch(e){ /* squelch */ }
282
283 this._setStack(newStack, by);
284 },
285
286 _onFocusNode: function(/*DomNode*/ node){
287 // summary:
288 // Callback when node is focused
289
290 if(!node){
291 return;
292 }
293
294 if(node.nodeType == 9){
295 // Ignore focus events on the document itself. This is here so that
296 // (for example) clicking the up/down arrows of a spinner
297 // (which don't get focus) won't cause that widget to blur. (FF issue)
298 return;
299 }
300
301 this._onTouchNode(node);
302
303 if(node == this.curNode){ return; }
304 this.set("curNode", node);
305 },
306
307 _setStack: function(/*String[]*/ newStack, /*String*/ by){
308 // summary:
309 // The stack of active widgets has changed. Send out appropriate events and records new stack.
310 // newStack:
311 // array of widget id's, starting from the top (outermost) widget
312 // by:
313 // "mouse" if the focus/touch was caused by a mouse down event
314
315 var oldStack = this.activeStack;
316 this.set("activeStack", newStack);
317
318 // compare old stack to new stack to see how many elements they have in common
319 for(var nCommon=0; nCommon<Math.min(oldStack.length, newStack.length); nCommon++){
320 if(oldStack[nCommon] != newStack[nCommon]){
321 break;
322 }
323 }
324
325 var widget;
326 // for all elements that have gone out of focus, set focused=false
327 for(var i=oldStack.length-1; i>=nCommon; i--){
328 widget = registry.byId(oldStack[i]);
329 if(widget){
330 widget._hasBeenBlurred = true; // TODO: used by form widgets, should be moved there
331 widget.set("focused", false);
332 if(widget._focusManager == this){
333 widget._onBlur(by);
334 }
335 this.emit("widget-blur", widget, by);
336 }
337 }
338
339 // for all element that have come into focus, set focused=true
340 for(i=nCommon; i<newStack.length; i++){
341 widget = registry.byId(newStack[i]);
342 if(widget){
343 widget.set("focused", true);
344 if(widget._focusManager == this){
345 widget._onFocus(by);
346 }
347 this.emit("widget-focus", widget, by);
348 }
349 }
350 },
351
352 focus: function(node){
353 // summary:
354 // Focus the specified node, suppressing errors if they occur
355 if(node){
356 try{ node.focus(); }catch(e){/*quiet*/}
357 }
358 }
359 });
360
361 var singleton = new FocusManager();
362
363 // register top window and all the iframes it contains
364 ready(function(){
365 var handle = singleton.registerWin(win.doc.parentWindow || win.doc.defaultView);
366 if(has("ie")){
367 unload.addOnWindowUnload(function(){
368 handle.remove();
369 handle = null;
370 })
371 }
372 });
373
374 // Setup dijit.focus as a pointer to the singleton but also (for backwards compatibility)
375 // as a function to set focus.
376 dijit.focus = function(node){
377 singleton.focus(node); // indirection here allows dijit/_base/focus.js to override behavior
378 };
379 for(var attr in singleton){
380 if(!/^_/.test(attr)){
381 dijit.focus[attr] = typeof singleton[attr] == "function" ? lang.hitch(singleton, attr) : singleton[attr];
382 }
383 }
384 singleton.watch(function(attr, oldVal, newVal){
385 dijit.focus[attr] = newVal;
386 });
387
388 return singleton;
389});