You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
467 lines
15 KiB
JavaScript
467 lines
15 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
});
|
|
});
|