source: Dev/branches/rest-dojo-ui/client/dijit/_HasDropDown.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 16.2 KB
Line 
1define([
2        "dojo/_base/declare", // declare
3        "dojo/_base/Deferred",
4        "dojo/_base/event", // event.stop
5        "dojo/dom", // dom.isDescendant
6        "dojo/dom-attr", // domAttr.set
7        "dojo/dom-class", // domClass.add domClass.contains domClass.remove
8        "dojo/dom-geometry", // domGeometry.marginBox domGeometry.position
9        "dojo/dom-style", // domStyle.set
10        "dojo/has",
11        "dojo/keys", // keys.DOWN_ARROW keys.ENTER keys.ESCAPE
12        "dojo/_base/lang", // lang.hitch lang.isFunction
13        "dojo/touch",
14        "dojo/_base/window", // win.doc
15        "dojo/window", // winUtils.getBox
16        "./registry",   // registry.byNode()
17        "./focus",
18        "./popup",
19        "./_FocusMixin"
20], function(declare, Deferred, event,dom, domAttr, domClass, domGeometry, domStyle, has, keys, lang, touch,
21                        win, winUtils, registry, focus, popup, _FocusMixin){
22
23/*=====
24        var _FocusMixin = dijit._FocusMixin;
25=====*/
26
27        // module:
28        //              dijit/_HasDropDown
29        // summary:
30        //              Mixin for widgets that need drop down ability.
31
32        return declare("dijit._HasDropDown", _FocusMixin, {
33                // summary:
34                //              Mixin for widgets that need drop down ability.
35
36                // _buttonNode: [protected] DomNode
37                //              The button/icon/node to click to display the drop down.
38                //              Can be set via a data-dojo-attach-point assignment.
39                //              If missing, then either focusNode or domNode (if focusNode is also missing) will be used.
40                _buttonNode: null,
41
42                // _arrowWrapperNode: [protected] DomNode
43                //              Will set CSS class dijitUpArrow, dijitDownArrow, dijitRightArrow etc. on this node depending
44                //              on where the drop down is set to be positioned.
45                //              Can be set via a data-dojo-attach-point assignment.
46                //              If missing, then _buttonNode will be used.
47                _arrowWrapperNode: null,
48
49                // _popupStateNode: [protected] DomNode
50                //              The node to set the popupActive class on.
51                //              Can be set via a data-dojo-attach-point assignment.
52                //              If missing, then focusNode or _buttonNode (if focusNode is missing) will be used.
53                _popupStateNode: null,
54
55                // _aroundNode: [protected] DomNode
56                //              The node to display the popup around.
57                //              Can be set via a data-dojo-attach-point assignment.
58                //              If missing, then domNode will be used.
59                _aroundNode: null,
60
61                // dropDown: [protected] Widget
62                //              The widget to display as a popup.  This widget *must* be
63                //              defined before the startup function is called.
64                dropDown: null,
65
66                // autoWidth: [protected] Boolean
67                //              Set to true to make the drop down at least as wide as this
68                //              widget.  Set to false if the drop down should just be its
69                //              default width
70                autoWidth: true,
71
72                // forceWidth: [protected] Boolean
73                //              Set to true to make the drop down exactly as wide as this
74                //              widget.  Overrides autoWidth.
75                forceWidth: false,
76
77                // maxHeight: [protected] Integer
78                //              The max height for our dropdown.
79                //              Any dropdown taller than this will have scrollbars.
80                //              Set to 0 for no max height, or -1 to limit height to available space in viewport
81                maxHeight: 0,
82
83                // dropDownPosition: [const] String[]
84                //              This variable controls the position of the drop down.
85                //              It's an array of strings with the following values:
86                //
87                //                      * before: places drop down to the left of the target node/widget, or to the right in
88                //                        the case of RTL scripts like Hebrew and Arabic
89                //                      * after: places drop down to the right of the target node/widget, or to the left in
90                //                        the case of RTL scripts like Hebrew and Arabic
91                //                      * above: drop down goes above target node
92                //                      * below: drop down goes below target node
93                //
94                //              The list is positions is tried, in order, until a position is found where the drop down fits
95                //              within the viewport.
96                //
97                dropDownPosition: ["below","above"],
98
99                // _stopClickEvents: Boolean
100                //              When set to false, the click events will not be stopped, in
101                //              case you want to use them in your subwidget
102                _stopClickEvents: true,
103
104                _onDropDownMouseDown: function(/*Event*/ e){
105                        // summary:
106                        //              Callback when the user mousedown's on the arrow icon
107                        if(this.disabled || this.readOnly){ return; }
108
109                        event.stop(e);
110
111                        this._docHandler = this.connect(win.doc, touch.release, "_onDropDownMouseUp");
112
113                        this.toggleDropDown();
114                },
115
116                _onDropDownMouseUp: function(/*Event?*/ e){
117                        // summary:
118                        //              Callback when the user lifts their mouse after mouse down on the arrow icon.
119                        //              If the drop down is a simple menu and the mouse is over the menu, we execute it, otherwise, we focus our
120                        //              drop down widget.  If the event is missing, then we are not
121                        //              a mouseup event.
122                        //
123                        //              This is useful for the common mouse movement pattern
124                        //              with native browser <select> nodes:
125                        //                      1. mouse down on the select node (probably on the arrow)
126                        //                      2. move mouse to a menu item while holding down the mouse button
127                        //                      3. mouse up.  this selects the menu item as though the user had clicked it.
128                        if(e && this._docHandler){
129                                this.disconnect(this._docHandler);
130                        }
131                        var dropDown = this.dropDown, overMenu = false;
132
133                        if(e && this._opened){
134                                // This code deals with the corner-case when the drop down covers the original widget,
135                                // because it's so large.  In that case mouse-up shouldn't select a value from the menu.
136                                // Find out if our target is somewhere in our dropdown widget,
137                                // but not over our _buttonNode (the clickable node)
138                                var c = domGeometry.position(this._buttonNode, true);
139                                if(!(e.pageX >= c.x && e.pageX <= c.x + c.w) ||
140                                        !(e.pageY >= c.y && e.pageY <= c.y + c.h)){
141                                        var t = e.target;
142                                        while(t && !overMenu){
143                                                if(domClass.contains(t, "dijitPopup")){
144                                                        overMenu = true;
145                                                }else{
146                                                        t = t.parentNode;
147                                                }
148                                        }
149                                        if(overMenu){
150                                                t = e.target;
151                                                if(dropDown.onItemClick){
152                                                        var menuItem;
153                                                        while(t && !(menuItem = registry.byNode(t))){
154                                                                t = t.parentNode;
155                                                        }
156                                                        if(menuItem && menuItem.onClick && menuItem.getParent){
157                                                                menuItem.getParent().onItemClick(menuItem, e);
158                                                        }
159                                                }
160                                                return;
161                                        }
162                                }
163                        }
164                        if(this._opened){
165                                if(dropDown.focus && dropDown.autoFocus !== false){
166                                        // Focus the dropdown widget - do it on a delay so that we
167                                        // don't steal our own focus.
168                                        window.setTimeout(lang.hitch(dropDown, "focus"), 1);
169                                }
170                        }else{
171                                // The drop down arrow icon probably can't receive focus, but widget itself should get focus.
172                                // setTimeout() needed to make it work on IE (test DateTextBox)
173                                setTimeout(lang.hitch(this, "focus"), 0);
174                        }
175
176                        if(has("ios")){
177                                this._justGotMouseUp = true;
178                                setTimeout(lang.hitch(this, function(){
179                                        this._justGotMouseUp = false;
180                                }), 0);
181                        }
182                },
183
184                _onDropDownClick: function(/*Event*/ e){
185                        if(has("ios") && !this._justGotMouseUp){
186                                // This branch fires on iPhone for ComboBox, because the button node is an <input> and doesn't
187                                // generate touchstart/touchend events.   Pretend we just got a mouse down / mouse up.
188                                // The if(has("ios") is necessary since IE and desktop safari get spurious onclick events
189                                // when there are nested tables (specifically, clicking on a table that holds a dijit.form.Select,
190                                // but not on the Select itself, causes an onclick event on the Select)
191                                this._onDropDownMouseDown(e);
192                                this._onDropDownMouseUp(e);
193                        }
194
195                        // The drop down was already opened on mousedown/keydown; just need to call stopEvent().
196                        if(this._stopClickEvents){
197                                event.stop(e);
198                        }
199                },
200
201                buildRendering: function(){
202                        this.inherited(arguments);
203
204                        this._buttonNode = this._buttonNode || this.focusNode || this.domNode;
205                        this._popupStateNode = this._popupStateNode || this.focusNode || this._buttonNode;
206
207                        // Add a class to the "dijitDownArrowButton" type class to _buttonNode so theme can set direction of arrow
208                        // based on where drop down will normally appear
209                        var defaultPos = {
210                                        "after" : this.isLeftToRight() ? "Right" : "Left",
211                                        "before" : this.isLeftToRight() ? "Left" : "Right",
212                                        "above" : "Up",
213                                        "below" : "Down",
214                                        "left" : "Left",
215                                        "right" : "Right"
216                        }[this.dropDownPosition[0]] || this.dropDownPosition[0] || "Down";
217                        domClass.add(this._arrowWrapperNode || this._buttonNode, "dijit" + defaultPos + "ArrowButton");
218                },
219
220                postCreate: function(){
221                        // summary:
222                        //              set up nodes and connect our mouse and keypress events
223
224                        this.inherited(arguments);
225
226                        this.connect(this._buttonNode, touch.press, "_onDropDownMouseDown");
227                        this.connect(this._buttonNode, "onclick", "_onDropDownClick");
228                        this.connect(this.focusNode, "onkeypress", "_onKey");
229                        this.connect(this.focusNode, "onkeyup", "_onKeyUp");
230                },
231
232                destroy: function(){
233                        if(this.dropDown){
234                                // Destroy the drop down, unless it's already been destroyed.  This can happen because
235                                // the drop down is a direct child of <body> even though it's logically my child.
236                                if(!this.dropDown._destroyed){
237                                        this.dropDown.destroyRecursive();
238                                }
239                                delete this.dropDown;
240                        }
241                        this.inherited(arguments);
242                },
243
244                _onKey: function(/*Event*/ e){
245                        // summary:
246                        //              Callback when the user presses a key while focused on the button node
247
248                        if(this.disabled || this.readOnly){ return; }
249
250                        var d = this.dropDown, target = e.target;
251                        if(d && this._opened && d.handleKey){
252                                if(d.handleKey(e) === false){
253                                        /* false return code means that the drop down handled the key */
254                                        event.stop(e);
255                                        return;
256                                }
257                        }
258                        if(d && this._opened && e.charOrCode == keys.ESCAPE){
259                                this.closeDropDown();
260                                event.stop(e);
261                        }else if(!this._opened &&
262                                        (e.charOrCode == keys.DOWN_ARROW ||
263                                                ( (e.charOrCode == keys.ENTER || e.charOrCode == " ") &&
264                                                  //ignore enter and space if the event is for a text input
265                                                  ((target.tagName || "").toLowerCase() !== 'input' ||
266                                                     (target.type && target.type.toLowerCase() !== 'text'))))){
267                                // Toggle the drop down, but wait until keyup so that the drop down doesn't
268                                // get a stray keyup event, or in the case of key-repeat (because user held
269                                // down key for too long), stray keydown events
270                                this._toggleOnKeyUp = true;
271                                event.stop(e);
272                        }
273                },
274
275                _onKeyUp: function(){
276                        if(this._toggleOnKeyUp){
277                                delete this._toggleOnKeyUp;
278                                this.toggleDropDown();
279                                var d = this.dropDown;  // drop down may not exist until toggleDropDown() call
280                                if(d && d.focus){
281                                        setTimeout(lang.hitch(d, "focus"), 1);
282                                }
283                        }
284                },
285
286                _onBlur: function(){
287                        // summary:
288                        //              Called magically when focus has shifted away from this widget and it's dropdown
289
290                        // Don't focus on button if the user has explicitly focused on something else (happens
291                        // when user clicks another control causing the current popup to close)..
292                        // But if focus is inside of the drop down then reset focus to me, because IE doesn't like
293                        // it when you display:none a node with focus.
294                        var focusMe = focus.curNode && this.dropDown && dom.isDescendant(focus.curNode, this.dropDown.domNode);
295
296                        this.closeDropDown(focusMe);
297
298                        this.inherited(arguments);
299                },
300
301                isLoaded: function(){
302                        // summary:
303                        //              Returns true if the dropdown exists and it's data is loaded.  This can
304                        //              be overridden in order to force a call to loadDropDown().
305                        // tags:
306                        //              protected
307
308                        return true;
309                },
310
311                loadDropDown: function(/*Function*/ loadCallback){
312                        // summary:
313                        //              Creates the drop down if it doesn't exist, loads the data
314                        //              if there's an href and it hasn't been loaded yet, and then calls
315                        //              the given callback.
316                        // tags:
317                        //              protected
318
319                        // TODO: for 2.0, change API to return a Deferred, instead of calling loadCallback?
320                        loadCallback();
321                },
322
323                loadAndOpenDropDown: function(){
324                        // summary:
325                        //              Creates the drop down if it doesn't exist, loads the data
326                        //              if there's an href and it hasn't been loaded yet, and
327                        //              then opens the drop down.  This is basically a callback when the
328                        //              user presses the down arrow button to open the drop down.
329                        // returns: Deferred
330                        //              Deferred for the drop down widget that
331                        //              fires when drop down is created and loaded
332                        // tags:
333                        //              protected
334                        var d = new Deferred(),
335                                afterLoad = lang.hitch(this, function(){
336                                        this.openDropDown();
337                                        d.resolve(this.dropDown);
338                                });
339                        if(!this.isLoaded()){
340                                this.loadDropDown(afterLoad);
341                        }else{
342                                afterLoad();
343                        }
344                        return d;
345                },
346
347                toggleDropDown: function(){
348                        // summary:
349                        //              Callback when the user presses the down arrow button or presses
350                        //              the down arrow key to open/close the drop down.
351                        //              Toggle the drop-down widget; if it is up, close it, if not, open it
352                        // tags:
353                        //              protected
354
355                        if(this.disabled || this.readOnly){ return; }
356                        if(!this._opened){
357                                this.loadAndOpenDropDown();
358                        }else{
359                                this.closeDropDown();
360                        }
361                },
362
363                openDropDown: function(){
364                        // summary:
365                        //              Opens the dropdown for this widget.   To be called only when this.dropDown
366                        //              has been created and is ready to display (ie, it's data is loaded).
367                        // returns:
368                        //              return value of dijit.popup.open()
369                        // tags:
370                        //              protected
371
372                        var dropDown = this.dropDown,
373                                ddNode = dropDown.domNode,
374                                aroundNode = this._aroundNode || this.domNode,
375                                self = this;
376
377                        // Prepare our popup's height and honor maxHeight if it exists.
378
379                        // TODO: isn't maxHeight dependent on the return value from dijit.popup.open(),
380                        // ie, dependent on how much space is available (BK)
381
382                        if(!this._preparedNode){
383                                this._preparedNode = true;
384                                // Check if we have explicitly set width and height on the dropdown widget dom node
385                                if(ddNode.style.width){
386                                        this._explicitDDWidth = true;
387                                }
388                                if(ddNode.style.height){
389                                        this._explicitDDHeight = true;
390                                }
391                        }
392
393                        // Code for resizing dropdown (height limitation, or increasing width to match my width)
394                        if(this.maxHeight || this.forceWidth || this.autoWidth){
395                                var myStyle = {
396                                        display: "",
397                                        visibility: "hidden"
398                                };
399                                if(!this._explicitDDWidth){
400                                        myStyle.width = "";
401                                }
402                                if(!this._explicitDDHeight){
403                                        myStyle.height = "";
404                                }
405                                domStyle.set(ddNode, myStyle);
406
407                                // Figure out maximum height allowed (if there is a height restriction)
408                                var maxHeight = this.maxHeight;
409                                if(maxHeight == -1){
410                                        // limit height to space available in viewport either above or below my domNode
411                                        // (whichever side has more room)
412                                        var viewport = winUtils.getBox(),
413                                                position = domGeometry.position(aroundNode, false);
414                                        maxHeight = Math.floor(Math.max(position.y, viewport.h - (position.y + position.h)));
415                                }
416
417                                // Attach dropDown to DOM and make make visibility:hidden rather than display:none
418                                // so we call startup() and also get the size
419                                popup.moveOffScreen(dropDown);
420
421                                if(dropDown.startup && !dropDown._started){
422                                        dropDown.startup(); // this has to be done after being added to the DOM
423                                }
424                                // Get size of drop down, and determine if vertical scroll bar needed
425                                var mb = domGeometry.getMarginSize(ddNode);
426                                var overHeight = (maxHeight && mb.h > maxHeight);
427                                domStyle.set(ddNode, {
428                                        overflowX: "hidden",
429                                        overflowY: overHeight ? "auto" : "hidden"
430                                });
431                                if(overHeight){
432                                        mb.h = maxHeight;
433                                        if("w" in mb){
434                                                mb.w += 16;     // room for vertical scrollbar
435                                        }
436                                }else{
437                                        delete mb.h;
438                                }
439
440                                // Adjust dropdown width to match or be larger than my width
441                                if(this.forceWidth){
442                                        mb.w = aroundNode.offsetWidth;
443                                }else if(this.autoWidth){
444                                        mb.w = Math.max(mb.w, aroundNode.offsetWidth);
445                                }else{
446                                        delete mb.w;
447                                }
448
449                                // And finally, resize the dropdown to calculated height and width
450                                if(lang.isFunction(dropDown.resize)){
451                                        dropDown.resize(mb);
452                                }else{
453                                        domGeometry.setMarginBox(ddNode, mb);
454                                }
455                        }
456
457                        var retVal = popup.open({
458                                parent: this,
459                                popup: dropDown,
460                                around: aroundNode,
461                                orient: this.dropDownPosition,
462                                onExecute: function(){
463                                        self.closeDropDown(true);
464                                },
465                                onCancel: function(){
466                                        self.closeDropDown(true);
467                                },
468                                onClose: function(){
469                                        domAttr.set(self._popupStateNode, "popupActive", false);
470                                        domClass.remove(self._popupStateNode, "dijitHasDropDownOpen");
471                                        self._opened = false;
472                                }
473                        });
474                        domAttr.set(this._popupStateNode, "popupActive", "true");
475                        domClass.add(self._popupStateNode, "dijitHasDropDownOpen");
476                        this._opened=true;
477
478                        // TODO: set this.checked and call setStateClass(), to affect button look while drop down is shown
479                        return retVal;
480                },
481
482                closeDropDown: function(/*Boolean*/ focus){
483                        // summary:
484                        //              Closes the drop down on this widget
485                        // focus:
486                        //              If true, refocuses the button widget
487                        // tags:
488                        //              protected
489
490                        if(this._opened){
491                                if(focus){ this.focus(); }
492                                popup.close(this.dropDown);
493                                this._opened = false;
494                        }
495                }
496
497        });
498});
Note: See TracBrowser for help on using the repository browser.