UNPKG

dijit

Version:

Dijit provides a complete collection of user interface controls based on Dojo, giving you the power to create web applications that are highly optimized for usability, performance, internationalization, accessibility, but above all deliver an incredible u

473 lines (416 loc) 16.4 kB
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; } } }); });