dojo.provide("dijit.Menu"); dojo.require("dijit._Widget"); dojo.require("dijit._KeyNavContainer"); dojo.require("dijit._Templated"); 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(); } } }, 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) tabbing into it // 3) 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); } } }, _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 item._setSelected(false); // Close all popups that are open and descendants of this menu dijit.popup.close(item.popup); this._stopPopupTimer(); }, onItemUnhover: function(/*MenuItem*/ item){ // summary: // Callback fires when mouse exits a MenuItem // tags: // protected if(this.isActive){ this._stopPopupTimer(); } }, _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; } }, _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(/*Widget*/ item, /*Event*/ evt){ // summary: // Handle clicks on an item. // tags: // private if(item.disabled){ return false; } this.focusChild(item); if(item.popup){ if(!this.is_open){ 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; var popup = from_item.popup; if(popup.isShowingNow){ return; } popup.parentMenu = this; var self = this; dijit.popup.open({ parent: this, popup: popup, around: from_item.domNode, orient: this._orient || (this.isLeftToRight() ? {'TR': 'TL', 'TL': 'TR'} : {'TL': 'TR', 'TR': 'TL'}), onCancel: function(){ // called when the child menu is canceled dijit.popup.close(popup); from_item.focus(); // put focus back on my node self.currentPopup = null; }, onExecute: dojo.hitch(this, "_onDescendantExecute") }); this.currentPopup = popup; 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) setTimeout(dojo.hitch(popup, "focus"), 0); } }, 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; }, 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._stopPopupTimer(); this.parentMenu = null; this.isShowingNow = false; this.currentPopup = null; if(this.focusedChild){ this._onChildBlur(this.focusedChild); this.focusedChild = null; } }, _onFocus: function(){ // summary: // Called when 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. // tags: // protected this.isActive = true; dojo.addClass(this.domNode, "dijitMenuActive"); dojo.removeClass(this.domNode, "dijitMenuPassive"); this.inherited(arguments); }, _onBlur: function(){ // summary: // Called when focus is moved away from this Menu and it's submenus. // tags: // protected this.isActive = false; dojo.removeClass(this.domNode, "dijitMenuActive"); dojo.addClass(this.domNode, "dijitMenuPassive"); // If user blurs/clicks away from a MenuBar (or always visible Menu), then close all popped up submenus etc. this.onClose(); this.inherited(arguments); }, _onDescendantExecute: function(){ // summary: // Called when submenu is clicked. Close hierarchy of menus. // tags: // private this.onClose(); } }); 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 = []; }, templatePath: dojo.moduleUrl("dijit", "templates/Menu.html"), // 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, // _contextMenuWithMouse: [private] Boolean // Used to record mouse and keyboard events to determine if a context // menu is being opened with the keyboard or the mouse. _contextMenuWithMouse: false, 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(/*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 = dijit.getDocumentWindow(dijit.Menu._iframeContentDocument(iframe_el)) || // Moz. TODO: is this available when defaultView isn't? dijit.Menu._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); //TODO: this is to support context popups in Editor. Maybe this shouldn't be in dijit.Menu var win = dijit.getDocumentWindow(node.ownerDocument); if(node.tagName.toLowerCase()=="iframe"){ win = this._iframeContentWindow(node); node = dojo.withGlobal(win, dojo.body); } // to capture these events at the top level, // attach to document, not body var cn = (node == dojo.body() ? dojo.doc : node); node[this.id] = this._bindings.push([ dojo.connect(cn, (this.leftClickToOpen)?"onclick":"oncontextmenu", this, "_openMyself"), dojo.connect(cn, "onkeydown", this, "_contextKey"), dojo.connect(cn, "onmousedown", this, "_contextMouse") ]); }, unBindDomNode: function(/*String|DomNode*/ nodeName){ // summary: // Detach menu from given node var node = dojo.byId(nodeName); if(node){ var bid = node[this.id]-1, b = this._bindings[bid]; dojo.forEach(b, dojo.disconnect); delete this._bindings[bid]; } }, _contextKey: function(e){ // summary: // Code to handle popping up editor using F10 key rather than mouse // tags: // private this._contextMenuWithMouse = false; if(e.keyCode == dojo.keys.F10){ dojo.stopEvent(e); if(e.shiftKey && e.type=="keydown"){ // FF: copying the wrong property from e will cause the system // context menu to appear in spite of stopEvent. Don't know // exactly which properties cause this effect. var _e = { target: e.target, pageX: e.pageX, pageY: e.pageY }; _e.preventDefault = _e.stopPropagation = function(){}; // IE: without the delay, focus work in "open" causes the system // context menu to appear in spite of stopEvent. window.setTimeout(dojo.hitch(this, function(){ this._openMyself(_e); }), 1); } } }, _contextMouse: function(e){ // summary: // Helper to remember when we opened the context menu with the mouse instead // of with the keyboard // tags: // private this._contextMenuWithMouse = true; }, _openMyself: function(/*Event*/ e){ // summary: // Internal function for opening myself when the user // does a right-click or something similar // tags: // private if(this.leftClickToOpen&&e.button>0){ return; } dojo.stopEvent(e); // Get coordinates. // if we are opening the menu with the mouse or on safari open // the menu at the mouse cursor // (Safari does not have a keyboard command to open the context menu // and we don't currently have a reliable way to determine // _contextMenuWithMouse on Safari) var x,y; if(dojo.isSafari || this._contextMenuWithMouse){ x=e.pageX; y=e.pageY; }else{ // otherwise open near e.target var coords = dojo.coords(e.target, true); x = coords.x + 10; y = coords.y + 10; } var self=this; var savedFocus = dijit.getFocus(this); function closeAndRestoreFocus(){ // user has clicked on a menu or popup dijit.focus(savedFocus); dijit.popup.close(self); } dijit.popup.open({ popup: this, x: x, y: 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.targetNodeIds, this.unBindDomNode, this); this.inherited(arguments); } } ); // Back-compat (TODO: remove in 2.0) dojo.require("dijit.MenuItem"); dojo.require("dijit.PopupMenuItem"); dojo.require("dijit.CheckedMenuItem"); dojo.require("dijit.MenuSeparator");