define("dijit/_HasDropDown", [ "dojo/_base/declare", // declare "dojo/_base/Deferred", "dojo/_base/event", // event.stop "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", "dojo/keys", // keys.DOWN_ARROW keys.ENTER keys.ESCAPE "dojo/_base/lang", // lang.hitch lang.isFunction "dojo/touch", "dojo/_base/window", // win.doc "dojo/window", // winUtils.getBox "./registry", // registry.byNode() "./focus", "./popup", "./_FocusMixin" ], function(declare, Deferred, event,dom, domAttr, domClass, domGeometry, domStyle, has, keys, lang, touch, win, winUtils, registry, focus, popup, _FocusMixin){ /*===== var _FocusMixin = dijit._FocusMixin; =====*/ // module: // dijit/_HasDropDown // summary: // Mixin for widgets that need drop down ability. 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 popupActive class on. // 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: 0, // 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 subwidget _stopClickEvents: true, _onDropDownMouseDown: function(/*Event*/ e){ // summary: // Callback when the user mousedown's on the arrow icon if(this.disabled || this.readOnly){ return; } // Prevent default to stop things like text selection, but don't stop propogation, so that: // 1. TimeTextBox etc. can focusthe on mousedown // 2. dropDownButtonActive class applied by _CssStateMixin (on button depress) // 3. user defined onMouseDown handler fires e.preventDefault(); this._docHandler = this.connect(win.doc, touch.release, "_onDropDownMouseUp"); this.toggleDropDown(); }, _onDropDownMouseUp: function(/*Event?*/ e){ // summary: // Callback when the user lifts their mouse after mouse down on the arrow icon. // If the drop down is a simple menu and the mouse 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 and doesn't // generate touchstart/touchend events. Pretend we just got a mouse down / mouse up. // The if(has("ios") is necessary since IE and desktop safari get spurious onclick events // when there are nested tables (specifically, clicking on a table that holds a dijit.form.Select, // but not on the Select itself, causes an onclick event on the Select) this._onDropDownMouseDown(e); this._onDropDownMouseUp(e); } // The drop down was already opened on mousedown/keydown; just need to call stopEvent(). if(this._stopClickEvents){ event.stop(e); } }, 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 keypress events this.inherited(arguments); this.connect(this._buttonNode, touch.press, "_onDropDownMouseDown"); this.connect(this._buttonNode, "onclick", "_onDropDownClick"); this.connect(this.focusNode, "onkeypress", "_onKey"); this.connect(this.focusNode, "onkeyup", "_onKeyUp"); }, destroy: function(){ 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 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 */ event.stop(e); return; } } if(d && this._opened && e.charOrCode == keys.ESCAPE){ this.closeDropDown(); event.stop(e); }else if(!this._opened && (e.charOrCode == keys.DOWN_ARROW || ( (e.charOrCode == keys.ENTER || e.charOrCode == " ") && //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; event.stop(e); } }, _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){ setTimeout(lang.hitch(d, "focus"), 1); } } }, _onBlur: function(){ // summary: // Called magically when focus has shifted away from this widget and it's dropdown // Don't focus on button if the user has explicitly focused on something else (happens // when user clicks another control causing the current popup to close).. // But if focus is inside of the drop down then reset focus to me, because IE doesn't like // it when you display:none a node with focus. var focusMe = focus.curNode && this.dropDown && dom.isDescendant(focus.curNode, this.dropDown.domNode); this.closeDropDown(focusMe); 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(); } }, 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; // Prepare our popup's height and honor maxHeight if it exists. // TODO: isn't maxHeight dependent on the return value from dijit.popup.open(), // ie, dependent on how much space is available (BK) if(!this._preparedNode){ this._preparedNode = true; // Check if we have explicitly set width and height on the dropdown widget dom node if(ddNode.style.width){ this._explicitDDWidth = true; } if(ddNode.style.height){ this._explicitDDHeight = true; } } // Code for resizing dropdown (height limitation, or increasing width to match my width) if(this.maxHeight || this.forceWidth || this.autoWidth){ var myStyle = { display: "", visibility: "hidden" }; if(!this._explicitDDWidth){ myStyle.width = ""; } if(!this._explicitDDHeight){ myStyle.height = ""; } domStyle.set(ddNode, myStyle); // Figure out maximum height allowed (if there is a height restriction) var maxHeight = this.maxHeight; if(maxHeight == -1){ // limit height to space available in viewport either above or below my domNode // (whichever side has more room) var viewport = winUtils.getBox(), position = domGeometry.position(aroundNode, false); maxHeight = Math.floor(Math.max(position.y, viewport.h - (position.y + position.h))); } // Attach dropDown to DOM and make make visibility:hidden rather than display:none // so we call startup() and also get the size popup.moveOffScreen(dropDown); if(dropDown.startup && !dropDown._started){ dropDown.startup(); // this has to be done after being added to the DOM } // Get size of drop down, and determine if vertical scroll bar needed var mb = domGeometry.getMarginSize(ddNode); var overHeight = (maxHeight && mb.h > maxHeight); domStyle.set(ddNode, { overflowX: "hidden", overflowY: overHeight ? "auto" : "hidden" }); if(overHeight){ mb.h = maxHeight; if("w" in mb){ mb.w += 16; // room for vertical scrollbar } }else{ delete mb.h; } // Adjust dropdown width to match or be larger than my width if(this.forceWidth){ mb.w = aroundNode.offsetWidth; }else if(this.autoWidth){ mb.w = Math.max(mb.w, aroundNode.offsetWidth); }else{ delete mb.w; } // And finally, resize the dropdown to calculated height and width if(lang.isFunction(dropDown.resize)){ dropDown.resize(mb); }else{ domGeometry.setMarginBox(ddNode, mb); } } var retVal = popup.open({ parent: this, popup: dropDown, around: aroundNode, orient: this.dropDownPosition, onExecute: function(){ self.closeDropDown(true); }, onCancel: function(){ self.closeDropDown(true); }, onClose: function(){ domAttr.set(self._popupStateNode, "popupActive", false); domClass.remove(self._popupStateNode, "dijitHasDropDownOpen"); self._opened = false; } }); domAttr.set(this._popupStateNode, "popupActive", "true"); domClass.add(self._popupStateNode, "dijitHasDropDownOpen"); this._opened=true; // TODO: set this.checked and call setStateClass(), to affect button look while drop down is shown 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._opened){ if(focus){ this.focus(); } popup.close(this.dropDown); this._opened = false; } } }); });