/* Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. Available via Academic Free License >= 2.1 OR the modified BSD license. see: http://dojotoolkit.org/license for details */ if(!dojo._hasResource["dijit._base.focus"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. dojo._hasResource["dijit._base.focus"] = true; dojo.provide("dijit._base.focus"); dojo.require("dojo.window"); dojo.require("dijit._base.manager"); // summary: // These functions are used to query or set the focus and selection. // // Also, they trace when widgets become activated/deactivated, // so that the widget can fire _onFocus/_onBlur events. // "Active" here means something similar to "focused", but // "focus" isn't quite the right word because we keep track of // a whole stack of "active" widgets. Example: ComboButton --> Menu --> // MenuItem. The onBlur event for ComboButton doesn't fire due to focusing // on the Menu or a MenuItem, since they are considered part of the // ComboButton widget. It only happens when focus is shifted // somewhere completely different. dojo.mixin(dijit, { // _curFocus: DomNode // Currently focused item on screen _curFocus: null, // _prevFocus: DomNode // Previously focused item on screen _prevFocus: null, isCollapsed: function(){ // summary: // Returns true if there is no text selected return dijit.getBookmark().isCollapsed; }, getBookmark: function(){ // summary: // Retrieves a bookmark that can be used with moveToBookmark to return to the same range var bm, rg, tg, sel = dojo.doc.selection, cf = dijit._curFocus; if(dojo.global.getSelection){ //W3C Range API for selections. sel = dojo.global.getSelection(); if(sel){ if(sel.isCollapsed){ tg = cf? cf.tagName : ""; if(tg){ //Create a fake rangelike item to restore selections. tg = tg.toLowerCase(); if(tg == "textarea" || (tg == "input" && (!cf.type || cf.type.toLowerCase() == "text"))){ sel = { start: cf.selectionStart, end: cf.selectionEnd, node: cf, pRange: true }; return {isCollapsed: (sel.end <= sel.start), mark: sel}; //Object. } } bm = {isCollapsed:true}; if(sel.rangeCount){ bm.mark = sel.getRangeAt(0).cloneRange(); } }else{ rg = sel.getRangeAt(0); bm = {isCollapsed: false, mark: rg.cloneRange()}; } } }else if(sel){ // If the current focus was a input of some sort and no selection, don't bother saving // a native bookmark. This is because it causes issues with dialog/page selection restore. // So, we need to create psuedo bookmarks to work with. tg = cf ? cf.tagName : ""; tg = tg.toLowerCase(); if(cf && tg && (tg == "button" || tg == "textarea" || tg == "input")){ if(sel.type && sel.type.toLowerCase() == "none"){ return { isCollapsed: true, mark: null } }else{ rg = sel.createRange(); return { isCollapsed: rg.text && rg.text.length?false:true, mark: { range: rg, pRange: true } }; } } bm = {}; //'IE' way for selections. try{ // createRange() throws exception when dojo in iframe //and nothing selected, see #9632 rg = sel.createRange(); bm.isCollapsed = !(sel.type == 'Text' ? rg.htmlText.length : rg.length); }catch(e){ bm.isCollapsed = true; return bm; } if(sel.type.toUpperCase() == 'CONTROL'){ if(rg.length){ bm.mark=[]; var i=0,len=rg.length; while(i<len){ bm.mark.push(rg.item(i++)); } }else{ bm.isCollapsed = true; bm.mark = null; } }else{ bm.mark = rg.getBookmark(); } }else{ console.warn("No idea how to store the current selection for this browser!"); } return bm; // Object }, moveToBookmark: function(/*Object*/bookmark){ // summary: // Moves current selection to a bookmark // bookmark: // This should be a returned object from dijit.getBookmark() var _doc = dojo.doc, mark = bookmark.mark; if(mark){ if(dojo.global.getSelection){ //W3C Rangi API (FF, WebKit, Opera, etc) var sel = dojo.global.getSelection(); if(sel && sel.removeAllRanges){ if(mark.pRange){ var r = mark; var n = r.node; n.selectionStart = r.start; n.selectionEnd = r.end; }else{ sel.removeAllRanges(); sel.addRange(mark); } }else{ console.warn("No idea how to restore selection for this browser!"); } }else if(_doc.selection && mark){ //'IE' way. var rg; if(mark.pRange){ rg = mark.range; }else if(dojo.isArray(mark)){ rg = _doc.body.createControlRange(); //rg.addElement does not have call/apply method, so can not call it directly //rg is not available in "range.addElement(item)", so can't use that either dojo.forEach(mark, function(n){ rg.addElement(n); }); }else{ rg = _doc.body.createTextRange(); rg.moveToBookmark(mark); } rg.select(); } } }, getFocus: function(/*Widget?*/ menu, /*Window?*/ openedForWindow){ // summary: // Called as getFocus(), this returns an Object showing the current focus // and selected text. // // Called as getFocus(widget), where widget is a (widget representing) a button // that was just pressed, it returns where focus was before that button // was pressed. (Pressing the button may have either shifted focus to the button, // or removed focus altogether.) In this case the selected text is not returned, // since it can't be accurately determined. // // menu: dijit._Widget or {domNode: DomNode} structure // The button that was just pressed. If focus has disappeared or moved // to this button, returns the previous focus. In this case the bookmark // information is already lost, and null is returned. // // openedForWindow: // iframe in which menu was opened // // returns: // A handle to restore focus/selection, to be passed to `dijit.focus` var node = !dijit._curFocus || (menu && dojo.isDescendant(dijit._curFocus, menu.domNode)) ? dijit._prevFocus : dijit._curFocus; return { node: node, bookmark: (node == dijit._curFocus) && dojo.withGlobal(openedForWindow || dojo.global, dijit.getBookmark), openedForWindow: openedForWindow }; // Object }, focus: function(/*Object || DomNode */ handle){ // summary: // Sets the focused node and the selection according to argument. // To set focus to an iframe's content, pass in the iframe itself. // handle: // object returned by get(), or a DomNode if(!handle){ return; } var node = "node" in handle ? handle.node : handle, // because handle is either DomNode or a composite object bookmark = handle.bookmark, openedForWindow = handle.openedForWindow, collapsed = bookmark ? bookmark.isCollapsed : false; // Set the focus // Note that for iframe's we need to use the <iframe> to follow the parentNode chain, // but we need to set focus to iframe.contentWindow if(node){ var focusNode = (node.tagName.toLowerCase() == "iframe") ? node.contentWindow : node; if(focusNode && focusNode.focus){ try{ // Gecko throws sometimes if setting focus is impossible, // node not displayed or something like that focusNode.focus(); }catch(e){/*quiet*/} } dijit._onFocusNode(node); } // set the selection // do not need to restore if current selection is not empty // (use keyboard to select a menu item) or if previous selection was collapsed // as it may cause focus shift (Esp in IE). if(bookmark && dojo.withGlobal(openedForWindow || dojo.global, dijit.isCollapsed) && !collapsed){ if(openedForWindow){ openedForWindow.focus(); } try{ dojo.withGlobal(openedForWindow || dojo.global, dijit.moveToBookmark, null, [bookmark]); }catch(e2){ /*squelch IE internal error, see http://trac.dojotoolkit.org/ticket/1984 */ } } }, // _activeStack: dijit._Widget[] // List of currently active widgets (focused widget and it's ancestors) _activeStack: [], 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 to pass to unregisterIframe() return dijit.registerWin(iframe.contentWindow, iframe); }, unregisterIframe: function(/*Object*/ handle){ // summary: // Unregisters listeners on the specified iframe created by registerIframe. // After calling be sure to delete or null out the handle itself. // handle: // Handle returned by registerIframe() dijit.unregisterWin(handle); }, 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 to pass to unregisterWin() // TODO: make this function private in 2.0; Editor/users should call registerIframe(), var mousedownListener = function(evt){ dijit._justMouseDowned = true; setTimeout(function(){ dijit._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(dojo.isIE && evt && evt.srcElement && evt.srcElement.parentNode == null){ return; } dijit._onTouchNode(effectiveNode || evt.target || evt.srcElement, "mouse"); }; //dojo.connect(targetWindow, "onscroll", ???); // Listen for blur and focus events on targetWindow's document. // IIRC, I'm using attachEvent() rather than dojo.connect() because focus/blur events don't bubble // through dojo.connect(), and also maybe to catch the focus events early, before onfocus handlers // fire. // 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 = dojo.isIE ? targetWindow.document.documentElement : targetWindow.document; if(doc){ if(dojo.isIE){ targetWindow.document.body.attachEvent('onmousedown', mousedownListener); var activateListener = function(evt){ // IE reports that nodes like <body> have gotten focus, even though they have tabIndex=-1, // Should consider those more like a mouse-click than a focus.... if(evt.srcElement.tagName.toLowerCase() != "#document" && dijit.isTabNavigable(evt.srcElement)){ dijit._onFocusNode(effectiveNode || evt.srcElement); }else{ dijit._onTouchNode(effectiveNode || evt.srcElement); } }; doc.attachEvent('onactivate', activateListener); var deactivateListener = function(evt){ dijit._onBlurNode(effectiveNode || evt.srcElement); }; doc.attachEvent('ondeactivate', deactivateListener); return function(){ targetWindow.document.detachEvent('onmousedown', mousedownListener); doc.detachEvent('onactivate', activateListener); doc.detachEvent('ondeactivate', deactivateListener); doc = null; // prevent memory leak (apparent circular reference via closure) }; }else{ doc.body.addEventListener('mousedown', mousedownListener, true); var focusListener = function(evt){ dijit._onFocusNode(effectiveNode || evt.target); }; doc.addEventListener('focus', focusListener, true); var blurListener = function(evt){ dijit._onBlurNode(effectiveNode || evt.target); }; doc.addEventListener('blur', blurListener, true); return function(){ doc.body.removeEventListener('mousedown', mousedownListener, true); doc.removeEventListener('focus', focusListener, true); doc.removeEventListener('blur', blurListener, true); doc = null; // prevent memory leak (apparent circular reference via closure) }; } } }, unregisterWin: function(/*Handle*/ handle){ // summary: // Unregisters listeners on the specified window (either the main // window or an iframe's window) according to handle returned from registerWin(). // After calling be sure to delete or null out the handle itself. // Currently our handle is actually a function handle && handle(); }, _onBlurNode: function(/*DomNode*/ node){ // summary: // Called when focus leaves a node. // Usually ignored, _unless_ it *isn't* follwed by touching another node, // which indicates that we tabbed off the last field on the page, // in which case every widget is marked inactive dijit._prevFocus = dijit._curFocus; dijit._curFocus = null; if(dijit._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 event then mark all widgets as inactive. if(dijit._clearActiveWidgetsTimer){ clearTimeout(dijit._clearActiveWidgetsTimer); } dijit._clearActiveWidgetsTimer = setTimeout(function(){ delete dijit._clearActiveWidgetsTimer; dijit._setStack([]); dijit._prevFocus = null; }, 100); }, _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(dijit._clearActiveWidgetsTimer){ clearTimeout(dijit._clearActiveWidgetsTimer); delete dijit._clearActiveWidgetsTimer; } // compute stack of active widgets (ex: ComboButton --> Menu --> MenuItem) var newStack=[]; try{ while(node){ var popupParent = dojo.attr(node, "dijitPopupParent"); if(popupParent){ node=dijit.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 === dojo.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=dojo.window.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 && dijit.byId(id); if(widget && !(by == "mouse" && widget.get("disabled"))){ newStack.unshift(id); } node=node.parentNode; } } }catch(e){ /* squelch */ } dijit._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; } dijit._onTouchNode(node); if(node == dijit._curFocus){ return; } if(dijit._curFocus){ dijit._prevFocus = dijit._curFocus; } dijit._curFocus = node; dojo.publish("focusNode", [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 = dijit._activeStack; dijit._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, send blur event for(var i=oldStack.length-1; i>=nCommon; i--){ widget = dijit.byId(oldStack[i]); if(widget){ widget._focused = false; widget.set("focused", false); widget._hasBeenBlurred = true; if(widget._onBlur){ widget._onBlur(by); } dojo.publish("widgetBlur", [widget, by]); } } // for all element that have come into focus, send focus event for(i=nCommon; i<newStack.length; i++){ widget = dijit.byId(newStack[i]); if(widget){ widget._focused = true; widget.set("focused", true); if(widget._onFocus){ widget._onFocus(by); } dojo.publish("widgetFocus", [widget, by]); } } } }); // register top window and all the iframes it contains dojo.addOnLoad(function(){ var handle = dijit.registerWin(window); if(dojo.isIE){ dojo.addOnWindowUnload(function(){ dijit.unregisterWin(handle); handle = null; }) } }); }