source: Dev/trunk/src/client/dijit/focus.js @ 532

Last change on this file since 532 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

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