bc2a5e75dcf150a70eeed2ed23bc2e2be5c9c1ac
[moodle.git] / lib / yui / src / notification / js / dialogue.js
1 /**
2  * The generic dialogue class for use in Moodle.
3  *
4  * @module moodle-core-notification
5  * @submodule moodle-core-notification-dialogue
6  */
8 var DIALOGUE_NAME = 'Moodle dialogue',
9     DIALOGUE,
10     DIALOGUE_FULLSCREEN_CLASS,
11     DIALOGUE_HIDDEN_CLASS,
12     EXISTING_WINDOW_SELECTOR,
13     NOSCROLLING_CLASS;
15 DIALOGUE_MODAL_CLASS = 'yui3-widget-modal';
16 DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX+'-fullscreen';
17 DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX+'-hidden';
18 EXISTING_WINDOW_SELECTOR = '[role=dialog]';
19 NOSCROLLING_CLASS = 'no-scrolling';
21 /**
22  * A re-usable dialogue box with Moodle classes applied.
23  *
24  * @param {Object} config Object literal specifying the dialogue configuration properties.
25  * @constructor
26  * @class M.core.dialogue
27  * @extends Y.Panel
28  */
29 DIALOGUE = function(config) {
30     COUNT++;
31     var id = 'moodle-dialogue-'+COUNT;
32     config.notificationBase =
33         Y.Node.create('<div class="'+CSS.BASE+'">')
34               .append(Y.Node.create('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
35               .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
36               .append(Y.Node.create('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
37               .append(Y.Node.create('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
38     Y.one(document.body).append(config.notificationBase);
40     if (config.additionalBaseClass) {
41         config.notificationBase.addClass(config.additionalBaseClass);
42     }
44     config.srcNode =    '#'+id;
45     config.width =      config.width || '400px';
46     config.visible =    config.visible || false;
47     config.center =     config.centered && true;
48     config.centered =   false;
49     config.COUNT = COUNT;
51     if (config.width === 'auto') {
52         delete config.width;
53     }
55     // lightbox param to keep the stable versions API.
56     if (config.lightbox !== false) {
57         config.modal = true;
58     }
59     delete config.lightbox;
61     // closeButton param to keep the stable versions API.
62     if (config.closeButton === false) {
63         config.buttons = null;
64     } else {
65         config.buttons = [
66             {
67                 section: Y.WidgetStdMod.HEADER,
68                 classNames: 'closebutton',
69                 action: function () {
70                     this.hide();
71                 }
72             }
73         ];
74     }
75     DIALOGUE.superclass.constructor.apply(this, [config]);
77     if (config.closeButton !== false) {
78         // The buttons constructor does not allow custom attributes
79         this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
80     }
81 };
82 Y.extend(DIALOGUE, Y.Panel, {
83     // Window resize event listener.
84     _resizeevent : null,
85     // Orientation change event listener.
86     _orientationevent : null,
88     /**
89      * Initialise the dialogue.
90      *
91      * @method initializer
92      * @return void
93      */
94     initializer : function(config) {
95         var bb;
97         this.render();
98         this.show();
99         this.after('visibleChange', this.visibilityChanged, this);
100         if (config.center) {
101             this.centerDialogue();
102         }
103         if (!config.visible) {
104             this.hide();
105         }
106         this.set('COUNT', COUNT);
108         // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
109         // and allow setting of z-index in theme.
110         bb = this.get('boundingBox');
112         if (config.extraClasses) {
113             Y.Array.each(config.extraClasses, bb.addClass, bb);
114         }
115         if (config.visible) {
116             this.applyZIndex();
117         }
118     },
120     /**
121      * Either set the zindex to the supplied value, or set it to one more than the highest existing
122      * dialog in the page.
123      *
124      * @method visibilityChanged
125      * @return void
126      */
127     applyZIndex : function() {
128         var highestzindex = 0,
129             zindex,
130             bb;
132         bb = this.get('boundingBox');
133         if (this.get('zIndex')) {
134             // The zindex was specified so we should use that.
135             bb.setStyle('zIndex', this.get('zIndex'));
136         } else {
137             // Determine the correct zindex by looking at all existing dialogs in the page.
138             // Get the zindex of the parent of each wrapper node.
139             Y.all(EXISTING_WINDOW_SELECTOR).each(function (node) {
140                 zindex = node.getStyle('zIndex');
142                 // In most cases the zindex is set on the parent of the dialog.
143                 if (!zindex) {
144                     zindex = node.get('parentNode').getStyle('zIndex');
145                 }
147                 if (zindex) {
148                     zindex = parseInt(zindex, 10);
150                     if (zindex > highestzindex) {
151                         highestzindex = zindex;
152                     }
153                 }
154             });
155             // Only set the zindex if we found a wrapper.
156             if (highestzindex > 0) {
157                 bb.setStyle('zIndex', highestzindex + 1);
158             }
159         }
160     },
162     /**
163      * Enable or disable document scrolling (see if there are any modal or fullscreen popups).
164      *
165      * @method toggleDocumentScrolling
166      * @param Boolean scroll - If true, allow document scrolling.
167      * @return void
168      */
169     toggleDocumentScrolling : function() {
170         var windowroot = Y.one(Y.config.doc.body),
171             scroll = true,
172             search;
174         search = '.' + DIALOGUE_FULLSCREEN_CLASS + ', .' + DIALOGUE_MODAL_CLASS;
175         Y.all(search).each(function (node) {
176             if (!node.hasClass(DIALOGUE_HIDDEN_CLASS)) {
177                 scroll = false;
178             }
179         });
181         if (Y.UA.ie > 0) {
182             // Remember the previous value:
183             windowroot = Y.one('html');
184         }
185         if (scroll) {
186             if (windowroot.hasClass(NOSCROLLING_CLASS)) {
187                 windowroot.removeClass(NOSCROLLING_CLASS);
188             }
189         } else {
190             windowroot.addClass(NOSCROLLING_CLASS);
191         }
192     },
194     /**
195      * Event listener for the visibility changed event.
196      *
197      * @method visibilityChanged
198      * @return void
199      */
200     visibilityChanged : function(e) {
201         var titlebar;
202         if (e.attrName === 'visible') {
203             this.get('maskNode').addClass(CSS.LIGHTBOX);
204             if (e.prevVal && !e.newVal) {
205                 if (this._resizeevent) {
206                     this._resizeevent.detach();
207                     this._resizeevent = null;
208                 }
209                 if (this._orientationevent) {
210                     this._orientationevent.detach();
211                     this._orientationevent = null;
212                 }
213             }
214             if (!e.prevVal && e.newVal) {
215                 // This needs to be done each time the dialog is shown as new dialogs may have been opened.
216                 this.applyZIndex();
217                 // This needs to be done each time the dialog is shown as the window may have been resized.
218                 this.makeResponsive();
219                 if (!this.shouldResizeFullscreen()) {
220                     if (this.get('draggable')) {
221                         titlebar = '#' + this.get('id') + ' .' + CSS.HEADER;
222                         this.plug(Y.Plugin.Drag, {handles : [titlebar]});
223                         Y.one(titlebar).setStyle('cursor', 'move');
224                     }
225                 }
226             }
227             if (this.get('center') && !e.prevVal && e.newVal) {
228                 this.centerDialogue();
229             }
230             this.toggleDocumentScrolling();
231         }
232     },
233     /**
234      * If the responsive attribute is set on the dialog, and the window size is
235      * smaller than the responsive width - make the dialog fullscreen.
236      *
237      * @method makeResponsive
238      * @return void
239      */
240     makeResponsive : function() {
241         var bb = this.get('boundingBox'),
242             content;
244         if (this.shouldResizeFullscreen()) {
245             // Make this dialogue fullscreen on a small screen.
246             // Disable the page scrollbars.
248             // Size and position the fullscreen dialog.
250             bb.addClass(DIALOGUE_PREFIX+'-fullscreen');
251             bb.setStyles({'left' : null, 'top' : null, 'width' : null, 'height' : null});
253             content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
254             content.setStyle('overflow', 'auto');
255         } else {
256             if (this.get('responsive')) {
257                 // We must reset any of the fullscreen changes.
258                 bb.removeClass(DIALOGUE_PREFIX+'-fullscreen')
259                     .setStyles({'overflow' : 'inherit',
260                                 'width' : this.get('width'),
261                                 'height' : this.get('height')});
262                 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
263                 content.setStyle('overflow', 'inherit');
265             }
266         }
267     },
268     /**
269      * Center the dialog on the screen.
270      *
271      * @method centerDialogue
272      * @return void
273      */
274     centerDialogue : function() {
275         var bb = this.get('boundingBox'),
276             hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
277             x,
278             y;
280         // Don't adjust the position if we are in full screen mode.
281         if (this.shouldResizeFullscreen()) {
282             return;
283         }
284         if (hidden) {
285             bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
286         }
287         x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
288         y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
289         bb.setStyles({ 'left' : x, 'top' : y});
291         if (hidden) {
292             bb.addClass(DIALOGUE_HIDDEN_CLASS);
293         }
294     },
295     /**
296      * Return if this dialogue should be fullscreen or not.
297      * Responsive attribute must be true and we should not be in an iframe and the screen width should
298      * be less than the responsive width.
299      *
300      * @method shouldResizeFullscreen
301      * @return Boolean
302      */
303     shouldResizeFullscreen : function() {
304         return (window === window.parent) && this.get('responsive') &&
305                Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
306     }
307 }, {
308     NAME : DIALOGUE_NAME,
309     CSS_PREFIX : DIALOGUE_PREFIX,
310     ATTRS : {
311         notificationBase : {
313         },
315         /**
316          * Whether to display the dialogue modally and with a
317          * lightbox style.
318          *
319          * @attribute lightbox
320          * @type Boolean
321          * @default true
322          */
323         lightbox : {
324             validator : Y.Lang.isBoolean,
325             value : true
326         },
328         /**
329          * Whether to display a close button on the dialogue.
330          *
331          * Note, we do not recommend hiding the close button as this has
332          * potential accessibility concerns.
333          *
334          * @attribute closeButton
335          * @type Boolean
336          * @default true
337          */
338         closeButton : {
339             validator : Y.Lang.isBoolean,
340             value : true
341         },
343         /**
344          * The title for the close button if one is to be shown.
345          *
346          * @attribute closeButtonTitle
347          * @type String
348          * @default 'Close'
349          */
350         closeButtonTitle : {
351             validator : Y.Lang.isString,
352             value : 'Close'
353         },
355         /**
356          * Whether to display the dialogue centrally on the screen.
357          *
358          * @attribute center
359          * @type Boolean
360          * @default true
361          */
362         center : {
363             validator : Y.Lang.isBoolean,
364             value : true
365         },
367         /**
368          * Whether to make the dialogue movable around the page.
369          *
370          * @attribute draggable
371          * @type Boolean
372          * @default false
373          */
374         draggable : {
375             validator : Y.Lang.isBoolean,
376             value : false
377         },
379         /**
380          * Used to generate a unique id for the dialogue.
381          *
382          * @attribute COUNT
383          * @type Integer
384          * @default 0
385          */
386         COUNT: {
387             value: 0
388         },
390         /**
391          * Used to disable the fullscreen resizing behaviour if required.
392          *
393          * @attribute responsive
394          * @type Boolean
395          * @default true
396          */
397         responsive : {
398             validator : Y.Lang.isBoolean,
399             value : true
400         },
402         /**
403          * The width that this dialogue should be resized to fullscreen.
404          *
405          * @attribute responsiveWidth
406          * @type Integer
407          * @default 768
408          */
409         responsiveWidth : {
410             value : 768
411         }
412     }
413 });
415 M.core.dialogue = DIALOGUE;