source: Dev/branches/rest-dojo-ui/client/dijit/Dialog.js @ 274

Last change on this file since 274 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: 19.5 KB
Line 
1define([
2        "require",
3        "dojo/_base/array", // array.forEach array.indexOf array.map
4        "dojo/_base/connect", // connect._keypress
5        "dojo/_base/declare", // declare
6        "dojo/_base/Deferred", // Deferred
7        "dojo/dom", // dom.isDescendant
8        "dojo/dom-class", // domClass.add domClass.contains
9        "dojo/dom-geometry", // domGeometry.position
10        "dojo/dom-style", // domStyle.set
11        "dojo/_base/event", // event.stop
12        "dojo/_base/fx", // fx.fadeIn fx.fadeOut
13        "dojo/i18n", // i18n.getLocalization
14        "dojo/_base/kernel", // kernel.isAsync
15        "dojo/keys",
16        "dojo/_base/lang", // lang.mixin lang.hitch
17        "dojo/on",
18        "dojo/ready",
19        "dojo/_base/sniff", // has("ie") has("opera")
20        "dojo/_base/window", // win.body
21        "dojo/window", // winUtils.getBox
22        "dojo/dnd/Moveable", // Moveable
23        "dojo/dnd/TimedMoveable", // TimedMoveable
24        "./focus",
25        "./_base/manager",      // manager.defaultDuration
26        "./_Widget",
27        "./_TemplatedMixin",
28        "./_CssStateMixin",
29        "./form/_FormMixin",
30        "./_DialogMixin",
31        "./DialogUnderlay",
32        "./layout/ContentPane",
33        "dojo/text!./templates/Dialog.html",
34        ".",                    // for back-compat, exporting dijit._underlay (remove in 2.0)
35        "dojo/i18n!./nls/common"
36], function(require, array, connect, declare, Deferred,
37                        dom, domClass, domGeometry, domStyle, event, fx, i18n, kernel, keys, lang, on, ready, has, win, winUtils,
38                        Moveable, TimedMoveable, focus, manager, _Widget, _TemplatedMixin, _CssStateMixin, _FormMixin, _DialogMixin,
39                        DialogUnderlay, ContentPane, template, dijit){
40       
41/*=====
42        var _Widget = dijit._Widget;
43        var _TemplatedMixin = dijit._TemplatedMixin;
44        var _CssStateMixin = dijit._CssStateMixin;
45        var _FormMixin = dijit.form._FormMixin;
46        var _DialogMixin = dijit._DialogMixin;
47=====*/
48
49
50        // module:
51        //              dijit/Dialog
52        // summary:
53        //              A modal dialog Widget
54
55
56        /*=====
57        dijit._underlay = function(kwArgs){
58                // summary:
59                //              A shared instance of a `dijit.DialogUnderlay`
60                //
61                // description:
62                //              A shared instance of a `dijit.DialogUnderlay` created and
63                //              used by `dijit.Dialog`, though never created until some Dialog
64                //              or subclass thereof is shown.
65        };
66        =====*/
67
68        var _DialogBase = declare("dijit._DialogBase", [_TemplatedMixin, _FormMixin, _DialogMixin, _CssStateMixin], {
69                // summary:
70                //              A modal dialog Widget
71                //
72                // description:
73                //              Pops up a modal dialog window, blocking access to the screen
74                //              and also graying out the screen Dialog is extended from
75                //              ContentPane so it supports all the same parameters (href, etc.)
76                //
77                // example:
78                // |    <div data-dojo-type="dijit.Dialog" data-dojo-props="href: 'test.html'"></div>
79                //
80                // example:
81                // |    var foo = new dijit.Dialog({ title: "test dialog", content: "test content" };
82                // |    dojo.body().appendChild(foo.domNode);
83                // |    foo.startup();
84
85                templateString: template,
86
87                baseClass: "dijitDialog",
88
89                cssStateNodes: {
90                        closeButtonNode: "dijitDialogCloseIcon"
91                },
92
93                // Map widget attributes to DOMNode attributes.
94                _setTitleAttr: [
95                        { node: "titleNode", type: "innerHTML" },
96                        { node: "titleBar", type: "attribute" }
97                ],
98
99                // open: [readonly] Boolean
100                //              True if Dialog is currently displayed on screen.
101                open: false,
102
103                // duration: Integer
104                //              The time in milliseconds it takes the dialog to fade in and out
105                duration: manager.defaultDuration,
106
107                // refocus: Boolean
108                //              A Toggle to modify the default focus behavior of a Dialog, which
109                //              is to re-focus the element which had focus before being opened.
110                //              False will disable refocusing. Default: true
111                refocus: true,
112
113                // autofocus: Boolean
114                //              A Toggle to modify the default focus behavior of a Dialog, which
115                //              is to focus on the first dialog element after opening the dialog.
116                //              False will disable autofocusing. Default: true
117                autofocus: true,
118
119                // _firstFocusItem: [private readonly] DomNode
120                //              The pointer to the first focusable node in the dialog.
121                //              Set by `dijit._DialogMixin._getFocusItems`.
122                _firstFocusItem: null,
123
124                // _lastFocusItem: [private readonly] DomNode
125                //              The pointer to which node has focus prior to our dialog.
126                //              Set by `dijit._DialogMixin._getFocusItems`.
127                _lastFocusItem: null,
128
129                // doLayout: [protected] Boolean
130                //              Don't change this parameter from the default value.
131                //              This ContentPane parameter doesn't make sense for Dialog, since Dialog
132                //              is never a child of a layout container, nor can you specify the size of
133                //              Dialog in order to control the size of an inner widget.
134                doLayout: false,
135
136                // draggable: Boolean
137                //              Toggles the moveable aspect of the Dialog. If true, Dialog
138                //              can be dragged by it's title. If false it will remain centered
139                //              in the viewport.
140                draggable: true,
141
142                //aria-describedby: String
143                //              Allows the user to add an aria-describedby attribute onto the dialog.   The value should
144                //              be the id of the container element of text that describes the dialog purpose (usually
145                //              the first text in the dialog).
146                //              <div data-dojo-type="dijit.Dialog" aria-describedby="intro" .....>
147                //                      <div id="intro">Introductory text</div>
148                //                      <div>rest of dialog contents</div>
149                //              </div>
150                "aria-describedby":"",
151
152                postMixInProperties: function(){
153                        var _nlsResources = i18n.getLocalization("dijit", "common");
154                        lang.mixin(this, _nlsResources);
155                        this.inherited(arguments);
156                },
157
158                postCreate: function(){
159                        domStyle.set(this.domNode, {
160                                display: "none",
161                                position:"absolute"
162                        });
163                        win.body().appendChild(this.domNode);
164
165                        this.inherited(arguments);
166
167                        this.connect(this, "onExecute", "hide");
168                        this.connect(this, "onCancel", "hide");
169                        this._modalconnects = [];
170                },
171
172                onLoad: function(){
173                        // summary:
174                        //              Called when data has been loaded from an href.
175                        //              Unlike most other callbacks, this function can be connected to (via `dojo.connect`)
176                        //              but should *not* be overridden.
177                        // tags:
178                        //              callback
179
180                        // when href is specified we need to reposition the dialog after the data is loaded
181                        // and find the focusable elements
182                        this._position();
183                        if(this.autofocus && DialogLevelManager.isTop(this)){
184                                this._getFocusItems(this.domNode);
185                                focus.focus(this._firstFocusItem);
186                        }
187                        this.inherited(arguments);
188                },
189
190                _endDrag: function(){
191                        // summary:
192                        //              Called after dragging the Dialog. Saves the position of the dialog in the viewport,
193                        //              and also adjust position to be fully within the viewport, so user doesn't lose access to handle
194                        var nodePosition = domGeometry.position(this.domNode),
195                                viewport = winUtils.getBox();
196                        nodePosition.y = Math.min(Math.max(nodePosition.y, 0), (viewport.h - nodePosition.h));
197                        nodePosition.x = Math.min(Math.max(nodePosition.x, 0), (viewport.w - nodePosition.w));
198                        this._relativePosition = nodePosition;
199                        this._position();
200                },
201
202                _setup: function(){
203                        // summary:
204                        //              Stuff we need to do before showing the Dialog for the first
205                        //              time (but we defer it until right beforehand, for
206                        //              performance reasons).
207                        // tags:
208                        //              private
209
210                        var node = this.domNode;
211
212                        if(this.titleBar && this.draggable){
213                                this._moveable = new ((has("ie") == 6) ? TimedMoveable // prevent overload, see #5285
214                                        : Moveable)(node, { handle: this.titleBar });
215                                this.connect(this._moveable, "onMoveStop", "_endDrag");
216                        }else{
217                                domClass.add(node,"dijitDialogFixed");
218                        }
219
220                        this.underlayAttrs = {
221                                dialogId: this.id,
222                                "class": array.map(this["class"].split(/\s/), function(s){ return s+"_underlay"; }).join(" ")
223                        };
224                },
225
226                _size: function(){
227                        // summary:
228                        //              If necessary, shrink dialog contents so dialog fits in viewport
229                        // tags:
230                        //              private
231
232                        this._checkIfSingleChild();
233
234                        // If we resized the dialog contents earlier, reset them back to original size, so
235                        // that if the user later increases the viewport size, the dialog can display w/out a scrollbar.
236                        // Need to do this before the domGeometry.position(this.domNode) call below.
237                        if(this._singleChild){
238                                if(this._singleChildOriginalStyle){
239                                        this._singleChild.domNode.style.cssText = this._singleChildOriginalStyle;
240                                }
241                                delete this._singleChildOriginalStyle;
242                        }else{
243                                domStyle.set(this.containerNode, {
244                                        width:"auto",
245                                        height:"auto"
246                                });
247                        }
248
249                        var bb = domGeometry.position(this.domNode);
250                        var viewport = winUtils.getBox();
251                        if(bb.w >= viewport.w || bb.h >= viewport.h){
252                                // Reduce size of dialog contents so that dialog fits in viewport
253
254                                var w = Math.min(bb.w, Math.floor(viewport.w * 0.75)),
255                                        h = Math.min(bb.h, Math.floor(viewport.h * 0.75));
256
257                                if(this._singleChild && this._singleChild.resize){
258                                        this._singleChildOriginalStyle = this._singleChild.domNode.style.cssText;
259                                        this._singleChild.resize({w: w, h: h});
260                                }else{
261                                        domStyle.set(this.containerNode, {
262                                                width: w + "px",
263                                                height: h + "px",
264                                                overflow: "auto",
265                                                position: "relative"    // workaround IE bug moving scrollbar or dragging dialog
266                                        });
267                                }
268                        }else{
269                                if(this._singleChild && this._singleChild.resize){
270                                        this._singleChild.resize();
271                                }
272                        }
273                },
274
275                _position: function(){
276                        // summary:
277                        //              Position modal dialog in the viewport. If no relative offset
278                        //              in the viewport has been determined (by dragging, for instance),
279                        //              center the node. Otherwise, use the Dialog's stored relative offset,
280                        //              and position the node to top: left: values based on the viewport.
281                        if(!domClass.contains(win.body(), "dojoMove")){ // don't do anything if called during auto-scroll
282                                var node = this.domNode,
283                                        viewport = winUtils.getBox(),
284                                        p = this._relativePosition,
285                                        bb = p ? null : domGeometry.position(node),
286                                        l = Math.floor(viewport.l + (p ? p.x : (viewport.w - bb.w) / 2)),
287                                        t = Math.floor(viewport.t + (p ? p.y : (viewport.h - bb.h) / 2))
288                                ;
289                                domStyle.set(node,{
290                                        left: l + "px",
291                                        top: t + "px"
292                                });
293                        }
294                },
295
296                _onKey: function(/*Event*/ evt){
297                        // summary:
298                        //              Handles the keyboard events for accessibility reasons
299                        // tags:
300                        //              private
301
302                        if(evt.charOrCode){
303                                var node = evt.target;
304                                if(evt.charOrCode === keys.TAB){
305                                        this._getFocusItems(this.domNode);
306                                }
307                                var singleFocusItem = (this._firstFocusItem == this._lastFocusItem);
308                                // see if we are shift-tabbing from first focusable item on dialog
309                                if(node == this._firstFocusItem && evt.shiftKey && evt.charOrCode === keys.TAB){
310                                        if(!singleFocusItem){
311                                                focus.focus(this._lastFocusItem); // send focus to last item in dialog
312                                        }
313                                        event.stop(evt);
314                                }else if(node == this._lastFocusItem && evt.charOrCode === keys.TAB && !evt.shiftKey){
315                                        if(!singleFocusItem){
316                                                focus.focus(this._firstFocusItem); // send focus to first item in dialog
317                                        }
318                                        event.stop(evt);
319                                }else{
320                                        // see if the key is for the dialog
321                                        while(node){
322                                                if(node == this.domNode || domClass.contains(node, "dijitPopup")){
323                                                        if(evt.charOrCode == keys.ESCAPE){
324                                                                this.onCancel();
325                                                        }else{
326                                                                return; // just let it go
327                                                        }
328                                                }
329                                                node = node.parentNode;
330                                        }
331                                        // this key is for the disabled document window
332                                        if(evt.charOrCode !== keys.TAB){ // allow tabbing into the dialog for a11y
333                                                event.stop(evt);
334                                        // opera won't tab to a div
335                                        }else if(!has("opera")){
336                                                try{
337                                                        this._firstFocusItem.focus();
338                                                }catch(e){ /*squelch*/ }
339                                        }
340                                }
341                        }
342                },
343
344                show: function(){
345                        // summary:
346                        //              Display the dialog
347                        // returns: dojo.Deferred
348                        //              Deferred object that resolves when the display animation is complete
349
350                        if(this.open){ return; }
351
352                        if(!this._started){
353                                this.startup();
354                        }
355
356                        // first time we show the dialog, there's some initialization stuff to do
357                        if(!this._alreadyInitialized){
358                                this._setup();
359                                this._alreadyInitialized=true;
360                        }
361
362                        if(this._fadeOutDeferred){
363                                this._fadeOutDeferred.cancel();
364                        }
365
366                        this._modalconnects.push(on(window, "scroll", lang.hitch(this, "layout")));
367                        this._modalconnects.push(on(window, "resize", lang.hitch(this, function(){
368                                // IE gives spurious resize events and can actually get stuck
369                                // in an infinite loop if we don't ignore them
370                                var viewport = winUtils.getBox();
371                                if(!this._oldViewport ||
372                                                viewport.h != this._oldViewport.h ||
373                                                viewport.w != this._oldViewport.w){
374                                        this.layout();
375                                        this._oldViewport = viewport;
376                                }
377                        })));
378                        this._modalconnects.push(on(this.domNode, connect._keypress, lang.hitch(this, "_onKey")));
379
380                        domStyle.set(this.domNode, {
381                                opacity:0,
382                                display:""
383                        });
384
385                        this._set("open", true);
386                        this._onShow(); // lazy load trigger
387
388                        this._size();
389                        this._position();
390
391                        // fade-in Animation object, setup below
392                        var fadeIn;
393
394                        this._fadeInDeferred = new Deferred(lang.hitch(this, function(){
395                                fadeIn.stop();
396                                delete this._fadeInDeferred;
397                        }));
398
399                        fadeIn = fx.fadeIn({
400                                node: this.domNode,
401                                duration: this.duration,
402                                beforeBegin: lang.hitch(this, function(){
403                                        DialogLevelManager.show(this, this.underlayAttrs);
404                                }),
405                                onEnd: lang.hitch(this, function(){
406                                        if(this.autofocus && DialogLevelManager.isTop(this)){
407                                                // find focusable items each time dialog is shown since if dialog contains a widget the
408                                                // first focusable items can change
409                                                this._getFocusItems(this.domNode);
410                                                focus.focus(this._firstFocusItem);
411                                        }
412                                        this._fadeInDeferred.callback(true);
413                                        delete this._fadeInDeferred;
414                                })
415                        }).play();
416
417                        return this._fadeInDeferred;
418                },
419
420                hide: function(){
421                        // summary:
422                        //              Hide the dialog
423                        // returns: dojo.Deferred
424                        //              Deferred object that resolves when the hide animation is complete
425
426                        // if we haven't been initialized yet then we aren't showing and we can just return
427                        if(!this._alreadyInitialized){
428                                return;
429                        }
430                        if(this._fadeInDeferred){
431                                this._fadeInDeferred.cancel();
432                        }
433
434                        // fade-in Animation object, setup below
435                        var fadeOut;
436
437                        this._fadeOutDeferred = new Deferred(lang.hitch(this, function(){
438                                fadeOut.stop();
439                                delete this._fadeOutDeferred;
440                        }));
441                        // fire onHide when the promise resolves.
442                        this._fadeOutDeferred.then(lang.hitch(this, 'onHide'));
443
444                        fadeOut = fx.fadeOut({
445                                node: this.domNode,
446                                duration: this.duration,
447                                onEnd: lang.hitch(this, function(){
448                                        this.domNode.style.display = "none";
449                                        DialogLevelManager.hide(this);
450                                        this._fadeOutDeferred.callback(true);
451                                        delete this._fadeOutDeferred;
452                                })
453                         }).play();
454
455                        if(this._scrollConnected){
456                                this._scrollConnected = false;
457                        }
458                        var h;
459                        while(h = this._modalconnects.pop()){
460                                h.remove();
461                        }
462
463                        if(this._relativePosition){
464                                delete this._relativePosition;
465                        }
466                        this._set("open", false);
467
468                        return this._fadeOutDeferred;
469                },
470
471                layout: function(){
472                        // summary:
473                        //              Position the Dialog and the underlay
474                        // tags:
475                        //              private
476                        if(this.domNode.style.display != "none"){
477                                if(dijit._underlay){    // avoid race condition during show()
478                                        dijit._underlay.layout();
479                                }
480                                this._position();
481                        }
482                },
483
484                destroy: function(){
485                        if(this._fadeInDeferred){
486                                this._fadeInDeferred.cancel();
487                        }
488                        if(this._fadeOutDeferred){
489                                this._fadeOutDeferred.cancel();
490                        }
491                        if(this._moveable){
492                                this._moveable.destroy();
493                        }
494                        var h;
495                        while(h = this._modalconnects.pop()){
496                                h.remove();
497                        }
498
499                        DialogLevelManager.hide(this);
500
501                        this.inherited(arguments);
502                }
503        });
504
505        var Dialog = declare("dijit.Dialog", [ContentPane, _DialogBase], {});
506        Dialog._DialogBase = _DialogBase;       // for monkey patching
507
508        var DialogLevelManager = Dialog._DialogLevelManager = {
509                // summary:
510                //              Controls the various active "levels" on the page, starting with the
511                //              stuff initially visible on the page (at z-index 0), and then having an entry for
512                //              each Dialog shown.
513
514                _beginZIndex: 950,
515
516                show: function(/*dijit._Widget*/ dialog, /*Object*/ underlayAttrs){
517                        // summary:
518                        //              Call right before fade-in animation for new dialog.
519                        //              Saves current focus, displays/adjusts underlay for new dialog,
520                        //              and sets the z-index of the dialog itself.
521                        //
522                        //              New dialog will be displayed on top of all currently displayed dialogs.
523                        //
524                        //              Caller is responsible for setting focus in new dialog after the fade-in
525                        //              animation completes.
526
527                        // Save current focus
528                        ds[ds.length-1].focus = focus.curNode;
529
530                        // Display the underlay, or if already displayed then adjust for this new dialog
531                        var underlay = dijit._underlay;
532                        if(!underlay || underlay._destroyed){
533                                underlay = dijit._underlay = new DialogUnderlay(underlayAttrs);
534                        }else{
535                                underlay.set(dialog.underlayAttrs);
536                        }
537
538                        // Set z-index a bit above previous dialog
539                        var zIndex = ds[ds.length-1].dialog ? ds[ds.length-1].zIndex + 2 : Dialog._DialogLevelManager._beginZIndex;
540                        if(ds.length == 1){     // first dialog
541                                underlay.show();
542                        }
543                        domStyle.set(dijit._underlay.domNode, 'zIndex', zIndex - 1);
544
545                        // Dialog
546                        domStyle.set(dialog.domNode, 'zIndex', zIndex);
547
548                        ds.push({dialog: dialog, underlayAttrs: underlayAttrs, zIndex: zIndex});
549                },
550
551                hide: function(/*dijit._Widget*/ dialog){
552                        // summary:
553                        //              Called when the specified dialog is hidden/destroyed, after the fade-out
554                        //              animation ends, in order to reset page focus, fix the underlay, etc.
555                        //              If the specified dialog isn't open then does nothing.
556                        //
557                        //              Caller is responsible for either setting display:none on the dialog domNode,
558                        //              or calling dijit.popup.hide(), or removing it from the page DOM.
559
560                        if(ds[ds.length-1].dialog == dialog){
561                                // Removing the top (or only) dialog in the stack, return focus
562                                // to previous dialog
563
564                                ds.pop();
565
566                                var pd = ds[ds.length-1];       // the new active dialog (or the base page itself)
567
568                                // Adjust underlay
569                                if(ds.length == 1){
570                                        // Returning to original page.
571                                        // Hide the underlay, unless the underlay widget has already been destroyed
572                                        // because we are being called during page unload (when all widgets are destroyed)
573                                        if(!dijit._underlay._destroyed){
574                                                dijit._underlay.hide();
575                                        }
576                                }else{
577                                        // Popping back to previous dialog, adjust underlay
578                                        domStyle.set(dijit._underlay.domNode, 'zIndex', pd.zIndex - 1);
579                                        dijit._underlay.set(pd.underlayAttrs);
580                                }
581
582                                // Adjust focus
583                                if(dialog.refocus){
584                                        // If we are returning control to a previous dialog but for some reason
585                                        // that dialog didn't have a focused field, set focus to first focusable item.
586                                        // This situation could happen if two dialogs appeared at nearly the same time,
587                                        // since a dialog doesn't set it's focus until the fade-in is finished.
588                                        var focus = pd.focus;
589                                        if(pd.dialog && (!focus || !dom.isDescendant(focus, pd.dialog.domNode))){
590                                                pd.dialog._getFocusItems(pd.dialog.domNode);
591                                                focus = pd.dialog._firstFocusItem;
592                                        }
593
594                                        if(focus){
595                                                focus.focus();
596                                        }
597                                }
598                        }else{
599                                // Removing a dialog out of order (#9944, #10705).
600                                // Don't need to mess with underlay or z-index or anything.
601                                var idx = array.indexOf(array.map(ds, function(elem){return elem.dialog}), dialog);
602                                if(idx != -1){
603                                        ds.splice(idx, 1);
604                                }
605                        }
606                },
607
608                isTop: function(/*dijit._Widget*/ dialog){
609                        // summary:
610                        //              Returns true if specified Dialog is the top in the task
611                        return ds[ds.length-1].dialog == dialog;
612                }
613        };
614
615        // Stack representing the various active "levels" on the page, starting with the
616        // stuff initially visible on the page (at z-index 0), and then having an entry for
617        // each Dialog shown.
618        // Each element in stack has form {
619        //              dialog: dialogWidget,
620        //              focus: returnFromGetFocus(),
621        //              underlayAttrs: attributes to set on underlay (when this widget is active)
622        // }
623        var ds = Dialog._dialogStack = [
624                {dialog: null, focus: null, underlayAttrs: null}        // entry for stuff at z-index: 0
625        ];
626
627        // Back compat w/1.6, remove for 2.0
628        if(!kernel.isAsync){
629                ready(0, function(){
630                        var requires = ["dijit/TooltipDialog"];
631                        require(requires);      // use indirection so modules not rolled into a build
632                });
633        }
634
635        return Dialog;
636});
Note: See TracBrowser for help on using the repository browser.