define([ "dojo/_base/array", // array.indexOf "dojo/_base/declare", // declare "dojo/dom", // dom.isDescendant domClass.replace "dojo/dom-attr", "dojo/dom-class", // domClass.replace "dojo/_base/lang", // lang.hitch "dojo/mouse", // mouse.enter, mouse.leave "dojo/on", "dojo/window", "./a11yclick", "./registry", "./_Widget", "./_CssStateMixin", "./_KeyNavContainer", "./_TemplatedMixin" ], function(array, declare, dom, domAttr, domClass, lang, mouse, on, winUtils, a11yclick, registry, _Widget, _CssStateMixin, _KeyNavContainer, _TemplatedMixin){ // module: // dijit/_MenuBase return declare("dijit._MenuBase", [_Widget, _TemplatedMixin, _KeyNavContainer, _CssStateMixin], { // summary: // Abstract base class for Menu and MenuBar. // Subclass should implement _onUpArrow(), _onDownArrow(), _onLeftArrow(), and _onRightArrow(). // selected: dijit/MenuItem // Currently selected (a.k.a. highlighted) MenuItem, or null if no MenuItem is selected. // If a submenu is open, will be set to MenuItem that displayed the submenu. OTOH, if // this Menu is in passive mode (i.e. hasn't been clicked yet), will be null, because // "selected" is not merely "hovered". selected: null, _setSelectedAttr: function(item){ if(this.selected != item){ if(this.selected){ this.selected._setSelected(false); this._onChildDeselect(this.selected); } if(item){ item._setSelected(true); } this._set("selected", item); } }, // activated: [readonly] Boolean // This Menu has been clicked (mouse or via space/arrow key) or opened as a submenu, // so mere mouseover will open submenus. Focusing a menu via TAB does NOT automatically make it active // 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. activated: false, _setActivatedAttr: function(val){ domClass.toggle(this.domNode, "dijitMenuActive", val); domClass.toggle(this.domNode, "dijitMenuPassive", !val); this._set("activated", val); }, // parentMenu: [readonly] Widget // pointer to menu that displayed me parentMenu: null, // popupDelay: Integer // After a menu has been activated (by clicking on it etc.), number of milliseconds before hovering // (without clicking) another MenuItem causes that MenuItem's popup to automatically open. popupDelay: 500, // passivePopupDelay: Integer // For a passive (unclicked) Menu, number of milliseconds before hovering (without clicking) will cause // the popup to open. Default is Infinity, meaning you need to click the menu to open it. passivePopupDelay: Infinity, // autoFocus: Boolean // A toggle to control whether or not a Menu gets focused when opened as a drop down from a MenuBar // or DropDownButton/ComboButton. Note though that it always get focused when opened via the keyboard. autoFocus: false, childSelector: function(/*DOMNode*/ node){ // summary: // Selector (passed to on.selector()) used to identify MenuItem child widgets, but exclude inert children // like MenuSeparator. If subclass overrides to a string (ex: "> *"), the subclass must require dojo/query. // tags: // protected var widget = registry.byNode(node); return node.parentNode == this.containerNode && widget && widget.focus; }, postCreate: function(){ var self = this, matches = typeof this.childSelector == "string" ? this.childSelector : lang.hitch(this, "childSelector"); this.own( on(this.containerNode, on.selector(matches, mouse.enter), function(){ self.onItemHover(registry.byNode(this)); }), on(this.containerNode, on.selector(matches, mouse.leave), function(){ self.onItemUnhover(registry.byNode(this)); }), on(this.containerNode, on.selector(matches, a11yclick), function(evt){ self.onItemClick(registry.byNode(this), evt); evt.stopPropagation(); evt.preventDefault(); }) ); this.inherited(arguments); }, onKeyboardSearch: function(/*MenuItem*/ item, /*Event*/ evt, /*String*/ searchString, /*Number*/ numMatches){ // summary: // Attach point for notification about when a menu item has been searched for // via the keyboard search mechanism. // tags: // protected this.inherited(arguments); if(!!item && (numMatches == -1 || (!!item.popup && numMatches == 1))){ this.onItemClick(item, evt); } }, _keyboardSearchCompare: function(/*dijit/_WidgetBase*/ item, /*String*/ searchString){ // summary: // Compares the searchString to the widget's text label, returning: // -1: a high priority match and stop searching // 0: no match // 1: a match but keep looking for a higher priority match // tags: // private if(!!item.shortcutKey){ // accessKey matches have priority return searchString == item.shortcutKey.toLowerCase() ? -1 : 0; } return this.inherited(arguments) ? 1 : 0; // change return value of -1 to 1 so that searching continues }, 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.onItemClick(this.focusedChild, 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) // highlight the parent menu item pointing to this popup (in case user temporarily moused over another MenuItem) this.set("selected", this.currentPopupItem); // cancel the pending close (if there is one) (in case user temporarily moused over another MenuItem) this._stopPendingCloseTimer(); }, 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 activates it) if(this.activated){ this.set("selected", item); if(item.popup && !item.disabled && !this.hover_timer){ this.hover_timer = this.defer(function(){ this._openItemPopup(item); }, this.popupDelay); } }else if(this.passivePopupDelay < Infinity){ if(this.passive_hover_timer){ this.passive_hover_timer.remove(); } this.passive_hover_timer = this.defer(function(){ this.onItemClick(item, {type: "click"}); }, this.passivePopupDelay); } this._hoveredChild = item; item._set("hovering", true); }, _onChildDeselect: function(item){ // summary: // Called when a child MenuItem becomes deselected. Setup timer to close its popup. this._stopPopupTimer(); // Setup timer to close all popups that are open and descendants of this menu. // Will be canceled if user quickly moves the mouse over the popup. if(this.currentPopupItem == item){ this._stopPendingCloseTimer(); this._pendingClose_timer = this.defer(function(){ this._pendingClose_timer = null; this.currentPopupItem = null; item._closePopup(); // this calls onClose }, this.popupDelay); } }, onItemUnhover: function(/*MenuItem*/ item){ // summary: // Callback fires when mouse exits a MenuItem // tags: // protected if(this._hoveredChild == item){ this._hoveredChild = null; } if(this.passive_hover_timer){ this.passive_hover_timer.remove(); this.passive_hover_timer = null; } item._set("hovering", false); }, _stopPopupTimer: function(){ // summary: // Cancels the popup timer because the user has stop hovering // on the MenuItem, etc. // tags: // private if(this.hover_timer){ this.hover_timer = this.hover_timer.remove(); } }, _stopPendingCloseTimer: function(){ // summary: // Cancels the pending-close timer because the close has been preempted // tags: // private if(this._pendingClose_timer){ this._pendingClose_timer = this._pendingClose_timer.remove(); } }, _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/_WidgetBase*/ item, /*Event*/ evt){ // summary: // Handle clicks on an item. // tags: // private if(this.passive_hover_timer){ this.passive_hover_timer.remove(); } this.focusChild(item); if(item.disabled){ return false; } if(item.popup){ this.set("selected", item); this.set("activated", true); var byKeyboard = /^key/.test(evt._origType || evt.type) || (evt.clientX == 0 && evt.clientY == 0); // detects accessKey like ALT+SHIFT+F, where type is "click" this._openItemPopup(item, byKeyboard); }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 ? item._onClick(evt) : item.onClick(evt); } }, _openItemPopup: function(/*dijit/MenuItem*/ from_item, /*Boolean*/ focus){ // summary: // Open the popup to the side of/underneath the current menu item, and optionally focus first item // tags: // protected if(from_item == this.currentPopupItem){ // Specified popup is already being shown, so just return return; } if(this.currentPopupItem){ // If another popup is currently shown, then close it this._stopPendingCloseTimer(); this.currentPopupItem._closePopup(); } this._stopPopupTimer(); var popup = from_item.popup; popup.parentMenu = this; // detect mouseover of the popup to handle lazy mouse movements that temporarily focus other menu items\c this.own(this._mouseoverHandle = on.once(popup.domNode, "mouseover", lang.hitch(this, "_onPopupHover"))); var self = this; from_item._openPopup({ parent: this, orient: this._orient || ["after", "before"], onCancel: function(){ // called when the child menu is canceled if(focus){ // put focus back on my node before focused node is hidden self.focusChild(from_item); } // close the submenu (be sure this is done _after_ focus is moved) self._cleanUp(); }, onExecute: lang.hitch(this, "_cleanUp", true), onClose: function(){ // Remove handler created by onItemHover if(self._mouseoverHandle){ self._mouseoverHandle.remove(); delete self._mouseoverHandle; } } }, focus); this.currentPopupItem = from_item; // TODO: focusing a popup should clear tabIndex on Menu (and it's child MenuItems), so that neither // TAB nor SHIFT-TAB returns to the menu. Only ESC or ENTER should return to the menu. }, 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.set("activated", 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.set("activated", false); this.set("selected", null); 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(); if(this.currentPopupItem){ // If focus is on a descendant MenuItem then move focus to me, // because IE doesn't like it when you display:none a node with focus, // and also so keyboard users don't lose control. // Likely, immediately after a user defined onClick handler will move focus somewhere // else, like a Dialog. if(this.focused){ domAttr.set(this.selected.focusNode, "tabIndex", this.tabIndex); this.selected.focusNode.focus(); } // Close all popups that are open and descendants of this menu this.currentPopupItem._closePopup(); this.currentPopupItem = 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.onItemUnhover(this._hoveredChild); // any previous mouse movement is trumped by focus selection } this.set("selected", item); }, _onBlur: function(){ // summary: // Called when focus is moved away from this Menu and it's submenus. // tags: // protected this._cleanUp(true); this.inherited(arguments); }, _cleanUp: function(/*Boolean*/ clearSelectedItem){ // 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.set("activated", false); } if(clearSelectedItem){ this.set("selected", null); } } }); });