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.
468 lines
16 KiB
JavaScript
468 lines
16 KiB
JavaScript
define([
|
|
"dojo/_base/declare", // declare
|
|
"dojo/_base/Deferred",
|
|
"dojo/dom", // dom.isDescendant
|
|
"dojo/dom-attr", // domAttr.set
|
|
"dojo/dom-class", // domClass.add domClass.contains domClass.remove
|
|
"dojo/dom-geometry", // domGeometry.marginBox domGeometry.position
|
|
"dojo/dom-style", // domStyle.set
|
|
"dojo/has", // has("touch")
|
|
"dojo/keys", // keys.DOWN_ARROW keys.ENTER keys.ESCAPE
|
|
"dojo/_base/lang", // lang.hitch lang.isFunction
|
|
"dojo/on",
|
|
"dojo/touch",
|
|
"./registry", // registry.byNode()
|
|
"./focus",
|
|
"./popup",
|
|
"./_FocusMixin"
|
|
], function(declare, Deferred, dom, domAttr, domClass, domGeometry, domStyle, has, keys, lang, on, touch,
|
|
registry, focus, popup, _FocusMixin){
|
|
|
|
|
|
// module:
|
|
// dijit/_HasDropDown
|
|
|
|
return declare("dijit._HasDropDown", _FocusMixin, {
|
|
// summary:
|
|
// Mixin for widgets that need drop down ability.
|
|
|
|
// _buttonNode: [protected] DomNode
|
|
// The button/icon/node to click to display the drop down.
|
|
// Can be set via a data-dojo-attach-point assignment.
|
|
// If missing, then either focusNode or domNode (if focusNode is also missing) will be used.
|
|
_buttonNode: null,
|
|
|
|
// _arrowWrapperNode: [protected] DomNode
|
|
// Will set CSS class dijitUpArrow, dijitDownArrow, dijitRightArrow etc. on this node depending
|
|
// on where the drop down is set to be positioned.
|
|
// Can be set via a data-dojo-attach-point assignment.
|
|
// If missing, then _buttonNode will be used.
|
|
_arrowWrapperNode: null,
|
|
|
|
// _popupStateNode: [protected] DomNode
|
|
// The node to set the aria-expanded class on.
|
|
// Also sets popupActive class but that will be removed in 2.0.
|
|
// Can be set via a data-dojo-attach-point assignment.
|
|
// If missing, then focusNode or _buttonNode (if focusNode is missing) will be used.
|
|
_popupStateNode: null,
|
|
|
|
// _aroundNode: [protected] DomNode
|
|
// The node to display the popup around.
|
|
// Can be set via a data-dojo-attach-point assignment.
|
|
// If missing, then domNode will be used.
|
|
_aroundNode: null,
|
|
|
|
// dropDown: [protected] Widget
|
|
// The widget to display as a popup. This widget *must* be
|
|
// defined before the startup function is called.
|
|
dropDown: null,
|
|
|
|
// autoWidth: [protected] Boolean
|
|
// Set to true to make the drop down at least as wide as this
|
|
// widget. Set to false if the drop down should just be its
|
|
// default width.
|
|
autoWidth: true,
|
|
|
|
// forceWidth: [protected] Boolean
|
|
// Set to true to make the drop down exactly as wide as this
|
|
// widget. Overrides autoWidth.
|
|
forceWidth: false,
|
|
|
|
// maxHeight: [protected] Integer
|
|
// The max height for our dropdown.
|
|
// Any dropdown taller than this will have scrollbars.
|
|
// Set to 0 for no max height, or -1 to limit height to available space in viewport
|
|
maxHeight: -1,
|
|
|
|
// dropDownPosition: [const] String[]
|
|
// This variable controls the position of the drop down.
|
|
// It's an array of strings with the following values:
|
|
//
|
|
// - before: places drop down to the left of the target node/widget, or to the right in
|
|
// the case of RTL scripts like Hebrew and Arabic
|
|
// - after: places drop down to the right of the target node/widget, or to the left in
|
|
// the case of RTL scripts like Hebrew and Arabic
|
|
// - above: drop down goes above target node
|
|
// - below: drop down goes below target node
|
|
//
|
|
// The list is positions is tried, in order, until a position is found where the drop down fits
|
|
// within the viewport.
|
|
//
|
|
dropDownPosition: ["below", "above"],
|
|
|
|
// _stopClickEvents: Boolean
|
|
// When set to false, the click events will not be stopped, in
|
|
// case you want to use them in your subclass
|
|
_stopClickEvents: true,
|
|
|
|
_onDropDownMouseDown: function(/*Event*/ e){
|
|
// summary:
|
|
// Callback when the user mousedown/touchstart on the arrow icon.
|
|
|
|
if(this.disabled || this.readOnly){
|
|
return;
|
|
}
|
|
|
|
// Prevent default to stop things like text selection, but don't stop propagation, so that:
|
|
// 1. TimeTextBox etc. can focus the <input> on mousedown
|
|
// 2. dropDownButtonActive class applied by _CssStateMixin (on button depress)
|
|
// 3. user defined onMouseDown handler fires
|
|
//
|
|
// Also, don't call preventDefault() on MSPointerDown event (on IE10) because that prevents the button
|
|
// from getting focus, and then the focus manager doesn't know what's going on (#17262)
|
|
if(e.type != "MSPointerDown" && e.type != "pointerdown"){
|
|
e.preventDefault();
|
|
}
|
|
|
|
this.own(on.once(this.ownerDocument, touch.release, lang.hitch(this, "_onDropDownMouseUp")));
|
|
|
|
this.toggleDropDown();
|
|
},
|
|
|
|
_onDropDownMouseUp: function(/*Event?*/ e){
|
|
// summary:
|
|
// Callback on mouseup/touchend after mousedown/touchstart on the arrow icon.
|
|
// Note that this function is called regardless of what node the event occurred on (but only after
|
|
// a mousedown/touchstart on the arrow).
|
|
//
|
|
// If the drop down is a simple menu and the cursor is over the menu, we execute it, otherwise, we focus our
|
|
// drop down widget. If the event is missing, then we are not
|
|
// a mouseup event.
|
|
//
|
|
// This is useful for the common mouse movement pattern
|
|
// with native browser `<select>` nodes:
|
|
//
|
|
// 1. mouse down on the select node (probably on the arrow)
|
|
// 2. move mouse to a menu item while holding down the mouse button
|
|
// 3. mouse up. this selects the menu item as though the user had clicked it.
|
|
|
|
var dropDown = this.dropDown, overMenu = false;
|
|
|
|
if(e && this._opened){
|
|
// This code deals with the corner-case when the drop down covers the original widget,
|
|
// because it's so large. In that case mouse-up shouldn't select a value from the menu.
|
|
// Find out if our target is somewhere in our dropdown widget,
|
|
// but not over our _buttonNode (the clickable node)
|
|
var c = domGeometry.position(this._buttonNode, true);
|
|
if(!(e.pageX >= c.x && e.pageX <= c.x + c.w) || !(e.pageY >= c.y && e.pageY <= c.y + c.h)){
|
|
var t = e.target;
|
|
while(t && !overMenu){
|
|
if(domClass.contains(t, "dijitPopup")){
|
|
overMenu = true;
|
|
}else{
|
|
t = t.parentNode;
|
|
}
|
|
}
|
|
if(overMenu){
|
|
t = e.target;
|
|
if(dropDown.onItemClick){
|
|
var menuItem;
|
|
while(t && !(menuItem = registry.byNode(t))){
|
|
t = t.parentNode;
|
|
}
|
|
if(menuItem && menuItem.onClick && menuItem.getParent){
|
|
menuItem.getParent().onItemClick(menuItem, e);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if(this._opened){
|
|
// Focus the dropdown widget unless it's a menu (in which case autoFocus is set to false).
|
|
// Even if it's a menu, we need to focus it if this is a fake mouse event caused by the user typing
|
|
// SPACE/ENTER while using JAWS. Jaws converts the SPACE/ENTER key into mousedown/mouseup events.
|
|
// If this.hovering is false then it's presumably actually a keyboard event.
|
|
if(dropDown.focus && (dropDown.autoFocus !== false || (e.type == "mouseup" && !this.hovering))){
|
|
// Do it on a delay so that we don't steal back focus from the dropdown.
|
|
this._focusDropDownTimer = this.defer(function(){
|
|
dropDown.focus();
|
|
delete this._focusDropDownTimer;
|
|
});
|
|
}
|
|
}else{
|
|
// The drop down arrow icon probably can't receive focus, but widget itself should get focus.
|
|
// defer() needed to make it work on IE (test DateTextBox)
|
|
if(this.focus){
|
|
this.defer("focus");
|
|
}
|
|
}
|
|
},
|
|
|
|
_onDropDownClick: function(/*Event*/ e){
|
|
// The drop down was already opened on mousedown/keydown; just need to stop the event
|
|
if(this._stopClickEvents){
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
|
|
buildRendering: function(){
|
|
this.inherited(arguments);
|
|
|
|
this._buttonNode = this._buttonNode || this.focusNode || this.domNode;
|
|
this._popupStateNode = this._popupStateNode || this.focusNode || this._buttonNode;
|
|
|
|
// Add a class to the "dijitDownArrowButton" type class to _buttonNode so theme can set direction of arrow
|
|
// based on where drop down will normally appear
|
|
var defaultPos = {
|
|
"after": this.isLeftToRight() ? "Right" : "Left",
|
|
"before": this.isLeftToRight() ? "Left" : "Right",
|
|
"above": "Up",
|
|
"below": "Down",
|
|
"left": "Left",
|
|
"right": "Right"
|
|
}[this.dropDownPosition[0]] || this.dropDownPosition[0] || "Down";
|
|
domClass.add(this._arrowWrapperNode || this._buttonNode, "dijit" + defaultPos + "ArrowButton");
|
|
},
|
|
|
|
postCreate: function(){
|
|
// summary:
|
|
// set up nodes and connect our mouse and keyboard events
|
|
|
|
this.inherited(arguments);
|
|
|
|
var keyboardEventNode = this.focusNode || this.domNode;
|
|
this.own(
|
|
on(this._buttonNode, touch.press, lang.hitch(this, "_onDropDownMouseDown")),
|
|
on(this._buttonNode, "click", lang.hitch(this, "_onDropDownClick")),
|
|
on(keyboardEventNode, "keydown", lang.hitch(this, "_onKey")),
|
|
on(keyboardEventNode, "keyup", lang.hitch(this, "_onKeyUp"))
|
|
);
|
|
},
|
|
|
|
destroy: function(){
|
|
// If dropdown is open, close it, to avoid leaving dijit/focus in a strange state.
|
|
// Put focus back on me to avoid the focused node getting destroyed, which flummoxes IE.
|
|
if(this._opened){
|
|
this.closeDropDown(true);
|
|
}
|
|
|
|
if(this.dropDown){
|
|
// Destroy the drop down, unless it's already been destroyed. This can happen because
|
|
// the drop down is a direct child of <body> even though it's logically my child.
|
|
if(!this.dropDown._destroyed){
|
|
this.dropDown.destroyRecursive();
|
|
}
|
|
delete this.dropDown;
|
|
}
|
|
this.inherited(arguments);
|
|
},
|
|
|
|
_onKey: function(/*Event*/ e){
|
|
// summary:
|
|
// Callback when the user presses a key while focused on the button node
|
|
|
|
if(this.disabled || this.readOnly){
|
|
return;
|
|
}
|
|
var d = this.dropDown, target = e.target;
|
|
if(d && this._opened && d.handleKey){
|
|
if(d.handleKey(e) === false){
|
|
/* false return code means that the drop down handled the key */
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
if(d && this._opened && e.keyCode == keys.ESCAPE){
|
|
this.closeDropDown();
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}else if(!this._opened &&
|
|
(e.keyCode == keys.DOWN_ARROW ||
|
|
// ignore unmodified SPACE if _KeyNavMixin has active searching in progress
|
|
( (e.keyCode == keys.ENTER || (e.keyCode == keys.SPACE && (!this._searchTimer || (e.ctrlKey || e.altKey || e.metaKey)))) &&
|
|
//ignore enter and space if the event is for a text input
|
|
((target.tagName || "").toLowerCase() !== 'input' ||
|
|
(target.type && target.type.toLowerCase() !== 'text'))))){
|
|
// Toggle the drop down, but wait until keyup so that the drop down doesn't
|
|
// get a stray keyup event, or in the case of key-repeat (because user held
|
|
// down key for too long), stray keydown events
|
|
this._toggleOnKeyUp = true;
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
|
|
_onKeyUp: function(){
|
|
if(this._toggleOnKeyUp){
|
|
delete this._toggleOnKeyUp;
|
|
this.toggleDropDown();
|
|
var d = this.dropDown; // drop down may not exist until toggleDropDown() call
|
|
if(d && d.focus){
|
|
this.defer(lang.hitch(d, "focus"), 1);
|
|
}
|
|
}
|
|
},
|
|
|
|
_onBlur: function(){
|
|
// summary:
|
|
// Called magically when focus has shifted away from this widget and it's dropdown
|
|
|
|
// Close dropdown but don't focus my <input>. User may have focused somewhere else (ex: clicked another
|
|
// input), and even if they just clicked a blank area of the screen, focusing my <input> will unwantedly
|
|
// popup the keyboard on mobile.
|
|
this.closeDropDown(false);
|
|
|
|
this.inherited(arguments);
|
|
},
|
|
|
|
isLoaded: function(){
|
|
// summary:
|
|
// Returns true if the dropdown exists and it's data is loaded. This can
|
|
// be overridden in order to force a call to loadDropDown().
|
|
// tags:
|
|
// protected
|
|
|
|
return true;
|
|
},
|
|
|
|
loadDropDown: function(/*Function*/ loadCallback){
|
|
// summary:
|
|
// Creates the drop down if it doesn't exist, loads the data
|
|
// if there's an href and it hasn't been loaded yet, and then calls
|
|
// the given callback.
|
|
// tags:
|
|
// protected
|
|
|
|
// TODO: for 2.0, change API to return a Deferred, instead of calling loadCallback?
|
|
loadCallback();
|
|
},
|
|
|
|
loadAndOpenDropDown: function(){
|
|
// summary:
|
|
// Creates the drop down if it doesn't exist, loads the data
|
|
// if there's an href and it hasn't been loaded yet, and
|
|
// then opens the drop down. This is basically a callback when the
|
|
// user presses the down arrow button to open the drop down.
|
|
// returns: Deferred
|
|
// Deferred for the drop down widget that
|
|
// fires when drop down is created and loaded
|
|
// tags:
|
|
// protected
|
|
var d = new Deferred(),
|
|
afterLoad = lang.hitch(this, function(){
|
|
this.openDropDown();
|
|
d.resolve(this.dropDown);
|
|
});
|
|
if(!this.isLoaded()){
|
|
this.loadDropDown(afterLoad);
|
|
}else{
|
|
afterLoad();
|
|
}
|
|
return d;
|
|
},
|
|
|
|
toggleDropDown: function(){
|
|
// summary:
|
|
// Callback when the user presses the down arrow button or presses
|
|
// the down arrow key to open/close the drop down.
|
|
// Toggle the drop-down widget; if it is up, close it, if not, open it
|
|
// tags:
|
|
// protected
|
|
|
|
if(this.disabled || this.readOnly){
|
|
return;
|
|
}
|
|
if(!this._opened){
|
|
this.loadAndOpenDropDown();
|
|
}else{
|
|
this.closeDropDown(true); // refocus button to avoid hiding node w/focus
|
|
}
|
|
},
|
|
|
|
openDropDown: function(){
|
|
// summary:
|
|
// Opens the dropdown for this widget. To be called only when this.dropDown
|
|
// has been created and is ready to display (ie, it's data is loaded).
|
|
// returns:
|
|
// return value of dijit/popup.open()
|
|
// tags:
|
|
// protected
|
|
|
|
var dropDown = this.dropDown,
|
|
ddNode = dropDown.domNode,
|
|
aroundNode = this._aroundNode || this.domNode,
|
|
self = this;
|
|
|
|
var retVal = popup.open({
|
|
parent: this,
|
|
popup: dropDown,
|
|
around: aroundNode,
|
|
orient: this.dropDownPosition,
|
|
maxHeight: this.maxHeight,
|
|
onExecute: function(){
|
|
self.closeDropDown(true);
|
|
},
|
|
onCancel: function(){
|
|
self.closeDropDown(true);
|
|
},
|
|
onClose: function(){
|
|
domAttr.set(self._popupStateNode, "popupActive", false);
|
|
domClass.remove(self._popupStateNode, "dijitHasDropDownOpen");
|
|
self._set("_opened", false); // use set() because _CssStateMixin is watching
|
|
}
|
|
});
|
|
|
|
// Set width of drop down if necessary, so that dropdown width + width of scrollbar (from popup wrapper)
|
|
// matches width of aroundNode
|
|
if(this.forceWidth || (this.autoWidth && aroundNode.offsetWidth > dropDown._popupWrapper.offsetWidth)){
|
|
var widthAdjust = aroundNode.offsetWidth - dropDown._popupWrapper.offsetWidth;
|
|
var resizeArgs = {
|
|
w: dropDown.domNode.offsetWidth + widthAdjust
|
|
};
|
|
if(lang.isFunction(dropDown.resize)){
|
|
dropDown.resize(resizeArgs);
|
|
}else{
|
|
domGeometry.setMarginBox(ddNode, resizeArgs);
|
|
}
|
|
|
|
// If dropdown is right-aligned then compensate for width change by changing horizontal position
|
|
if(retVal.corner[1] == "R"){
|
|
dropDown._popupWrapper.style.left =
|
|
(dropDown._popupWrapper.style.left.replace("px", "") - widthAdjust) + "px";
|
|
}
|
|
}
|
|
|
|
domAttr.set(this._popupStateNode, "popupActive", "true");
|
|
domClass.add(this._popupStateNode, "dijitHasDropDownOpen");
|
|
this._set("_opened", true); // use set() because _CssStateMixin is watching
|
|
|
|
this._popupStateNode.setAttribute("aria-expanded", "true");
|
|
this._popupStateNode.setAttribute("aria-owns", dropDown.id);
|
|
|
|
// Set aria-labelledby on dropdown if it's not already set to something more meaningful
|
|
if(ddNode.getAttribute("role") !== "presentation" && !ddNode.getAttribute("aria-labelledby")){
|
|
ddNode.setAttribute("aria-labelledby", this.id);
|
|
}
|
|
|
|
return retVal;
|
|
},
|
|
|
|
closeDropDown: function(/*Boolean*/ focus){
|
|
// summary:
|
|
// Closes the drop down on this widget
|
|
// focus:
|
|
// If true, refocuses the button widget
|
|
// tags:
|
|
// protected
|
|
|
|
if(this._focusDropDownTimer){
|
|
this._focusDropDownTimer.remove();
|
|
delete this._focusDropDownTimer;
|
|
}
|
|
|
|
if(this._opened){
|
|
this._popupStateNode.setAttribute("aria-expanded", "false");
|
|
if(focus && this.focus){
|
|
this.focus();
|
|
}
|
|
popup.close(this.dropDown);
|
|
this._opened = false;
|
|
}
|
|
}
|
|
|
|
});
|
|
});
|