You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
466 lines
15 KiB
466 lines
15 KiB
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(); |
|
}), |
|
on(this.containerNode, on.selector(matches, "focusin"), function(){ |
|
self._onItemFocus(registry.byNode(this)); |
|
}) |
|
); |
|
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); |
|
} |
|
} |
|
}); |
|
});
|
|
|