define([ "dojo/aspect", "dojo/_base/declare", // declare "dojo/dom", // domAttr.get dom.isDescendant "dojo/dom-attr", // domAttr.get dom.isDescendant "dojo/dom-construct", // connect to domConstruct.empty, domConstruct.destroy "dojo/Evented", "dojo/_base/lang", // lang.hitch "dojo/on", "dojo/ready", "dojo/sniff", // has("ie") "dojo/Stateful", "dojo/_base/unload", // unload.addOnWindowUnload "dojo/_base/window", // win.body "dojo/window", // winUtils.get "./a11y", // a11y.isTabNavigable "./registry", // registry.byId "./main" // to set dijit.focus ], function(aspect, declare, dom, domAttr, domConstruct, Evented, lang, on, ready, has, Stateful, unload, win, winUtils, a11y, registry, dijit){ // module: // dijit/focus var FocusManager = declare([Stateful, Evented], { // summary: // Tracks the currently focused node, and which widgets are currently "active". // Access via require(["dijit/focus"], function(focus){ ... }). // // A widget is considered active if it or a descendant widget has focus, // or if a non-focusable node of this widget or a descendant was recently clicked. // // Call focus.watch("curNode", callback) to track the current focused DOMNode, // or focus.watch("activeStack", callback) to track the currently focused stack of widgets. // // Call focus.on("widget-blur", func) or focus.on("widget-focus", ...) to monitor when // when widgets become active/inactive // // Finally, focus(node) will focus a node, suppressing errors if the node doesn't exist. // curNode: DomNode // Currently focused item on screen curNode: null, // activeStack: dijit/_WidgetBase[] // List of currently active widgets (focused widget and it's ancestors) activeStack: [], constructor: function(){ // Don't leave curNode/prevNode pointing to bogus elements var check = lang.hitch(this, function(node){ if(dom.isDescendant(this.curNode, node)){ this.set("curNode", null); } if(dom.isDescendant(this.prevNode, node)){ this.set("prevNode", null); } }); aspect.before(domConstruct, "empty", check); aspect.before(domConstruct, "destroy", check); }, registerIframe: function(/*DomNode*/ iframe){ // summary: // Registers listeners on the specified iframe so that any click // or focus event on that iframe (or anything in it) is reported // as a focus/click event on the `<iframe>` itself. // description: // Currently only used by editor. // returns: // Handle with remove() method to deregister. return this.registerWin(iframe.contentWindow, iframe); }, registerWin: function(/*Window?*/targetWindow, /*DomNode?*/ effectiveNode){ // summary: // Registers listeners on the specified window (either the main // window or an iframe's window) to detect when the user has clicked somewhere // or focused somewhere. // description: // Users should call registerIframe() instead of this method. // targetWindow: // If specified this is the window associated with the iframe, // i.e. iframe.contentWindow. // effectiveNode: // If specified, report any focus events inside targetWindow as // an event on effectiveNode, rather than on evt.target. // returns: // Handle with remove() method to deregister. // TODO: make this function private in 2.0; Editor/users should call registerIframe(), var _this = this; var mousedownListener = function(evt){ _this._justMouseDowned = true; setTimeout(function(){ _this._justMouseDowned = false; }, 0); // workaround weird IE bug where the click is on an orphaned node // (first time clicking a Select/DropDownButton inside a TooltipDialog) if(has("ie") && evt && evt.srcElement && evt.srcElement.parentNode == null){ return; } _this._onTouchNode(effectiveNode || evt.target || evt.srcElement, "mouse"); }; // Listen for blur and focus events on targetWindow's document. // Using attachEvent()/addEventListener() rather than on() to try to catch mouseDown events even // if other code calls evt.stopPropagation(). But rethink for 2.0 since that doesn't work for attachEvent(), // which watches events at the bubbling phase rather than capturing phase, like addEventListener(..., false). // Connect to <html> (rather than document) on IE to avoid memory leaks, but document on other browsers because // (at least for FF) the focus event doesn't fire on <html> or <body>. var doc = has("ie") ? targetWindow.document.documentElement : targetWindow.document; if(doc){ if(has("ie")){ targetWindow.document.body.attachEvent('onmousedown', mousedownListener); var focusinListener = function(evt){ // IE reports that nodes like <body> have gotten focus, even though they have tabIndex=-1, // ignore those events var tag = evt.srcElement.tagName.toLowerCase(); if(tag == "#document" || tag == "body"){ return; } // Previous code called _onTouchNode() for any activate event on a non-focusable node. Can // probably just ignore such an event as it will be handled by onmousedown handler above, but // leaving the code for now. if(a11y.isTabNavigable(evt.srcElement)){ _this._onFocusNode(effectiveNode || evt.srcElement); }else{ _this._onTouchNode(effectiveNode || evt.srcElement); } }; doc.attachEvent('onfocusin', focusinListener); var focusoutListener = function(evt){ _this._onBlurNode(effectiveNode || evt.srcElement); }; doc.attachEvent('onfocusout', focusoutListener); return { remove: function(){ targetWindow.document.detachEvent('onmousedown', mousedownListener); doc.detachEvent('onfocusin', focusinListener); doc.detachEvent('onfocusout', focusoutListener); doc = null; // prevent memory leak (apparent circular reference via closure) } }; }else{ doc.body.addEventListener('mousedown', mousedownListener, true); doc.body.addEventListener('touchstart', mousedownListener, true); var focusListener = function(evt){ _this._onFocusNode(effectiveNode || evt.target); }; doc.addEventListener('focus', focusListener, true); var blurListener = function(evt){ _this._onBlurNode(effectiveNode || evt.target); }; doc.addEventListener('blur', blurListener, true); return { remove: function(){ doc.body.removeEventListener('mousedown', mousedownListener, true); doc.body.removeEventListener('touchstart', mousedownListener, true); doc.removeEventListener('focus', focusListener, true); doc.removeEventListener('blur', blurListener, true); doc = null; // prevent memory leak (apparent circular reference via closure) } }; } } }, _onBlurNode: function(/*DomNode*/ node){ // summary: // Called when focus leaves a node. // Usually ignored, _unless_ it *isn't* followed by touching another node, // which indicates that we tabbed off the last field on the page, // in which case every widget is marked inactive // If the blur event isn't followed by a focus event, it means the user clicked on something unfocusable, // so clear focus. if(this._clearFocusTimer){ clearTimeout(this._clearFocusTimer); } this._clearFocusTimer = setTimeout(lang.hitch(this, function(){ this.set("prevNode", this.curNode); this.set("curNode", null); }), 0); if(this._justMouseDowned){ // the mouse down caused a new widget to be marked as active; this blur event // is coming late, so ignore it. return; } // If the blur event isn't followed by a focus or touch event then mark all widgets as inactive. if(this._clearActiveWidgetsTimer){ clearTimeout(this._clearActiveWidgetsTimer); } this._clearActiveWidgetsTimer = setTimeout(lang.hitch(this, function(){ delete this._clearActiveWidgetsTimer; this._setStack([]); }), 0); }, _onTouchNode: function(/*DomNode*/ node, /*String*/ by){ // summary: // Callback when node is focused or mouse-downed // node: // The node that was touched. // by: // "mouse" if the focus/touch was caused by a mouse down event // ignore the recent blurNode event if(this._clearActiveWidgetsTimer){ clearTimeout(this._clearActiveWidgetsTimer); delete this._clearActiveWidgetsTimer; } // compute stack of active widgets (ex: ComboButton --> Menu --> MenuItem) var newStack=[]; try{ while(node){ var popupParent = domAttr.get(node, "dijitPopupParent"); if(popupParent){ node=registry.byId(popupParent).domNode; }else if(node.tagName && node.tagName.toLowerCase() == "body"){ // is this the root of the document or just the root of an iframe? if(node === win.body()){ // node is the root of the main document break; } // otherwise, find the iframe this node refers to (can't access it via parentNode, // need to do this trick instead). window.frameElement is supported in IE/FF/Webkit node=winUtils.get(node.ownerDocument).frameElement; }else{ // if this node is the root node of a widget, then add widget id to stack, // except ignore clicks on disabled widgets (actually focusing a disabled widget still works, // to support MenuItem) var id = node.getAttribute && node.getAttribute("widgetId"), widget = id && registry.byId(id); if(widget && !(by == "mouse" && widget.get("disabled"))){ newStack.unshift(id); } node=node.parentNode; } } }catch(e){ /* squelch */ } this._setStack(newStack, by); }, _onFocusNode: function(/*DomNode*/ node){ // summary: // Callback when node is focused if(!node){ return; } if(node.nodeType == 9){ // Ignore focus events on the document itself. This is here so that // (for example) clicking the up/down arrows of a spinner // (which don't get focus) won't cause that widget to blur. (FF issue) return; } // There was probably a blur event right before this event, but since we have a new focus, don't // do anything with the blur if(this._clearFocusTimer){ clearTimeout(this._clearFocusTimer); delete this._clearFocusTimer; } this._onTouchNode(node); if(node == this.curNode){ return; } this.set("prevNode", this.curNode); this.set("curNode", node); }, _setStack: function(/*String[]*/ newStack, /*String*/ by){ // summary: // The stack of active widgets has changed. Send out appropriate events and records new stack. // newStack: // array of widget id's, starting from the top (outermost) widget // by: // "mouse" if the focus/touch was caused by a mouse down event var oldStack = this.activeStack; this.set("activeStack", newStack); // compare old stack to new stack to see how many elements they have in common for(var nCommon=0; nCommon<Math.min(oldStack.length, newStack.length); nCommon++){ if(oldStack[nCommon] != newStack[nCommon]){ break; } } var widget; // for all elements that have gone out of focus, set focused=false for(var i=oldStack.length-1; i>=nCommon; i--){ widget = registry.byId(oldStack[i]); if(widget){ widget._hasBeenBlurred = true; // TODO: used by form widgets, should be moved there widget.set("focused", false); if(widget._focusManager == this){ widget._onBlur(by); } this.emit("widget-blur", widget, by); } } // for all element that have come into focus, set focused=true for(i=nCommon; i<newStack.length; i++){ widget = registry.byId(newStack[i]); if(widget){ widget.set("focused", true); if(widget._focusManager == this){ widget._onFocus(by); } this.emit("widget-focus", widget, by); } } }, focus: function(node){ // summary: // Focus the specified node, suppressing errors if they occur if(node){ try{ node.focus(); }catch(e){/*quiet*/} } } }); var singleton = new FocusManager(); // register top window and all the iframes it contains ready(function(){ var handle = singleton.registerWin(winUtils.get(win.doc)); if(has("ie")){ unload.addOnWindowUnload(function(){ if(handle){ // because this gets called twice when doh.robot is running handle.remove(); handle = null; } }); } }); // Setup dijit.focus as a pointer to the singleton but also (for backwards compatibility) // as a function to set focus. Remove for 2.0. dijit.focus = function(node){ singleton.focus(node); // indirection here allows dijit/_base/focus.js to override behavior }; for(var attr in singleton){ if(!/^_/.test(attr)){ dijit.focus[attr] = typeof singleton[attr] == "function" ? lang.hitch(singleton, attr) : singleton[attr]; } } singleton.watch(function(attr, oldVal, newVal){ dijit.focus[attr] = newVal; }); return singleton; });