MDL-42340 JavaScript: Stop dialogues forcing overflow - use CSS instead
[moodle.git] / lib / yui / build / moodle-core-notification-dialogue / moodle-core-notification-dialogue.js
1 YUI.add('moodle-core-notification-dialogue', function (Y, NAME) {
3 var DIALOGUE_PREFIX,
4     BASE,
5     COUNT,
6     CONFIRMYES,
7     CONFIRMNO,
8     TITLE,
9     QUESTION,
10     CSS;
12 DIALOGUE_PREFIX = 'moodle-dialogue',
13 BASE = 'notificationBase',
14 COUNT = 0,
15 CONFIRMYES = 'yesLabel',
16 CONFIRMNO = 'noLabel',
17 TITLE = 'title',
18 QUESTION = 'question',
19 CSS = {
20     BASE : 'moodle-dialogue-base',
21     WRAP : 'moodle-dialogue-wrap',
22     HEADER : 'moodle-dialogue-hd',
23     BODY : 'moodle-dialogue-bd',
24     CONTENT : 'moodle-dialogue-content',
25     FOOTER : 'moodle-dialogue-ft',
26     HIDDEN : 'hidden',
27     LIGHTBOX : 'moodle-dialogue-lightbox'
28 };
30 // Set up the namespace once.
31 M.core = M.core || {};
32 /**
33  * The generic dialogue class for use in Moodle.
34  *
35  * @module moodle-core-notification
36  * @submodule moodle-core-notification-dialogue
37  */
39 var DIALOGUE_NAME = 'Moodle dialogue',
40     DIALOGUE,
41     DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen',
42     DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden',
43     DIALOGUE_SELECTOR =' [role=dialog]',
44     MENUBAR_SELECTOR = '[role=menubar]';
46 /**
47  * A re-usable dialogue box with Moodle classes applied.
48  *
49  * @param {Object} config Object literal specifying the dialogue configuration properties.
50  * @constructor
51  * @class M.core.dialogue
52  * @extends Y.Panel
53  */
54 DIALOGUE = function(config) {
55     COUNT++;
56     var id = 'moodle-dialogue-'+COUNT;
57     config.notificationBase =
58         Y.Node.create('<div class="'+CSS.BASE+'">')
59               .append(Y.Node.create('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
60               .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
61               .append(Y.Node.create('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
62               .append(Y.Node.create('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
63     Y.one(document.body).append(config.notificationBase);
65     if (config.additionalBaseClass) {
66         config.notificationBase.addClass(config.additionalBaseClass);
67     }
69     config.srcNode =    '#'+id;
70     config.width =      config.width || '400px';
71     config.visible =    config.visible || false;
72     config.center =     config.centered && true;
73     config.centered =   false;
74     config.COUNT = COUNT;
76     if (config.width === 'auto') {
77         delete config.width;
78     }
80     // lightbox param to keep the stable versions API.
81     if (config.lightbox !== false) {
82         config.modal = true;
83     }
84     delete config.lightbox;
86     // closeButton param to keep the stable versions API.
87     if (config.closeButton === false) {
88         config.buttons = null;
89     } else {
90         config.buttons = [
91             {
92                 section: Y.WidgetStdMod.HEADER,
93                 classNames: 'closebutton',
94                 action: function () {
95                     this.hide();
96                 }
97             }
98         ];
99     }
100     DIALOGUE.superclass.constructor.apply(this, [config]);
102     if (config.closeButton !== false) {
103         // The buttons constructor does not allow custom attributes
104         this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
105     }
106 };
107 Y.extend(DIALOGUE, Y.Panel, {
108     // Window resize event listener.
109     _resizeevent : null,
110     // Orientation change event listener.
111     _orientationevent : null,
113     /**
114      * Initialise the dialogue.
115      *
116      * @method initializer
117      * @return void
118      */
119     initializer : function(config) {
120         var bb;
122         this.render();
123         this.makeResponsive();
124         this.after('visibleChange', this.visibilityChanged, this);
125         if (config.center) {
126             this.centerDialogue();
127         }
128         this.set('COUNT', COUNT);
130         // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
131         // and allow setting of z-index in theme.
132         bb = this.get('boundingBox');
134         if (config.extraClasses) {
135             Y.Array.each(config.extraClasses, bb.addClass, bb);
136         }
137         if (config.visible) {
138             this.applyZIndex();
139         }
140         // We must show - after the dialogue has been positioned,
141         // either by centerDialogue or makeResonsive. This is because the show() will trigger
142         // a focus on the dialogue, which will scroll the page. If the dialogue has not
143         // been positioned it will scroll back to the top of the page.
144         if (config.visible) {
145             this.show();
146         }
147     },
149     /**
150      * Either set the zindex to the supplied value, or set it to one more than the highest existing
151      * dialog in the page.
152      *
153      * @method visibilityChanged
154      * @return void
155      */
156     applyZIndex : function() {
157         var highestzindex = 0,
158             bb = this.get('boundingBox'),
159             zindex = this.get('zIndex');
160         if (zindex) {
161             // The zindex was specified so we should use that.
162             bb.setStyle('zIndex', zindex);
163         } else {
164             // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
165             Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR).each(function (node) {
166                 var zindex = this.findZIndex(node);
167                 if (zindex > highestzindex) {
168                     highestzindex = zindex;
169                 }
170             }, this);
171             // Only set the zindex if we found a wrapper.
172             if (highestzindex > 0) {
173                 bb.setStyle('zIndex', (highestzindex + 1).toString());
174             }
175         }
176     },
178     /**
179      * Finds the zIndex of the given node or its parent.
180      *
181      * @method findZIndex
182      * @param Node node
183      * @returns int Return either the zIndex of 0 if one was not found.
184      */
185     findZIndex : function(node) {
186         // In most cases the zindex is set on the parent of the dialog.
187         var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
188         if (zindex) {
189             return parseInt(zindex, 10);
190         }
191         return 0;
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         }
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_FULLSCREEN_CLASS);
250             bb.setStyles({'left' : null,
251                           'top' : null,
252                           'width' : null,
253                           'height' : null,
254                           'right' : null,
255                           'bottom' : null});
257             content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
258         } else {
259             if (this.get('responsive')) {
260                 // We must reset any of the fullscreen changes.
261                 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
262                     .setStyles({'width' : this.get('width'),
263                                 'height' : this.get('height')});
264                 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
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     },
308     /**
309      * Override the show method to set keyboard focus on the dialogue.
310      *
311      * @method show
312      * @return void
313      */
314     show : function() {
315         var result = null,
316             header = this.headerNode,
317             content = this.bodyNode;
319         result = DIALOGUE.superclass.show.call(this);
320         if (header && header !== '') {
321             header.focus();
322         } else if (content && content !== '') {
323             content.focus();
324         }
325         return result;
326     }
327 }, {
328     NAME : DIALOGUE_NAME,
329     CSS_PREFIX : DIALOGUE_PREFIX,
330     ATTRS : {
331         notificationBase : {
333         },
335         /**
336          * Whether to display the dialogue modally and with a
337          * lightbox style.
338          *
339          * @attribute lightbox
340          * @type Boolean
341          * @default true
342          */
343         lightbox : {
344             validator : Y.Lang.isBoolean,
345             value : true
346         },
348         /**
349          * Whether to display a close button on the dialogue.
350          *
351          * Note, we do not recommend hiding the close button as this has
352          * potential accessibility concerns.
353          *
354          * @attribute closeButton
355          * @type Boolean
356          * @default true
357          */
358         closeButton : {
359             validator : Y.Lang.isBoolean,
360             value : true
361         },
363         /**
364          * The title for the close button if one is to be shown.
365          *
366          * @attribute closeButtonTitle
367          * @type String
368          * @default 'Close'
369          */
370         closeButtonTitle : {
371             validator : Y.Lang.isString,
372             value : 'Close'
373         },
375         /**
376          * Whether to display the dialogue centrally on the screen.
377          *
378          * @attribute center
379          * @type Boolean
380          * @default true
381          */
382         center : {
383             validator : Y.Lang.isBoolean,
384             value : true
385         },
387         /**
388          * Whether to make the dialogue movable around the page.
389          *
390          * @attribute draggable
391          * @type Boolean
392          * @default false
393          */
394         draggable : {
395             validator : Y.Lang.isBoolean,
396             value : false
397         },
399         /**
400          * Used to generate a unique id for the dialogue.
401          *
402          * @attribute COUNT
403          * @type Integer
404          * @default 0
405          */
406         COUNT: {
407             value: 0
408         },
410         /**
411          * Used to disable the fullscreen resizing behaviour if required.
412          *
413          * @attribute responsive
414          * @type Boolean
415          * @default true
416          */
417         responsive : {
418             validator : Y.Lang.isBoolean,
419             value : true
420         },
422         /**
423          * The width that this dialogue should be resized to fullscreen.
424          *
425          * @attribute responsiveWidth
426          * @type Integer
427          * @default 768
428          */
429         responsiveWidth : {
430             value : 768
431         }
432     }
433 });
435 M.core.dialogue = DIALOGUE;
438 }, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin"]});