diff options
Diffstat (limited to 'lib/dijit/Menu.js')
-rw-r--r-- | lib/dijit/Menu.js | 1009 |
1 files changed, 700 insertions, 309 deletions
diff --git a/lib/dijit/Menu.js b/lib/dijit/Menu.js index 83a2ddbad..c5a48660e 100644 --- a/lib/dijit/Menu.js +++ b/lib/dijit/Menu.js @@ -1,325 +1,716 @@ /* - Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + 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.Menu"]){ -dojo._hasResource["dijit.Menu"]=true; +if(!dojo._hasResource["dijit.Menu"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.Menu"] = true; dojo.provide("dijit.Menu"); dojo.require("dojo.window"); dojo.require("dijit._Widget"); dojo.require("dijit._KeyNavContainer"); dojo.require("dijit._Templated"); -dojo.declare("dijit._MenuBase",[dijit._Widget,dijit._Templated,dijit._KeyNavContainer],{parentMenu:null,popupDelay:500,startup:function(){ -if(this._started){ -return; -} -dojo.forEach(this.getChildren(),function(_1){ -_1.startup(); -}); -this.startupKeyNavChildren(); -this.inherited(arguments); -},onExecute:function(){ -},onCancel:function(_2){ -},_moveToPopup:function(_3){ -if(this.focusedChild&&this.focusedChild.popup&&!this.focusedChild.disabled){ -this.focusedChild._onClick(_3); -}else{ -var _4=this._getTopMenu(); -if(_4&&_4._isMenuBar){ -_4.focusNext(); -} -} -},_onPopupHover:function(_5){ -if(this.currentPopup&&this.currentPopup._pendingClose_timer){ -var _6=this.currentPopup.parentMenu; -if(_6.focusedChild){ -_6.focusedChild._setSelected(false); -} -_6.focusedChild=this.currentPopup.from_item; -_6.focusedChild._setSelected(true); -this._stopPendingCloseTimer(this.currentPopup); -} -},onItemHover:function(_7){ -if(this.isActive){ -this.focusChild(_7); -if(this.focusedChild.popup&&!this.focusedChild.disabled&&!this.hover_timer){ -this.hover_timer=setTimeout(dojo.hitch(this,"_openPopup"),this.popupDelay); -} -} -if(this.focusedChild){ -this.focusChild(_7); -} -this._hoveredChild=_7; -},_onChildBlur:function(_8){ -this._stopPopupTimer(); -_8._setSelected(false); -var _9=_8.popup; -if(_9){ -this._stopPendingCloseTimer(_9); -_9._pendingClose_timer=setTimeout(function(){ -_9._pendingClose_timer=null; -if(_9.parentMenu){ -_9.parentMenu.currentPopup=null; -} -dijit.popup.close(_9); -},this.popupDelay); -} -},onItemUnhover:function(_a){ -if(this.isActive){ -this._stopPopupTimer(); -} -if(this._hoveredChild==_a){ -this._hoveredChild=null; -} -},_stopPopupTimer:function(){ -if(this.hover_timer){ -clearTimeout(this.hover_timer); -this.hover_timer=null; -} -},_stopPendingCloseTimer:function(_b){ -if(_b._pendingClose_timer){ -clearTimeout(_b._pendingClose_timer); -_b._pendingClose_timer=null; -} -},_stopFocusTimer:function(){ -if(this._focus_timer){ -clearTimeout(this._focus_timer); -this._focus_timer=null; -} -},_getTopMenu:function(){ -for(var _c=this;_c.parentMenu;_c=_c.parentMenu){ -} -return _c; -},onItemClick:function(_d,_e){ -if(typeof this.isShowingNow=="undefined"){ -this._markActive(); -} -this.focusChild(_d); -if(_d.disabled){ -return false; -} -if(_d.popup){ -this._openPopup(); -}else{ -this.onExecute(); -_d.onClick(_e); -} -},_openPopup:function(){ -this._stopPopupTimer(); -var _f=this.focusedChild; -if(!_f){ -return; -} -var _10=_f.popup; -if(_10.isShowingNow){ -return; -} -if(this.currentPopup){ -this._stopPendingCloseTimer(this.currentPopup); -dijit.popup.close(this.currentPopup); -} -_10.parentMenu=this; -_10.from_item=_f; -var _11=this; -dijit.popup.open({parent:this,popup:_10,around:_f.domNode,orient:this._orient||(this.isLeftToRight()?{"TR":"TL","TL":"TR","BR":"BL","BL":"BR"}:{"TL":"TR","TR":"TL","BL":"BR","BR":"BL"}),onCancel:function(){ -_11.focusChild(_f); -_11._cleanUp(); -_f._setSelected(true); -_11.focusedChild=_f; -},onExecute:dojo.hitch(this,"_cleanUp")}); -this.currentPopup=_10; -_10.connect(_10.domNode,"onmouseenter",dojo.hitch(_11,"_onPopupHover")); -if(_10.focus){ -_10._focus_timer=setTimeout(dojo.hitch(_10,function(){ -this._focus_timer=null; -this.focus(); -}),0); -} -},_markActive:function(){ -this.isActive=true; -dojo.addClass(this.domNode,"dijitMenuActive"); -dojo.removeClass(this.domNode,"dijitMenuPassive"); -},onOpen:function(e){ -this.isShowingNow=true; -this._markActive(); -},_markInactive:function(){ -this.isActive=false; -dojo.removeClass(this.domNode,"dijitMenuActive"); -dojo.addClass(this.domNode,"dijitMenuPassive"); -},onClose:function(){ -this._stopFocusTimer(); -this._markInactive(); -this.isShowingNow=false; -this.parentMenu=null; -},_closeChild:function(){ -this._stopPopupTimer(); -if(this.focusedChild){ -this.focusedChild._setSelected(false); -this.focusedChild._onUnhover(); -this.focusedChild=null; -} -if(this.currentPopup){ -dijit.popup.close(this.currentPopup); -this.currentPopup=null; -} -},_onItemFocus:function(_12){ -if(this._hoveredChild&&this._hoveredChild!=_12){ -this._hoveredChild._onUnhover(); -} -},_onBlur:function(){ -this._cleanUp(); -this.inherited(arguments); -},_cleanUp:function(){ -this._closeChild(); -if(typeof this.isShowingNow=="undefined"){ -this._markInactive(); -} -}}); -dojo.declare("dijit.Menu",dijit._MenuBase,{constructor:function(){ -this._bindings=[]; -},templateString:dojo.cache("dijit","templates/Menu.html","<table class=\"dijit dijitMenu dijitMenuPassive dijitReset dijitMenuTable\" waiRole=\"menu\" tabIndex=\"${tabIndex}\" dojoAttachEvent=\"onkeypress:_onKeyPress\" cellspacing=0>\n\t<tbody class=\"dijitReset\" dojoAttachPoint=\"containerNode\"></tbody>\n</table>\n"),baseClass:"dijitMenu",targetNodeIds:[],contextMenuForWindow:false,leftClickToOpen:false,refocus:true,postCreate:function(){ -if(this.contextMenuForWindow){ -this.bindDomNode(dojo.body()); -}else{ -dojo.forEach(this.targetNodeIds,this.bindDomNode,this); -} -var k=dojo.keys,l=this.isLeftToRight(); -this._openSubMenuKey=l?k.RIGHT_ARROW:k.LEFT_ARROW; -this._closeSubMenuKey=l?k.LEFT_ARROW:k.RIGHT_ARROW; -this.connectKeyNavHandlers([k.UP_ARROW],[k.DOWN_ARROW]); -},_onKeyPress:function(evt){ -if(evt.ctrlKey||evt.altKey){ -return; -} -switch(evt.charOrCode){ -case this._openSubMenuKey: -this._moveToPopup(evt); -dojo.stopEvent(evt); -break; -case this._closeSubMenuKey: -if(this.parentMenu){ -if(this.parentMenu._isMenuBar){ -this.parentMenu.focusPrev(); -}else{ -this.onCancel(false); -} -}else{ -dojo.stopEvent(evt); -} -break; -} -},_iframeContentWindow:function(_13){ -var win=dojo.window.get(this._iframeContentDocument(_13))||this._iframeContentDocument(_13)["__parent__"]||(_13.name&&dojo.doc.frames[_13.name])||null; -return win; -},_iframeContentDocument:function(_14){ -var doc=_14.contentDocument||(_14.contentWindow&&_14.contentWindow.document)||(_14.name&&dojo.doc.frames[_14.name]&&dojo.doc.frames[_14.name].document)||null; -return doc; -},bindDomNode:function(_15){ -_15=dojo.byId(_15); -var cn; -if(_15.tagName.toLowerCase()=="iframe"){ -var _16=_15,win=this._iframeContentWindow(_16); -cn=dojo.withGlobal(win,dojo.body); -}else{ -cn=(_15==dojo.body()?dojo.doc.documentElement:_15); -} -var _17={node:_15,iframe:_16}; -dojo.attr(_15,"_dijitMenu"+this.id,this._bindings.push(_17)); -var _18=dojo.hitch(this,function(cn){ -return [dojo.connect(cn,this.leftClickToOpen?"onclick":"oncontextmenu",this,function(evt){ -dojo.stopEvent(evt); -this._scheduleOpen(evt.target,_16,{x:evt.pageX,y:evt.pageY}); -}),dojo.connect(cn,"onkeydown",this,function(evt){ -if(evt.shiftKey&&evt.keyCode==dojo.keys.F10){ -dojo.stopEvent(evt); -this._scheduleOpen(evt.target,_16); -} -})]; -}); -_17.connects=cn?_18(cn):[]; -if(_16){ -_17.onloadHandler=dojo.hitch(this,function(){ -var win=this._iframeContentWindow(_16); -cn=dojo.withGlobal(win,dojo.body); -_17.connects=_18(cn); -}); -if(_16.addEventListener){ -_16.addEventListener("load",_17.onloadHandler,false); -}else{ -_16.attachEvent("onload",_17.onloadHandler); -} -} -},unBindDomNode:function(_19){ -var _1a; -try{ -_1a=dojo.byId(_19); -} -catch(e){ -return; -} -var _1b="_dijitMenu"+this.id; -if(_1a&&dojo.hasAttr(_1a,_1b)){ -var bid=dojo.attr(_1a,_1b)-1,b=this._bindings[bid]; -dojo.forEach(b.connects,dojo.disconnect); -var _1c=b.iframe; -if(_1c){ -if(_1c.removeEventListener){ -_1c.removeEventListener("load",b.onloadHandler,false); -}else{ -_1c.detachEvent("onload",b.onloadHandler); -} -} -dojo.removeAttr(_1a,_1b); -delete this._bindings[bid]; -} -},_scheduleOpen:function(_1d,_1e,_1f){ -if(!this._openTimer){ -this._openTimer=setTimeout(dojo.hitch(this,function(){ -delete this._openTimer; -this._openMyself({target:_1d,iframe:_1e,coords:_1f}); -}),1); -} -},_openMyself:function(_20){ -var _21=_20.target,_22=_20.iframe,_23=_20.coords; -if(_23){ -if(_22){ -var od=_21.ownerDocument,ifc=dojo.position(_22,true),win=this._iframeContentWindow(_22),_24=dojo.withGlobal(win,"_docScroll",dojo); -var cs=dojo.getComputedStyle(_22),tp=dojo._toPixelValue,_25=(dojo.isIE&&dojo.isQuirks?0:tp(_22,cs.paddingLeft))+(dojo.isIE&&dojo.isQuirks?tp(_22,cs.borderLeftWidth):0),top=(dojo.isIE&&dojo.isQuirks?0:tp(_22,cs.paddingTop))+(dojo.isIE&&dojo.isQuirks?tp(_22,cs.borderTopWidth):0); -_23.x+=ifc.x+_25-_24.x; -_23.y+=ifc.y+top-_24.y; -} -}else{ -_23=dojo.position(_21,true); -_23.x+=10; -_23.y+=10; -} -var _26=this; -var _27=dijit.getFocus(this); -function _28(){ -if(_26.refocus){ -dijit.focus(_27); -} -dijit.popup.close(_26); -}; -dijit.popup.open({popup:this,x:_23.x,y:_23.y,onExecute:_28,onCancel:_28,orient:this.isLeftToRight()?"L":"R"}); -this.focus(); -this._onBlur=function(){ -this.inherited("_onBlur",arguments); -dijit.popup.close(this); -}; -},uninitialize:function(){ -dojo.forEach(this._bindings,function(b){ -if(b){ -this.unBindDomNode(b.node); -} -},this); -this.inherited(arguments); -}}); dojo.require("dijit.MenuItem"); dojo.require("dijit.PopupMenuItem"); dojo.require("dijit.CheckedMenuItem"); dojo.require("dijit.MenuSeparator"); + + +// "dijit/MenuItem", "dijit/PopupMenuItem", "dijit/CheckedMenuItem", "dijit/MenuSeparator" for Back-compat (TODO: remove in 2.0) + +dojo.declare("dijit._MenuBase", + [dijit._Widget, dijit._Templated, dijit._KeyNavContainer], +{ + // summary: + // Base class for Menu and MenuBar + + // parentMenu: [readonly] Widget + // pointer to menu that displayed me + parentMenu: null, + + // popupDelay: Integer + // number of milliseconds before hovering (without clicking) causes the popup to automatically open. + popupDelay: 500, + + startup: function(){ + if(this._started){ return; } + + dojo.forEach(this.getChildren(), function(child){ child.startup(); }); + this.startupKeyNavChildren(); + + this.inherited(arguments); + }, + + onExecute: function(){ + // summary: + // Attach point for notification about when a menu item has been executed. + // This is an internal mechanism used for Menus to signal to their parent to + // close them, because they are about to execute the onClick handler. In + // general developers should not attach to or override this method. + // tags: + // protected + }, + + onCancel: function(/*Boolean*/ closeAll){ + // summary: + // Attach point for notification about when the user cancels the current menu + // This is an internal mechanism used for Menus to signal to their parent to + // close them. In general developers should not attach to or override this method. + // tags: + // protected + }, + + _moveToPopup: function(/*Event*/ evt){ + // summary: + // This handles the right arrow key (left arrow key on RTL systems), + // which will either open a submenu, or move to the next item in the + // ancestor MenuBar + // tags: + // private + + if(this.focusedChild && this.focusedChild.popup && !this.focusedChild.disabled){ + this.focusedChild._onClick(evt); + }else{ + var topMenu = this._getTopMenu(); + if(topMenu && topMenu._isMenuBar){ + topMenu.focusNext(); + } + } + }, + + _onPopupHover: function(/*Event*/ evt){ + // summary: + // This handler is called when the mouse moves over the popup. + // tags: + // private + + // if the mouse hovers over a menu popup that is in pending-close state, + // then stop the close operation. + // This can't be done in onItemHover since some popup targets don't have MenuItems (e.g. ColorPicker) + if(this.currentPopup && this.currentPopup._pendingClose_timer){ + var parentMenu = this.currentPopup.parentMenu; + // highlight the parent menu item pointing to this popup + if(parentMenu.focusedChild){ + parentMenu.focusedChild._setSelected(false); + } + parentMenu.focusedChild = this.currentPopup.from_item; + parentMenu.focusedChild._setSelected(true); + // cancel the pending close + this._stopPendingCloseTimer(this.currentPopup); + } + }, + + onItemHover: function(/*MenuItem*/ item){ + // summary: + // Called when cursor is over a MenuItem. + // tags: + // protected + + // Don't do anything unless user has "activated" the menu by: + // 1) clicking it + // 2) opening it from a parent menu (which automatically focuses it) + if(this.isActive){ + this.focusChild(item); + if(this.focusedChild.popup && !this.focusedChild.disabled && !this.hover_timer){ + this.hover_timer = setTimeout(dojo.hitch(this, "_openPopup"), this.popupDelay); + } + } + // if the user is mixing mouse and keyboard navigation, + // then the menu may not be active but a menu item has focus, + // but it's not the item that the mouse just hovered over. + // To avoid both keyboard and mouse selections, use the latest. + if(this.focusedChild){ + this.focusChild(item); + } + this._hoveredChild = item; + }, + + _onChildBlur: function(item){ + // summary: + // Called when a child MenuItem becomes inactive because focus + // has been removed from the MenuItem *and* it's descendant menus. + // tags: + // private + this._stopPopupTimer(); + item._setSelected(false); + // Close all popups that are open and descendants of this menu + var itemPopup = item.popup; + if(itemPopup){ + this._stopPendingCloseTimer(itemPopup); + itemPopup._pendingClose_timer = setTimeout(function(){ + itemPopup._pendingClose_timer = null; + if(itemPopup.parentMenu){ + itemPopup.parentMenu.currentPopup = null; + } + dijit.popup.close(itemPopup); // this calls onClose + }, this.popupDelay); + } + }, + + onItemUnhover: function(/*MenuItem*/ item){ + // summary: + // Callback fires when mouse exits a MenuItem + // tags: + // protected + + if(this.isActive){ + this._stopPopupTimer(); + } + if(this._hoveredChild == item){ this._hoveredChild = null; } + }, + + _stopPopupTimer: function(){ + // summary: + // Cancels the popup timer because the user has stop hovering + // on the MenuItem, etc. + // tags: + // private + if(this.hover_timer){ + clearTimeout(this.hover_timer); + this.hover_timer = null; + } + }, + + _stopPendingCloseTimer: function(/*dijit._Widget*/ popup){ + // summary: + // Cancels the pending-close timer because the close has been preempted + // tags: + // private + if(popup._pendingClose_timer){ + clearTimeout(popup._pendingClose_timer); + popup._pendingClose_timer = null; + } + }, + + _stopFocusTimer: function(){ + // summary: + // Cancels the pending-focus timer because the menu was closed before focus occured + // tags: + // private + if(this._focus_timer){ + clearTimeout(this._focus_timer); + this._focus_timer = null; + } + }, + + _getTopMenu: function(){ + // summary: + // Returns the top menu in this chain of Menus + // tags: + // private + for(var top=this; top.parentMenu; top=top.parentMenu); + return top; + }, + + onItemClick: function(/*dijit._Widget*/ item, /*Event*/ evt){ + // summary: + // Handle clicks on an item. + // tags: + // private + + // this can't be done in _onFocus since the _onFocus events occurs asynchronously + if(typeof this.isShowingNow == 'undefined'){ // non-popup menu + this._markActive(); + } + + this.focusChild(item); + + if(item.disabled){ return false; } + + if(item.popup){ + this._openPopup(); + }else{ + // before calling user defined handler, close hierarchy of menus + // and restore focus to place it was when menu was opened + this.onExecute(); + + // user defined handler for click + item.onClick(evt); + } + }, + + _openPopup: function(){ + // summary: + // Open the popup to the side of/underneath the current menu item + // tags: + // protected + + this._stopPopupTimer(); + var from_item = this.focusedChild; + if(!from_item){ return; } // the focused child lost focus since the timer was started + var popup = from_item.popup; + if(popup.isShowingNow){ return; } + if(this.currentPopup){ + this._stopPendingCloseTimer(this.currentPopup); + dijit.popup.close(this.currentPopup); + } + popup.parentMenu = this; + popup.from_item = from_item; // helps finding the parent item that should be focused for this popup + var self = this; + dijit.popup.open({ + parent: this, + popup: popup, + around: from_item.domNode, + orient: this._orient || (this.isLeftToRight() ? + {'TR': 'TL', 'TL': 'TR', 'BR': 'BL', 'BL': 'BR'} : + {'TL': 'TR', 'TR': 'TL', 'BL': 'BR', 'BR': 'BL'}), + onCancel: function(){ // called when the child menu is canceled + // set isActive=false (_closeChild vs _cleanUp) so that subsequent hovering will NOT open child menus + // which seems aligned with the UX of most applications (e.g. notepad, wordpad, paint shop pro) + self.focusChild(from_item); // put focus back on my node + self._cleanUp(); // close the submenu (be sure this is done _after_ focus is moved) + from_item._setSelected(true); // oops, _cleanUp() deselected the item + self.focusedChild = from_item; // and unset focusedChild + }, + onExecute: dojo.hitch(this, "_cleanUp") + }); + + this.currentPopup = popup; + // detect mouseovers to handle lazy mouse movements that temporarily focus other menu items + popup.connect(popup.domNode, "onmouseenter", dojo.hitch(self, "_onPopupHover")); // cleaned up when the popped-up widget is destroyed on close + + if(popup.focus){ + // If user is opening the popup via keyboard (right arrow, or down arrow for MenuBar), + // if the cursor happens to collide with the popup, it will generate an onmouseover event + // even though the mouse wasn't moved. Use a setTimeout() to call popup.focus so that + // our focus() call overrides the onmouseover event, rather than vice-versa. (#8742) + popup._focus_timer = setTimeout(dojo.hitch(popup, function(){ + this._focus_timer = null; + this.focus(); + }), 0); + } + }, + + _markActive: function(){ + // summary: + // Mark this menu's state as active. + // Called when this Menu gets focus from: + // 1) clicking it (mouse or via space/arrow key) + // 2) being opened by a parent menu. + // This is not called just from mouse hover. + // Focusing a menu via TAB does NOT automatically set isActive + // since TAB is a navigation operation and not a selection one. + // For Windows apps, pressing the ALT key focuses the menubar + // menus (similar to TAB navigation) but the menu is not active + // (ie no dropdown) until an item is clicked. + this.isActive = true; + dojo.replaceClass(this.domNode, "dijitMenuActive", "dijitMenuPassive"); + }, + + onOpen: function(/*Event*/ e){ + // summary: + // Callback when this menu is opened. + // This is called by the popup manager as notification that the menu + // was opened. + // tags: + // private + + this.isShowingNow = true; + this._markActive(); + }, + + _markInactive: function(){ + // summary: + // Mark this menu's state as inactive. + this.isActive = false; // don't do this in _onBlur since the state is pending-close until we get here + dojo.replaceClass(this.domNode, "dijitMenuPassive", "dijitMenuActive"); + }, + + onClose: function(){ + // summary: + // Callback when this menu is closed. + // This is called by the popup manager as notification that the menu + // was closed. + // tags: + // private + + this._stopFocusTimer(); + this._markInactive(); + this.isShowingNow = false; + this.parentMenu = null; + }, + + _closeChild: function(){ + // summary: + // Called when submenu is clicked or focus is lost. Close hierarchy of menus. + // tags: + // private + this._stopPopupTimer(); + + var fromItem = this.focusedChild && this.focusedChild.from_item; + + if(this.currentPopup){ + // If focus is on my child menu then move focus to me, + // because IE doesn't like it when you display:none a node with focus + if(dijit._curFocus && dojo.isDescendant(dijit._curFocus, this.currentPopup.domNode)){ + this.focusedChild.focusNode.focus(); + } + // Close all popups that are open and descendants of this menu + dijit.popup.close(this.currentPopup); + this.currentPopup = null; + } + + if(this.focusedChild){ // unhighlight the focused item + this.focusedChild._setSelected(false); + this.focusedChild._onUnhover(); + this.focusedChild = null; + } + }, + + _onItemFocus: function(/*MenuItem*/ item){ + // summary: + // Called when child of this Menu gets focus from: + // 1) clicking it + // 2) tabbing into it + // 3) being opened by a parent menu. + // This is not called just from mouse hover. + if(this._hoveredChild && this._hoveredChild != item){ + this._hoveredChild._onUnhover(); // any previous mouse movement is trumped by focus selection + } + }, + + _onBlur: function(){ + // summary: + // Called when focus is moved away from this Menu and it's submenus. + // tags: + // protected + this._cleanUp(); + this.inherited(arguments); + }, + + _cleanUp: function(){ + // summary: + // Called when the user is done with this menu. Closes hierarchy of menus. + // tags: + // private + + this._closeChild(); // don't call this.onClose since that's incorrect for MenuBar's that never close + if(typeof this.isShowingNow == 'undefined'){ // non-popup menu doesn't call onClose + this._markInactive(); + } + } +}); + +dojo.declare("dijit.Menu", + dijit._MenuBase, + { + // summary + // A context menu you can assign to multiple elements + + // TODO: most of the code in here is just for context menu (right-click menu) + // support. In retrospect that should have been a separate class (dijit.ContextMenu). + // Split them for 2.0 + + constructor: function(){ + this._bindings = []; + }, + + templateString: dojo.cache("dijit", "templates/Menu.html", "<table class=\"dijit dijitMenu dijitMenuPassive dijitReset dijitMenuTable\" role=\"menu\" tabIndex=\"${tabIndex}\" dojoAttachEvent=\"onkeypress:_onKeyPress\" cellspacing=\"0\">\n\t<tbody class=\"dijitReset\" dojoAttachPoint=\"containerNode\"></tbody>\n</table>\n"), + + baseClass: "dijitMenu", + + // targetNodeIds: [const] String[] + // Array of dom node ids of nodes to attach to. + // Fill this with nodeIds upon widget creation and it becomes context menu for those nodes. + targetNodeIds: [], + + // contextMenuForWindow: [const] Boolean + // If true, right clicking anywhere on the window will cause this context menu to open. + // If false, must specify targetNodeIds. + contextMenuForWindow: false, + + // leftClickToOpen: [const] Boolean + // If true, menu will open on left click instead of right click, similiar to a file menu. + leftClickToOpen: false, + + // refocus: Boolean + // When this menu closes, re-focus the element which had focus before it was opened. + refocus: true, + + postCreate: function(){ + if(this.contextMenuForWindow){ + this.bindDomNode(dojo.body()); + }else{ + // TODO: should have _setTargetNodeIds() method to handle initialization and a possible + // later set('targetNodeIds', ...) call. There's also a problem that targetNodeIds[] + // gets stale after calls to bindDomNode()/unBindDomNode() as it still is just the original list (see #9610) + dojo.forEach(this.targetNodeIds, this.bindDomNode, this); + } + var k = dojo.keys, l = this.isLeftToRight(); + this._openSubMenuKey = l ? k.RIGHT_ARROW : k.LEFT_ARROW; + this._closeSubMenuKey = l ? k.LEFT_ARROW : k.RIGHT_ARROW; + this.connectKeyNavHandlers([k.UP_ARROW], [k.DOWN_ARROW]); + }, + + _onKeyPress: function(/*Event*/ evt){ + // summary: + // Handle keyboard based menu navigation. + // tags: + // protected + + if(evt.ctrlKey || evt.altKey){ return; } + + switch(evt.charOrCode){ + case this._openSubMenuKey: + this._moveToPopup(evt); + dojo.stopEvent(evt); + break; + case this._closeSubMenuKey: + if(this.parentMenu){ + if(this.parentMenu._isMenuBar){ + this.parentMenu.focusPrev(); + }else{ + this.onCancel(false); + } + }else{ + dojo.stopEvent(evt); + } + break; + } + }, + + // thanks burstlib! + _iframeContentWindow: function(/* HTMLIFrameElement */iframe_el){ + // summary: + // Returns the window reference of the passed iframe + // tags: + // private + var win = dojo.window.get(this._iframeContentDocument(iframe_el)) || + // Moz. TODO: is this available when defaultView isn't? + this._iframeContentDocument(iframe_el)['__parent__'] || + (iframe_el.name && dojo.doc.frames[iframe_el.name]) || null; + return win; // Window + }, + + _iframeContentDocument: function(/* HTMLIFrameElement */iframe_el){ + // summary: + // Returns a reference to the document object inside iframe_el + // tags: + // protected + var doc = iframe_el.contentDocument // W3 + || (iframe_el.contentWindow && iframe_el.contentWindow.document) // IE + || (iframe_el.name && dojo.doc.frames[iframe_el.name] && dojo.doc.frames[iframe_el.name].document) + || null; + return doc; // HTMLDocument + }, + + bindDomNode: function(/*String|DomNode*/ node){ + // summary: + // Attach menu to given node + node = dojo.byId(node); + + var cn; // Connect node + + // Support context menus on iframes. Rather than binding to the iframe itself we need + // to bind to the <body> node inside the iframe. + if(node.tagName.toLowerCase() == "iframe"){ + var iframe = node, + win = this._iframeContentWindow(iframe); + cn = dojo.withGlobal(win, dojo.body); + }else{ + + // To capture these events at the top level, attach to <html>, not <body>. + // Otherwise right-click context menu just doesn't work. + cn = (node == dojo.body() ? dojo.doc.documentElement : node); + } + + + // "binding" is the object to track our connection to the node (ie, the parameter to bindDomNode()) + var binding = { + node: node, + iframe: iframe + }; + + // Save info about binding in _bindings[], and make node itself record index(+1) into + // _bindings[] array. Prefix w/_dijitMenu to avoid setting an attribute that may + // start with a number, which fails on FF/safari. + dojo.attr(node, "_dijitMenu" + this.id, this._bindings.push(binding)); + + // Setup the connections to monitor click etc., unless we are connecting to an iframe which hasn't finished + // loading yet, in which case we need to wait for the onload event first, and then connect + // On linux Shift-F10 produces the oncontextmenu event, but on Windows it doesn't, so + // we need to monitor keyboard events in addition to the oncontextmenu event. + var doConnects = dojo.hitch(this, function(cn){ + return [ + // TODO: when leftClickToOpen is true then shouldn't space/enter key trigger the menu, + // rather than shift-F10? + dojo.connect(cn, this.leftClickToOpen ? "onclick" : "oncontextmenu", this, function(evt){ + // Schedule context menu to be opened unless it's already been scheduled from onkeydown handler + dojo.stopEvent(evt); + this._scheduleOpen(evt.target, iframe, {x: evt.pageX, y: evt.pageY}); + }), + dojo.connect(cn, "onkeydown", this, function(evt){ + if(evt.shiftKey && evt.keyCode == dojo.keys.F10){ + dojo.stopEvent(evt); + this._scheduleOpen(evt.target, iframe); // no coords - open near target node + } + }) + ]; + }); + binding.connects = cn ? doConnects(cn) : []; + + if(iframe){ + // Setup handler to [re]bind to the iframe when the contents are initially loaded, + // and every time the contents change. + // Need to do this b/c we are actually binding to the iframe's <body> node. + // Note: can't use dojo.connect(), see #9609. + + binding.onloadHandler = dojo.hitch(this, function(){ + // want to remove old connections, but IE throws exceptions when trying to + // access the <body> node because it's already gone, or at least in a state of limbo + + var win = this._iframeContentWindow(iframe); + cn = dojo.withGlobal(win, dojo.body); + binding.connects = doConnects(cn); + }); + if(iframe.addEventListener){ + iframe.addEventListener("load", binding.onloadHandler, false); + }else{ + iframe.attachEvent("onload", binding.onloadHandler); + } + } + }, + + unBindDomNode: function(/*String|DomNode*/ nodeName){ + // summary: + // Detach menu from given node + + var node; + try{ + node = dojo.byId(nodeName); + }catch(e){ + // On IE the dojo.byId() call will get an exception if the attach point was + // the <body> node of an <iframe> that has since been reloaded (and thus the + // <body> node is in a limbo state of destruction. + return; + } + + // node["_dijitMenu" + this.id] contains index(+1) into my _bindings[] array + var attrName = "_dijitMenu" + this.id; + if(node && dojo.hasAttr(node, attrName)){ + var bid = dojo.attr(node, attrName)-1, b = this._bindings[bid]; + dojo.forEach(b.connects, dojo.disconnect); + + // Remove listener for iframe onload events + var iframe = b.iframe; + if(iframe){ + if(iframe.removeEventListener){ + iframe.removeEventListener("load", b.onloadHandler, false); + }else{ + iframe.detachEvent("onload", b.onloadHandler); + } + } + + dojo.removeAttr(node, attrName); + delete this._bindings[bid]; + } + }, + + _scheduleOpen: function(/*DomNode?*/ target, /*DomNode?*/ iframe, /*Object?*/ coords){ + // summary: + // Set timer to display myself. Using a timer rather than displaying immediately solves + // two problems: + // + // 1. IE: without the delay, focus work in "open" causes the system + // context menu to appear in spite of stopEvent. + // + // 2. Avoid double-shows on linux, where shift-F10 generates an oncontextmenu event + // even after a dojo.stopEvent(e). (Shift-F10 on windows doesn't generate the + // oncontextmenu event.) + + if(!this._openTimer){ + this._openTimer = setTimeout(dojo.hitch(this, function(){ + delete this._openTimer; + this._openMyself({ + target: target, + iframe: iframe, + coords: coords + }); + }), 1); + } + }, + + _openMyself: function(args){ + // summary: + // Internal function for opening myself when the user does a right-click or something similar. + // args: + // This is an Object containing: + // * target: + // The node that is being clicked + // * iframe: + // If an <iframe> is being clicked, iframe points to that iframe + // * coords: + // Put menu at specified x/y position in viewport, or if iframe is + // specified, then relative to iframe. + // + // _openMyself() formerly took the event object, and since various code references + // evt.target (after connecting to _openMyself()), using an Object for parameters + // (so that old code still works). + + var target = args.target, + iframe = args.iframe, + coords = args.coords; + + // Get coordinates to open menu, either at specified (mouse) position or (if triggered via keyboard) + // then near the node the menu is assigned to. + if(coords){ + if(iframe){ + // Specified coordinates are on <body> node of an <iframe>, convert to match main document + var od = target.ownerDocument, + ifc = dojo.position(iframe, true), + win = this._iframeContentWindow(iframe), + scroll = dojo.withGlobal(win, "_docScroll", dojo); + + var cs = dojo.getComputedStyle(iframe), + tp = dojo._toPixelValue, + left = (dojo.isIE && dojo.isQuirks ? 0 : tp(iframe, cs.paddingLeft)) + (dojo.isIE && dojo.isQuirks ? tp(iframe, cs.borderLeftWidth) : 0), + top = (dojo.isIE && dojo.isQuirks ? 0 : tp(iframe, cs.paddingTop)) + (dojo.isIE && dojo.isQuirks ? tp(iframe, cs.borderTopWidth) : 0); + + coords.x += ifc.x + left - scroll.x; + coords.y += ifc.y + top - scroll.y; + } + }else{ + coords = dojo.position(target, true); + coords.x += 10; + coords.y += 10; + } + + var self=this; + var savedFocus = dijit.getFocus(this); + function closeAndRestoreFocus(){ + // user has clicked on a menu or popup + if(self.refocus){ + dijit.focus(savedFocus); + } + dijit.popup.close(self); + } + dijit.popup.open({ + popup: this, + x: coords.x, + y: coords.y, + onExecute: closeAndRestoreFocus, + onCancel: closeAndRestoreFocus, + orient: this.isLeftToRight() ? 'L' : 'R' + }); + this.focus(); + + this._onBlur = function(){ + this.inherited('_onBlur', arguments); + // Usually the parent closes the child widget but if this is a context + // menu then there is no parent + dijit.popup.close(this); + // don't try to restore focus; user has clicked another part of the screen + // and set focus there + }; + }, + + uninitialize: function(){ + dojo.forEach(this._bindings, function(b){ if(b){ this.unBindDomNode(b.node); } }, this); + this.inherited(arguments); + } +} +); + } |