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