531 lines
17 KiB
JavaScript
531 lines
17 KiB
JavaScript
/*
|
|
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;
|
|
})
|
|
}
|
|
});
|
|
|
|
}
|