source: Dev/branches/rest-dojo-ui/client/dijit/focus.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 13.4 KB
Line 
1define([
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});
Note: See TracBrowser for help on using the repository browser.