source: Dev/trunk/src/client/dijit/_HasDropDown.js @ 536

Last change on this file since 536 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 15.8 KB
Line 
1define([
2        "dojo/_base/declare", // declare
3        "dojo/_base/Deferred",
4        "dojo/dom", // dom.isDescendant
5        "dojo/dom-attr", // domAttr.set
6        "dojo/dom-class", // domClass.add domClass.contains domClass.remove
7        "dojo/dom-geometry", // domGeometry.marginBox domGeometry.position
8        "dojo/dom-style", // domStyle.set
9        "dojo/has", // has("touch")
10        "dojo/keys", // keys.DOWN_ARROW keys.ENTER keys.ESCAPE
11        "dojo/_base/lang", // lang.hitch lang.isFunction
12        "dojo/on",
13        "dojo/touch",
14        "./registry", // registry.byNode()
15        "./focus",
16        "./popup",
17        "./_FocusMixin"
18], function(declare, Deferred, dom, domAttr, domClass, domGeometry, domStyle, has, keys, lang, on, touch,
19                        registry, focus, popup, _FocusMixin){
20
21
22        // module:
23        //              dijit/_HasDropDown
24
25        return declare("dijit._HasDropDown", _FocusMixin, {
26                // summary:
27                //              Mixin for widgets that need drop down ability.
28
29                // _buttonNode: [protected] DomNode
30                //              The button/icon/node to click to display the drop down.
31                //              Can be set via a data-dojo-attach-point assignment.
32                //              If missing, then either focusNode or domNode (if focusNode is also missing) will be used.
33                _buttonNode: null,
34
35                // _arrowWrapperNode: [protected] DomNode
36                //              Will set CSS class dijitUpArrow, dijitDownArrow, dijitRightArrow etc. on this node depending
37                //              on where the drop down is set to be positioned.
38                //              Can be set via a data-dojo-attach-point assignment.
39                //              If missing, then _buttonNode will be used.
40                _arrowWrapperNode: null,
41
42                // _popupStateNode: [protected] DomNode
43                //              The node to set the aria-expanded class on.
44                //              Also sets popupActive class but that will be removed in 2.0.
45                //              Can be set via a data-dojo-attach-point assignment.
46                //              If missing, then focusNode or _buttonNode (if focusNode is missing) will be used.
47                _popupStateNode: null,
48
49                // _aroundNode: [protected] DomNode
50                //              The node to display the popup around.
51                //              Can be set via a data-dojo-attach-point assignment.
52                //              If missing, then domNode will be used.
53                _aroundNode: null,
54
55                // dropDown: [protected] Widget
56                //              The widget to display as a popup.  This widget *must* be
57                //              defined before the startup function is called.
58                dropDown: null,
59
60                // autoWidth: [protected] Boolean
61                //              Set to true to make the drop down at least as wide as this
62                //              widget.  Set to false if the drop down should just be its
63                //              default width.
64                autoWidth: true,
65
66                // forceWidth: [protected] Boolean
67                //              Set to true to make the drop down exactly as wide as this
68                //              widget.  Overrides autoWidth.
69                forceWidth: false,
70
71                // maxHeight: [protected] Integer
72                //              The max height for our dropdown.
73                //              Any dropdown taller than this will have scrollbars.
74                //              Set to 0 for no max height, or -1 to limit height to available space in viewport
75                maxHeight: -1,
76
77                // dropDownPosition: [const] String[]
78                //              This variable controls the position of the drop down.
79                //              It's an array of strings with the following values:
80                //
81                //              - before: places drop down to the left of the target node/widget, or to the right in
82                //                the case of RTL scripts like Hebrew and Arabic
83                //              - after: places drop down to the right of the target node/widget, or to the left in
84                //                the case of RTL scripts like Hebrew and Arabic
85                //              - above: drop down goes above target node
86                //              - below: drop down goes below target node
87                //
88                //              The list is positions is tried, in order, until a position is found where the drop down fits
89                //              within the viewport.
90                //
91                dropDownPosition: ["below", "above"],
92
93                // _stopClickEvents: Boolean
94                //              When set to false, the click events will not be stopped, in
95                //              case you want to use them in your subclass
96                _stopClickEvents: true,
97
98                _onDropDownMouseDown: function(/*Event*/ e){
99                        // summary:
100                        //              Callback when the user mousedown/touchstart on the arrow icon.
101
102                        if(this.disabled || this.readOnly){
103                                return;
104                        }
105
106                        // Prevent default to stop things like text selection, but don't stop propagation, so that:
107                        //              1. TimeTextBox etc. can focus the <input> on mousedown
108                        //              2. dropDownButtonActive class applied by _CssStateMixin (on button depress)
109                        //              3. user defined onMouseDown handler fires
110                        //
111                        // Also, don't call preventDefault() on MSPointerDown event (on IE10) because that prevents the button
112                        // from getting focus, and then the focus manager doesn't know what's going on (#17262)
113                        if(e.type != "MSPointerDown" && e.type != "pointerdown"){
114                                e.preventDefault();
115                        }
116
117                        this._docHandler = this.own(on(this.ownerDocument, touch.release, lang.hitch(this, "_onDropDownMouseUp")))[0];
118
119                        this.toggleDropDown();
120                },
121
122                _onDropDownMouseUp: function(/*Event?*/ e){
123                        // summary:
124                        //              Callback on mouseup/touchend after mousedown/touchstart on the arrow icon.
125                        //              Note that this function is called regardless of what node the event occurred on (but only after
126                        //              a mousedown/touchstart on the arrow).
127                        //
128                        //              If the drop down is a simple menu and the cursor is over the menu, we execute it, otherwise, we focus our
129                        //              drop down widget.  If the event is missing, then we are not
130                        //              a mouseup event.
131                        //
132                        //              This is useful for the common mouse movement pattern
133                        //              with native browser `<select>` nodes:
134                        //
135                        //              1. mouse down on the select node (probably on the arrow)
136                        //              2. move mouse to a menu item while holding down the mouse button
137                        //              3. mouse up.  this selects the menu item as though the user had clicked it.
138
139                        if(e && this._docHandler){
140                                this._docHandler.remove();
141                                this._docHandler = null;
142                        }
143                        var dropDown = this.dropDown, overMenu = false;
144
145                        if(e && this._opened){
146                                // This code deals with the corner-case when the drop down covers the original widget,
147                                // because it's so large.  In that case mouse-up shouldn't select a value from the menu.
148                                // Find out if our target is somewhere in our dropdown widget,
149                                // but not over our _buttonNode (the clickable node)
150                                var c = domGeometry.position(this._buttonNode, true);
151                                if(!(e.pageX >= c.x && e.pageX <= c.x + c.w) || !(e.pageY >= c.y && e.pageY <= c.y + c.h)){
152                                        var t = e.target;
153                                        while(t && !overMenu){
154                                                if(domClass.contains(t, "dijitPopup")){
155                                                        overMenu = true;
156                                                }else{
157                                                        t = t.parentNode;
158                                                }
159                                        }
160                                        if(overMenu){
161                                                t = e.target;
162                                                if(dropDown.onItemClick){
163                                                        var menuItem;
164                                                        while(t && !(menuItem = registry.byNode(t))){
165                                                                t = t.parentNode;
166                                                        }
167                                                        if(menuItem && menuItem.onClick && menuItem.getParent){
168                                                                menuItem.getParent().onItemClick(menuItem, e);
169                                                        }
170                                                }
171                                                return;
172                                        }
173                                }
174                        }
175                        if(this._opened){
176                                // Focus the dropdown widget unless it's a menu (in which case autoFocus is set to false).
177                                // Even if it's a menu, we need to focus it if this is a fake mouse event caused by the user typing
178                                // SPACE/ENTER while using JAWS.  Jaws converts the SPACE/ENTER key into mousedown/mouseup events.
179                                // If this.hovering is false then it's presumably actually a keyboard event.
180                                if(dropDown.focus && (dropDown.autoFocus !== false || (e.type == "mouseup" && !this.hovering))){
181                                        // Do it on a delay so that we don't steal back focus from the dropdown.
182                                        this._focusDropDownTimer = this.defer(function(){
183                                                dropDown.focus();
184                                                delete this._focusDropDownTimer;
185                                        });
186                                }
187                        }else{
188                                // The drop down arrow icon probably can't receive focus, but widget itself should get focus.
189                                // defer() needed to make it work on IE (test DateTextBox)
190                                if(this.focus){
191                                        this.defer("focus");
192                                }
193                        }
194                },
195
196                _onDropDownClick: function(/*Event*/ e){
197                        // The drop down was already opened on mousedown/keydown; just need to stop the event
198                        if(this._stopClickEvents){
199                                e.stopPropagation();
200                                e.preventDefault();
201                        }
202                },
203
204                buildRendering: function(){
205                        this.inherited(arguments);
206
207                        this._buttonNode = this._buttonNode || this.focusNode || this.domNode;
208                        this._popupStateNode = this._popupStateNode || this.focusNode || this._buttonNode;
209
210                        // Add a class to the "dijitDownArrowButton" type class to _buttonNode so theme can set direction of arrow
211                        // based on where drop down will normally appear
212                        var defaultPos = {
213                                "after": this.isLeftToRight() ? "Right" : "Left",
214                                "before": this.isLeftToRight() ? "Left" : "Right",
215                                "above": "Up",
216                                "below": "Down",
217                                "left": "Left",
218                                "right": "Right"
219                        }[this.dropDownPosition[0]] || this.dropDownPosition[0] || "Down";
220                        domClass.add(this._arrowWrapperNode || this._buttonNode, "dijit" + defaultPos + "ArrowButton");
221                },
222
223                postCreate: function(){
224                        // summary:
225                        //              set up nodes and connect our mouse and keyboard events
226
227                        this.inherited(arguments);
228
229                        var keyboardEventNode = this.focusNode || this.domNode;
230                        this.own(
231                                on(this._buttonNode, touch.press, lang.hitch(this, "_onDropDownMouseDown")),
232                                on(this._buttonNode, "click", lang.hitch(this, "_onDropDownClick")),
233                                on(keyboardEventNode, "keydown", lang.hitch(this, "_onKey")),
234                                on(keyboardEventNode, "keyup", lang.hitch(this, "_onKeyUp"))
235                        );
236                },
237
238                destroy: function(){
239                        if(this.dropDown){
240                                // Destroy the drop down, unless it's already been destroyed.  This can happen because
241                                // the drop down is a direct child of <body> even though it's logically my child.
242                                if(!this.dropDown._destroyed){
243                                        this.dropDown.destroyRecursive();
244                                }
245                                delete this.dropDown;
246                        }
247                        this.inherited(arguments);
248                },
249
250                _onKey: function(/*Event*/ e){
251                        // summary:
252                        //              Callback when the user presses a key while focused on the button node
253
254                        if(this.disabled || this.readOnly){
255                                return;
256                        }
257                        var d = this.dropDown, target = e.target;
258                        if(d && this._opened && d.handleKey){
259                                if(d.handleKey(e) === false){
260                                        /* false return code means that the drop down handled the key */
261                                        e.stopPropagation();
262                                        e.preventDefault();
263                                        return;
264                                }
265                        }
266                        if(d && this._opened && e.keyCode == keys.ESCAPE){
267                                this.closeDropDown();
268                                e.stopPropagation();
269                                e.preventDefault();
270                        }else if(!this._opened &&
271                                (e.keyCode == keys.DOWN_ARROW ||
272                                        // ignore unmodified SPACE if _KeyNavMixin has active searching in progress
273                                        ( (e.keyCode == keys.ENTER || (e.keyCode == keys.SPACE && (!this._searchTimer || (e.ctrlKey || e.altKey || e.metaKey)))) &&
274                                                //ignore enter and space if the event is for a text input
275                                                ((target.tagName || "").toLowerCase() !== 'input' ||
276                                                        (target.type && target.type.toLowerCase() !== 'text'))))){
277                                // Toggle the drop down, but wait until keyup so that the drop down doesn't
278                                // get a stray keyup event, or in the case of key-repeat (because user held
279                                // down key for too long), stray keydown events
280                                this._toggleOnKeyUp = true;
281                                e.stopPropagation();
282                                e.preventDefault();
283                        }
284                },
285
286                _onKeyUp: function(){
287                        if(this._toggleOnKeyUp){
288                                delete this._toggleOnKeyUp;
289                                this.toggleDropDown();
290                                var d = this.dropDown;  // drop down may not exist until toggleDropDown() call
291                                if(d && d.focus){
292                                        this.defer(lang.hitch(d, "focus"), 1);
293                                }
294                        }
295                },
296
297                _onBlur: function(){
298                        // summary:
299                        //              Called magically when focus has shifted away from this widget and it's dropdown
300
301                        // Close dropdown but don't focus my <input>.  User may have focused somewhere else (ex: clicked another
302                        // input), and even if they just clicked a blank area of the screen, focusing my <input> will unwantedly
303                        // popup the keyboard on mobile.
304                        this.closeDropDown(false);
305
306                        this.inherited(arguments);
307                },
308
309                isLoaded: function(){
310                        // summary:
311                        //              Returns true if the dropdown exists and it's data is loaded.  This can
312                        //              be overridden in order to force a call to loadDropDown().
313                        // tags:
314                        //              protected
315
316                        return true;
317                },
318
319                loadDropDown: function(/*Function*/ loadCallback){
320                        // summary:
321                        //              Creates the drop down if it doesn't exist, loads the data
322                        //              if there's an href and it hasn't been loaded yet, and then calls
323                        //              the given callback.
324                        // tags:
325                        //              protected
326
327                        // TODO: for 2.0, change API to return a Deferred, instead of calling loadCallback?
328                        loadCallback();
329                },
330
331                loadAndOpenDropDown: function(){
332                        // summary:
333                        //              Creates the drop down if it doesn't exist, loads the data
334                        //              if there's an href and it hasn't been loaded yet, and
335                        //              then opens the drop down.  This is basically a callback when the
336                        //              user presses the down arrow button to open the drop down.
337                        // returns: Deferred
338                        //              Deferred for the drop down widget that
339                        //              fires when drop down is created and loaded
340                        // tags:
341                        //              protected
342                        var d = new Deferred(),
343                                afterLoad = lang.hitch(this, function(){
344                                        this.openDropDown();
345                                        d.resolve(this.dropDown);
346                                });
347                        if(!this.isLoaded()){
348                                this.loadDropDown(afterLoad);
349                        }else{
350                                afterLoad();
351                        }
352                        return d;
353                },
354
355                toggleDropDown: function(){
356                        // summary:
357                        //              Callback when the user presses the down arrow button or presses
358                        //              the down arrow key to open/close the drop down.
359                        //              Toggle the drop-down widget; if it is up, close it, if not, open it
360                        // tags:
361                        //              protected
362
363                        if(this.disabled || this.readOnly){
364                                return;
365                        }
366                        if(!this._opened){
367                                this.loadAndOpenDropDown();
368                        }else{
369                                this.closeDropDown(true);       // refocus button to avoid hiding node w/focus
370                        }
371                },
372
373                openDropDown: function(){
374                        // summary:
375                        //              Opens the dropdown for this widget.   To be called only when this.dropDown
376                        //              has been created and is ready to display (ie, it's data is loaded).
377                        // returns:
378                        //              return value of dijit/popup.open()
379                        // tags:
380                        //              protected
381
382                        var dropDown = this.dropDown,
383                                ddNode = dropDown.domNode,
384                                aroundNode = this._aroundNode || this.domNode,
385                                self = this;
386
387                        var retVal = popup.open({
388                                parent: this,
389                                popup: dropDown,
390                                around: aroundNode,
391                                orient: this.dropDownPosition,
392                                maxHeight: this.maxHeight,
393                                onExecute: function(){
394                                        self.closeDropDown(true);
395                                },
396                                onCancel: function(){
397                                        self.closeDropDown(true);
398                                },
399                                onClose: function(){
400                                        domAttr.set(self._popupStateNode, "popupActive", false);
401                                        domClass.remove(self._popupStateNode, "dijitHasDropDownOpen");
402                                        self._set("_opened", false);    // use set() because _CssStateMixin is watching
403                                }
404                        });
405
406                        // Set width of drop down if necessary, so that dropdown width + width of scrollbar (from popup wrapper)
407                        // matches width of aroundNode
408                        if(this.forceWidth || (this.autoWidth && aroundNode.offsetWidth > dropDown._popupWrapper.offsetWidth)){
409                                var widthAdjust = aroundNode.offsetWidth - dropDown._popupWrapper.offsetWidth;
410                                var resizeArgs = {
411                                        w: dropDown.domNode.offsetWidth + widthAdjust
412                                };
413                                if(lang.isFunction(dropDown.resize)){
414                                        dropDown.resize(resizeArgs);
415                                }else{
416                                        domGeometry.setMarginBox(ddNode, resizeArgs);
417                                }
418
419                                // If dropdown is right-aligned then compensate for width change by changing horizontal position
420                                if(retVal.corner[1] == "R"){
421                                        dropDown._popupWrapper.style.left =
422                                                (dropDown._popupWrapper.style.left.replace("px", "") - widthAdjust) + "px";
423                                }
424                        }
425
426                        domAttr.set(this._popupStateNode, "popupActive", "true");
427                        domClass.add(this._popupStateNode, "dijitHasDropDownOpen");
428                        this._set("_opened", true);     // use set() because _CssStateMixin is watching
429
430                        this._popupStateNode.setAttribute("aria-expanded", "true");
431                        this._popupStateNode.setAttribute("aria-owns", dropDown.id);
432
433                        // Set aria-labelledby on dropdown if it's not already set to something more meaningful
434                        if(ddNode.getAttribute("role") !== "presentation" && !ddNode.getAttribute("aria-labelledby")){
435                                ddNode.setAttribute("aria-labelledby", this.id);
436                        }
437
438                        return retVal;
439                },
440
441                closeDropDown: function(/*Boolean*/ focus){
442                        // summary:
443                        //              Closes the drop down on this widget
444                        // focus:
445                        //              If true, refocuses the button widget
446                        // tags:
447                        //              protected
448
449                        if(this._focusDropDownTimer){
450                                this._focusDropDownTimer.remove();
451                                delete this._focusDropDownTimer;
452                        }
453
454                        if(this._opened){
455                                this._popupStateNode.setAttribute("aria-expanded", "false");
456                                if(focus){
457                                        this.focus();
458                                }
459                                popup.close(this.dropDown);
460                                this._opened = false;
461                        }
462                }
463
464        });
465});
Note: See TracBrowser for help on using the repository browser.