/* 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"]){ //_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.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", "\n\t\n
\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 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 , not . // 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 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 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 node of an