Merge branch '41214-26' of git://github.com/samhemelryk/moodle
[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',
12 DIALOGUE_MODAL_CLASS = 'yui3-widget-modal',
13 DIALOGUE_SELECTOR =' [role=dialog]',
14 MENUBAR_SELECTOR = '[role=menubar]',
15 NOSCROLLING_CLASS = 'no-scrolling';
78686995
AN
16
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 */
25DIALOGUE = function(config) {
26 COUNT++;
bf7c86cf 27 var id = 'moodle-dialogue-'+COUNT;
78686995
AN
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>')
baffb422 31 .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
78686995
AN
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);
b59f2e3b
SH
35
36 if (config.additionalBaseClass) {
37 config.notificationBase.addClass(config.additionalBaseClass);
38 }
39
78686995
AN
40 config.srcNode = '#'+id;
41 config.width = config.width || '400px';
42 config.visible = config.visible || false;
4fd8adab 43 config.center = config.centered && true;
78686995
AN
44 config.centered = false;
45 config.COUNT = COUNT;
46
b59f2e3b
SH
47 if (config.width === 'auto') {
48 delete config.width;
49 }
50
78686995
AN
51 // lightbox param to keep the stable versions API.
52 if (config.lightbox !== false) {
53 config.modal = true;
54 }
55 delete config.lightbox;
56
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]);
72
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};
78Y.extend(DIALOGUE, Y.Panel, {
d61c96b6
DW
79 // Window resize event listener.
80 _resizeevent : null,
81 // Orientation change event listener.
82 _orientationevent : null,
83
bf7c86cf
DW
84 /**
85 * Initialise the dialogue.
86 *
87 * @method initializer
88 * @return void
89 */
90 initializer : function(config) {
91 var bb;
92
78686995
AN
93 this.render();
94 this.show();
bf7c86cf 95 this.after('visibleChange', this.visibilityChanged, this);
dd66b6ab
DW
96 if (config.center) {
97 this.centerDialogue();
98 }
bf7c86cf
DW
99 if (!config.visible) {
100 this.hide();
101 }
78686995
AN
102 this.set('COUNT', COUNT);
103
104 // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
105 // and allow setting of z-index in theme.
d61c96b6 106 bb = this.get('boundingBox');
bf7c86cf
DW
107
108 if (config.extraClasses) {
109 Y.Array.each(config.extraClasses, bb.addClass, bb);
110 }
111 if (config.visible) {
112 this.applyZIndex();
113 }
114 },
115
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,
f2b235cb
SH
125 bb = this.get('boundingBox'),
126 zindex = this.get('zIndex');
127 if (zindex) {
bf7c86cf 128 // The zindex was specified so we should use that.
f2b235cb 129 bb.setStyle('zIndex', zindex);
bf7c86cf 130 } else {
f2b235cb
SH
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;
bf7c86cf 136 }
f2b235cb 137 }, this);
bf7c86cf
DW
138 // Only set the zindex if we found a wrapper.
139 if (highestzindex > 0) {
f2b235cb 140 bb.setStyle('zIndex', (highestzindex + 1).toString());
bf7c86cf
DW
141 }
142 }
143 },
144
f2b235cb
SH
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 },
160
bf7c86cf
DW
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;
172
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 });
179
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);
d61c96b6 190 }
78686995 191 },
bf7c86cf
DW
192
193 /**
194 * Event listener for the visibility changed event.
195 *
196 * @method visibilityChanged
197 * @return void
198 */
78686995
AN
199 visibilityChanged : function(e) {
200 var titlebar;
201 if (e.attrName === 'visible') {
202 this.get('maskNode').addClass(CSS.LIGHTBOX);
d61c96b6
DW
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 }
bf7c86cf
DW
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 }
78686995
AN
226 if (this.get('center') && !e.prevVal && e.newVal) {
227 this.centerDialogue();
228 }
bf7c86cf 229 this.toggleDocumentScrolling();
78686995
AN
230 }
231 },
bf7c86cf
DW
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() {
78686995 240 var bb = this.get('boundingBox'),
bf7c86cf
DW
241 content;
242
243 if (this.shouldResizeFullscreen()) {
d61c96b6
DW
244 // Make this dialogue fullscreen on a small screen.
245 // Disable the page scrollbars.
bf7c86cf 246
d61c96b6
DW
247 // Size and position the fullscreen dialog.
248
249 bb.addClass(DIALOGUE_PREFIX+'-fullscreen');
bf7c86cf 250 bb.setStyles({'left' : null, 'top' : null, 'width' : null, 'height' : null});
d61c96b6
DW
251
252 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
253 content.setStyle('overflow', 'auto');
d61c96b6
DW
254 } else {
255 if (this.get('responsive')) {
256 // We must reset any of the fullscreen changes.
257 bb.removeClass(DIALOGUE_PREFIX+'-fullscreen')
bf7c86cf
DW
258 .setStyles({'overflow' : 'inherit',
259 'width' : this.get('width'),
260 'height' : this.get('height')});
d61c96b6
DW
261 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
262 content.setStyle('overflow', 'inherit');
263
d61c96b6 264 }
d61c96b6 265 }
bf7c86cf
DW
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;
278
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});
78686995
AN
289
290 if (hidden) {
bf7c86cf 291 bb.addClass(DIALOGUE_HIDDEN_CLASS);
78686995 292 }
d61c96b6 293 },
bf7c86cf
DW
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');
2eaaae00
JF
305 },
306
307 /**
308 * Override the show method to set keyboard focus on the dialogue.
309 *
310 * @method show
311 * @return void
312 */
313 show : function() {
314 var result = null,
315 header = this.headerNode,
316 content = this.bodyNode;
317
318 result = DIALOGUE.superclass.show.call(this);
319 if (header && header !== '') {
320 header.focus();
321 } else if (content && content !== '') {
322 content.focus();
323 }
324 return result;
78686995
AN
325 }
326}, {
327 NAME : DIALOGUE_NAME,
328 CSS_PREFIX : DIALOGUE_PREFIX,
329 ATTRS : {
330 notificationBase : {
331
332 },
333
334 /**
335 * Whether to display the dialogue modally and with a
336 * lightbox style.
337 *
338 * @attribute lightbox
339 * @type Boolean
340 * @default true
341 */
342 lightbox : {
343 validator : Y.Lang.isBoolean,
344 value : true
345 },
346
347 /**
348 * Whether to display a close button on the dialogue.
349 *
350 * Note, we do not recommend hiding the close button as this has
351 * potential accessibility concerns.
352 *
353 * @attribute closeButton
354 * @type Boolean
355 * @default true
356 */
357 closeButton : {
358 validator : Y.Lang.isBoolean,
359 value : true
360 },
361
362 /**
363 * The title for the close button if one is to be shown.
364 *
365 * @attribute closeButtonTitle
366 * @type String
367 * @default 'Close'
368 */
369 closeButtonTitle : {
370 validator : Y.Lang.isString,
371 value : 'Close'
372 },
373
374 /**
375 * Whether to display the dialogue centrally on the screen.
376 *
377 * @attribute center
378 * @type Boolean
379 * @default true
380 */
381 center : {
382 validator : Y.Lang.isBoolean,
383 value : true
384 },
385
386 /**
387 * Whether to make the dialogue movable around the page.
388 *
389 * @attribute draggable
390 * @type Boolean
391 * @default false
392 */
393 draggable : {
394 validator : Y.Lang.isBoolean,
395 value : false
396 },
bf7c86cf
DW
397
398 /**
399 * Used to generate a unique id for the dialogue.
400 *
401 * @attribute COUNT
402 * @type Integer
403 * @default 0
404 */
78686995
AN
405 COUNT: {
406 value: 0
d61c96b6 407 },
bf7c86cf
DW
408
409 /**
410 * Used to disable the fullscreen resizing behaviour if required.
411 *
412 * @attribute responsive
413 * @type Boolean
414 * @default true
415 */
d61c96b6
DW
416 responsive : {
417 validator : Y.Lang.isBoolean,
418 value : true
419 },
bf7c86cf
DW
420
421 /**
422 * The width that this dialogue should be resized to fullscreen.
423 *
424 * @attribute responsiveWidth
425 * @type Integer
426 * @default 768
427 */
d61c96b6
DW
428 responsiveWidth : {
429 value : 768
78686995
AN
430 }
431 }
432});
433
434M.core.dialogue = DIALOGUE;