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