source: Dev/trunk/src/client/dijit/popup.js @ 501

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

Added Dojo 1.9.3 release.

File size: 15.3 KB
Line 
1define([
2        "dojo/_base/array", // array.forEach array.some
3        "dojo/aspect",
4        "dojo/_base/declare", // declare
5        "dojo/dom", // dom.isDescendant
6        "dojo/dom-attr", // domAttr.set
7        "dojo/dom-construct", // domConstruct.create domConstruct.destroy
8        "dojo/dom-geometry", // domGeometry.isBodyLtr
9        "dojo/dom-style", // domStyle.set
10        "dojo/has", // has("config-bgIframe")
11        "dojo/keys",
12        "dojo/_base/lang", // lang.hitch
13        "dojo/on",
14        "./place",
15        "./BackgroundIframe",
16        "./Viewport",
17        "./main"    // dijit (defining dijit.popup to match API doc)
18], function(array, aspect, declare, dom, domAttr, domConstruct, domGeometry, domStyle, has, keys, lang, on,
19                        place, BackgroundIframe, Viewport, dijit){
20
21        // module:
22        //              dijit/popup
23
24        /*=====
25         var __OpenArgs = {
26                 // popup: Widget
27                 //             widget to display
28                 // parent: Widget
29                 //             the button etc. that is displaying this popup
30                 // around: DomNode
31                 //             DOM node (typically a button); place popup relative to this node.  (Specify this *or* "x" and "y" parameters.)
32                 // x: Integer
33                 //             Absolute horizontal position (in pixels) to place node at.  (Specify this *or* "around" parameter.)
34                 // y: Integer
35                 //             Absolute vertical position (in pixels) to place node at.  (Specify this *or* "around" parameter.)
36                 // orient: Object|String
37                 //             When the around parameter is specified, orient should be a list of positions to try, ex:
38                 //     |       [ "below", "above" ]
39                 //             For backwards compatibility it can also be an (ordered) hash of tuples of the form
40                 //             (around-node-corner, popup-node-corner), ex:
41                 //     |       { "BL": "TL", "TL": "BL" }
42                 //             where BL means "bottom left" and "TL" means "top left", etc.
43                 //
44                 //             dijit/popup.open() tries to position the popup according to each specified position, in order,
45                 //             until the popup appears fully within the viewport.
46                 //
47                 //             The default value is ["below", "above"]
48                 //
49                 //             When an (x,y) position is specified rather than an around node, orient is either
50                 //             "R" or "L".  R (for right) means that it tries to put the popup to the right of the mouse,
51                 //             specifically positioning the popup's top-right corner at the mouse position, and if that doesn't
52                 //             fit in the viewport, then it tries, in order, the bottom-right corner, the top left corner,
53                 //             and the top-right corner.
54                 // onCancel: Function
55                 //             callback when user has canceled the popup by:
56                 //
57                 //             1. hitting ESC or
58                 //             2. by using the popup widget's proprietary cancel mechanism (like a cancel button in a dialog);
59                 //                i.e. whenever popupWidget.onCancel() is called, args.onCancel is called
60                 // onClose: Function
61                 //             callback whenever this popup is closed
62                 // onExecute: Function
63                 //             callback when user "executed" on the popup/sub-popup by selecting a menu choice, etc. (top menu only)
64                 // padding: place.__Position
65                 //             adding a buffer around the opening position. This is only useful when around is not set.
66                 // maxHeight: Integer
67                 //             The max height for the popup.  Any popup taller than this will have scrollbars.
68                 //             Set to Infinity for no max height.  Default is to limit height to available space in viewport,
69                 //             above or below the aroundNode or specified x/y position.
70         };
71         =====*/
72
73        function destroyWrapper(){
74                // summary:
75                //              Function to destroy wrapper when popup widget is destroyed.
76                //              Left in this scope to avoid memory leak on IE8 on refresh page, see #15206.
77                if(this._popupWrapper){
78                        domConstruct.destroy(this._popupWrapper);
79                        delete this._popupWrapper;
80                }
81        }
82
83        var PopupManager = declare(null, {
84                // summary:
85                //              Used to show drop downs (ex: the select list of a ComboBox)
86                //              or popups (ex: right-click context menus).
87
88                // _stack: dijit/_WidgetBase[]
89                //              Stack of currently popped up widgets.
90                //              (someone opened _stack[0], and then it opened _stack[1], etc.)
91                _stack: [],
92
93                // _beginZIndex: Number
94                //              Z-index of the first popup.   (If first popup opens other
95                //              popups they get a higher z-index.)
96                _beginZIndex: 1000,
97
98                _idGen: 1,
99
100                _repositionAll: function(){
101                        // summary:
102                        //              If screen has been scrolled, reposition all the popups in the stack.
103                        //              Then set timer to check again later.
104
105                        if(this._firstAroundNode){      // guard for when clearTimeout() on IE doesn't work
106                                var oldPos = this._firstAroundPosition,
107                                        newPos = domGeometry.position(this._firstAroundNode, true),
108                                        dx = newPos.x - oldPos.x,
109                                        dy = newPos.y - oldPos.y;
110
111                                if(dx || dy){
112                                        this._firstAroundPosition = newPos;
113                                        for(var i = 0; i < this._stack.length; i++){
114                                                var style = this._stack[i].wrapper.style;
115                                                style.top = (parseInt(style.top, 10) + dy) + "px";
116                                                if(style.right == "auto"){
117                                                        style.left = (parseInt(style.left, 10) + dx) + "px";
118                                                }else{
119                                                        style.right = (parseInt(style.right, 10) - dx) + "px";
120                                                }
121                                        }
122                                }
123
124                                this._aroundMoveListener = setTimeout(lang.hitch(this, "_repositionAll"), dx || dy ? 10 : 50);
125                        }
126                },
127
128                _createWrapper: function(/*Widget*/ widget){
129                        // summary:
130                        //              Initialization for widgets that will be used as popups.
131                        //              Puts widget inside a wrapper DIV (if not already in one),
132                        //              and returns pointer to that wrapper DIV.
133
134                        var wrapper = widget._popupWrapper,
135                                node = widget.domNode;
136
137                        if(!wrapper){
138                                // Create wrapper <div> for when this widget [in the future] will be used as a popup.
139                                // This is done early because of IE bugs where creating/moving DOM nodes causes focus
140                                // to go wonky, see tests/robot/Toolbar.html to reproduce
141                                wrapper = domConstruct.create("div", {
142                                        "class": "dijitPopup",
143                                        style: { display: "none"},
144                                        role: "region",
145                                        "aria-label": widget["aria-label"] || widget.label || widget.name || widget.id
146                                }, widget.ownerDocumentBody);
147                                wrapper.appendChild(node);
148
149                                var s = node.style;
150                                s.display = "";
151                                s.visibility = "";
152                                s.position = "";
153                                s.top = "0px";
154
155                                widget._popupWrapper = wrapper;
156                                aspect.after(widget, "destroy", destroyWrapper, true);
157                        }
158
159                        return wrapper;
160                },
161
162                moveOffScreen: function(/*Widget*/ widget){
163                        // summary:
164                        //              Moves the popup widget off-screen.
165                        //              Do not use this method to hide popups when not in use, because
166                        //              that will create an accessibility issue: the offscreen popup is
167                        //              still in the tabbing order.
168
169                        // Create wrapper if not already there
170                        var wrapper = this._createWrapper(widget);
171
172                        // Besides setting visibility:hidden, move it out of the viewport, see #5776, #10111, #13604
173                        var ltr = domGeometry.isBodyLtr(widget.ownerDocument),
174                                style = {
175                                        visibility: "hidden",
176                                        top: "-9999px",
177                                        display: ""
178                                };
179                        style[ltr ? "left" : "right"] = "-9999px";
180                        style[ltr ? "right" : "left"] = "auto";
181                        domStyle.set(wrapper, style);
182
183                        return wrapper;
184                },
185
186                hide: function(/*Widget*/ widget){
187                        // summary:
188                        //              Hide this popup widget (until it is ready to be shown).
189                        //              Initialization for widgets that will be used as popups
190                        //
191                        //              Also puts widget inside a wrapper DIV (if not already in one)
192                        //
193                        //              If popup widget needs to layout it should
194                        //              do so when it is made visible, and popup._onShow() is called.
195
196                        // Create wrapper if not already there
197                        var wrapper = this._createWrapper(widget);
198
199                        domStyle.set(wrapper, {
200                                display: "none",
201                                height: "auto",         // Open may have limited the height to fit in the viewport
202                                overflow: "visible",
203                                border: ""                      // Open() may have moved border from popup to wrapper.
204                        });
205
206                        // Open() may have moved border from popup to wrapper.  Move it back.
207                        var node = widget.domNode;
208                        if("_originalStyle" in node){
209                                node.style.cssText = node._originalStyle;
210                        }
211                },
212
213                getTopPopup: function(){
214                        // summary:
215                        //              Compute the closest ancestor popup that's *not* a child of another popup.
216                        //              Ex: For a TooltipDialog with a button that spawns a tree of menus, find the popup of the button.
217                        var stack = this._stack;
218                        for(var pi = stack.length - 1; pi > 0 && stack[pi].parent === stack[pi - 1].widget; pi--){
219                                /* do nothing, just trying to get right value for pi */
220                        }
221                        return stack[pi];
222                },
223
224                open: function(/*__OpenArgs*/ args){
225                        // summary:
226                        //              Popup the widget at the specified position
227                        //
228                        // example:
229                        //              opening at the mouse position
230                        //              |               popup.open({popup: menuWidget, x: evt.pageX, y: evt.pageY});
231                        //
232                        // example:
233                        //              opening the widget as a dropdown
234                        //              |               popup.open({parent: this, popup: menuWidget, around: this.domNode, onClose: function(){...}});
235                        //
236                        //              Note that whatever widget called dijit/popup.open() should also listen to its own _onBlur callback
237                        //              (fired from _base/focus.js) to know that focus has moved somewhere else and thus the popup should be closed.
238
239                        var stack = this._stack,
240                                widget = args.popup,
241                                node = widget.domNode,
242                                orient = args.orient || ["below", "below-alt", "above", "above-alt"],
243                                ltr = args.parent ? args.parent.isLeftToRight() : domGeometry.isBodyLtr(widget.ownerDocument),
244                                around = args.around,
245                                id = (args.around && args.around.id) ? (args.around.id + "_dropdown") : ("popup_" + this._idGen++);
246
247                        // If we are opening a new popup that isn't a child of a currently opened popup, then
248                        // close currently opened popup(s).   This should happen automatically when the old popups
249                        // gets the _onBlur() event, except that the _onBlur() event isn't reliable on IE, see [22198].
250                        while(stack.length && (!args.parent || !dom.isDescendant(args.parent.domNode, stack[stack.length - 1].widget.domNode))){
251                                this.close(stack[stack.length - 1].widget);
252                        }
253
254                        // Get pointer to popup wrapper, and create wrapper if it doesn't exist.  Remove display:none (but keep
255                        // off screen) so we can do sizing calculations.
256                        var wrapper = this.moveOffScreen(widget);
257
258                        if(widget.startup && !widget._started){
259                                widget.startup(); // this has to be done after being added to the DOM
260                        }
261
262                        // Limit height to space available in viewport either above or below aroundNode (whichever side has more
263                        // room), adding scrollbar if necessary. Can't add scrollbar to widget because it may be a <table> (ex:
264                        // dijit/Menu), so add to wrapper, and then move popup's border to wrapper so scroll bar inside border.
265                        var maxHeight, popupSize = domGeometry.position(node);
266                        if("maxHeight" in args && args.maxHeight != -1){
267                                maxHeight = args.maxHeight || Infinity; // map 0 --> infinity for back-compat of _HasDropDown.maxHeight
268                        }else{
269                                var viewport = Viewport.getEffectiveBox(this.ownerDocument),
270                                        aroundPos = around ? domGeometry.position(around, false) : {y: args.y - (args.padding||0), h: (args.padding||0) * 2};
271                                maxHeight = Math.floor(Math.max(aroundPos.y, viewport.h - (aroundPos.y + aroundPos.h)));
272                        }
273                        if(popupSize.h > maxHeight){
274                                // Get style of popup's border.  Unfortunately domStyle.get(node, "border") doesn't work on FF or IE,
275                                // and domStyle.get(node, "borderColor") etc. doesn't work on FF, so need to use fully qualified names.
276                                var cs = domStyle.getComputedStyle(node),
277                                        borderStyle = cs.borderLeftWidth + " " + cs.borderLeftStyle + " " + cs.borderLeftColor;
278                                domStyle.set(wrapper, {
279                                        overflowY: "scroll",
280                                        height: maxHeight + "px",
281                                        border: borderStyle     // so scrollbar is inside border
282                                });
283                                node._originalStyle = node.style.cssText;
284                                node.style.border = "none";
285                        }
286
287                        domAttr.set(wrapper, {
288                                id: id,
289                                style: {
290                                        zIndex: this._beginZIndex + stack.length
291                                },
292                                "class": "dijitPopup " + (widget.baseClass || widget["class"] || "").split(" ")[0] + "Popup",
293                                dijitPopupParent: args.parent ? args.parent.id : ""
294                        });
295
296                        if(stack.length == 0 && around){
297                                // First element on stack. Save position of aroundNode and setup listener for changes to that position.
298                                this._firstAroundNode = around;
299                                this._firstAroundPosition = domGeometry.position(around, true);
300                                this._aroundMoveListener = setTimeout(lang.hitch(this, "_repositionAll"), 50);
301                        }
302
303                        if(has("config-bgIframe") && !widget.bgIframe){
304                                // setting widget.bgIframe triggers cleanup in _WidgetBase.destroyRendering()
305                                widget.bgIframe = new BackgroundIframe(wrapper);
306                        }
307
308                        // position the wrapper node and make it visible
309                        var layoutFunc = widget.orient ? lang.hitch(widget, "orient") : null,
310                                best = around ?
311                                        place.around(wrapper, around, orient, ltr, layoutFunc) :
312                                        place.at(wrapper, args, orient == 'R' ? ['TR', 'BR', 'TL', 'BL'] : ['TL', 'BL', 'TR', 'BR'], args.padding,
313                                                layoutFunc);
314
315                        wrapper.style.visibility = "visible";
316                        node.style.visibility = "visible";      // counteract effects from _HasDropDown
317
318                        var handlers = [];
319
320                        // provide default escape and tab key handling
321                        // (this will work for any widget, not just menu)
322                        handlers.push(on(wrapper, "keydown", lang.hitch(this, function(evt){
323                                if(evt.keyCode == keys.ESCAPE && args.onCancel){
324                                        evt.stopPropagation();
325                                        evt.preventDefault();
326                                        args.onCancel();
327                                }else if(evt.keyCode == keys.TAB){
328                                        evt.stopPropagation();
329                                        evt.preventDefault();
330                                        var topPopup = this.getTopPopup();
331                                        if(topPopup && topPopup.onCancel){
332                                                topPopup.onCancel();
333                                        }
334                                }
335                        })));
336
337                        // watch for cancel/execute events on the popup and notify the caller
338                        // (for a menu, "execute" means clicking an item)
339                        if(widget.onCancel && args.onCancel){
340                                handlers.push(widget.on("cancel", args.onCancel));
341                        }
342
343                        handlers.push(widget.on(widget.onExecute ? "execute" : "change", lang.hitch(this, function(){
344                                var topPopup = this.getTopPopup();
345                                if(topPopup && topPopup.onExecute){
346                                        topPopup.onExecute();
347                                }
348                        })));
349
350                        stack.push({
351                                widget: widget,
352                                wrapper: wrapper,
353                                parent: args.parent,
354                                onExecute: args.onExecute,
355                                onCancel: args.onCancel,
356                                onClose: args.onClose,
357                                handlers: handlers
358                        });
359
360                        if(widget.onOpen){
361                                // TODO: in 2.0 standardize onShow() (used by StackContainer) and onOpen() (used here)
362                                widget.onOpen(best);
363                        }
364
365                        return best;
366                },
367
368                close: function(/*Widget?*/ popup){
369                        // summary:
370                        //              Close specified popup and any popups that it parented.
371                        //              If no popup is specified, closes all popups.
372
373                        var stack = this._stack;
374
375                        // Basically work backwards from the top of the stack closing popups
376                        // until we hit the specified popup, but IIRC there was some issue where closing
377                        // a popup would cause others to close too.  Thus if we are trying to close B in [A,B,C]
378                        // closing C might close B indirectly and then the while() condition will run where stack==[A]...
379                        // so the while condition is constructed defensively.
380                        while((popup && array.some(stack, function(elem){
381                                return elem.widget == popup;
382                        })) ||
383                                (!popup && stack.length)){
384                                var top = stack.pop(),
385                                        widget = top.widget,
386                                        onClose = top.onClose;
387
388                                if(widget.onClose){
389                                        // TODO: in 2.0 standardize onHide() (used by StackContainer) and onClose() (used here).
390                                        // Actually, StackContainer also calls onClose(), but to mean that the pane is being deleted
391                                        // (i.e. that the TabContainer's tab's [x] icon was clicked)
392                                        widget.onClose();
393                                }
394
395                                var h;
396                                while(h = top.handlers.pop()){
397                                        h.remove();
398                                }
399
400                                // Hide the widget and it's wrapper unless it has already been destroyed in above onClose() etc.
401                                if(widget && widget.domNode){
402                                        this.hide(widget);
403                                }
404
405                                if(onClose){
406                                        onClose();
407                                }
408                        }
409
410                        if(stack.length == 0 && this._aroundMoveListener){
411                                clearTimeout(this._aroundMoveListener);
412                                this._firstAroundNode = this._firstAroundPosition = this._aroundMoveListener = null;
413                        }
414                }
415        });
416
417        return (dijit.popup = new PopupManager());
418});
Note: See TracBrowser for help on using the repository browser.