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 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 `. User may have focused somewhere else (ex: clicked another // input), and even if they just clicked a blank area of the screen, focusing my 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; } } }); });