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