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.
472 lines
16 KiB
472 lines
16 KiB
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.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 |
|
}; |
|
this._origStyle = ddNode.style.cssText; |
|
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; |
|
} |
|
|
|
if(this._origStyle){ |
|
this.dropDown.domNode.style.cssText = this._origStyle; |
|
delete this._origStyle; |
|
} |
|
} |
|
}); |
|
});
|
|
|