365 lines
13 KiB
JavaScript
365 lines
13 KiB
JavaScript
define("dijit/focus", [
|
|
"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;
|
|
});
|