weekly release 2.6dev
[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
DW
9 DIALOGUE,
10 DIALOGUE_FULLSCREEN_CLASS,
11 DIALOGUE_HIDDEN_CLASS,
12 EXISTING_WINDOW_SELECTOR,
13 NOSCROLLING_CLASS;
14
15DIALOGUE_MODAL_CLASS = 'yui3-widget-modal';
16DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX+'-fullscreen';
17DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX+'-hidden';
18EXISTING_WINDOW_SELECTOR = '[role=dialog]';
19NOSCROLLING_CLASS = 'no-scrolling';
78686995
AN
20
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 */
29DIALOGUE = function(config) {
30 COUNT++;
bf7c86cf 31 var id = 'moodle-dialogue-'+COUNT;
78686995
AN
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>')
baffb422 35 .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
78686995
AN
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);
b59f2e3b
SH
39
40 if (config.additionalBaseClass) {
41 config.notificationBase.addClass(config.additionalBaseClass);
42 }
43
78686995
AN
44 config.srcNode = '#'+id;
45 config.width = config.width || '400px';
46 config.visible = config.visible || false;
4fd8adab 47 config.center = config.centered && true;
78686995
AN
48 config.centered = false;
49 config.COUNT = COUNT;
50
b59f2e3b
SH
51 if (config.width === 'auto') {
52 delete config.width;
53 }
54
78686995
AN
55 // lightbox param to keep the stable versions API.
56 if (config.lightbox !== false) {
57 config.modal = true;
58 }
59 delete config.lightbox;
60
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]);
76
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};
82Y.extend(DIALOGUE, Y.Panel, {
d61c96b6
DW
83 // Window resize event listener.
84 _resizeevent : null,
85 // Orientation change event listener.
86 _orientationevent : null,
87
bf7c86cf
DW
88 /**
89 * Initialise the dialogue.
90 *
91 * @method initializer
92 * @return void
93 */
94 initializer : function(config) {
95 var bb;
96
78686995
AN
97 this.render();
98 this.show();
bf7c86cf 99 this.after('visibleChange', this.visibilityChanged, this);
dd66b6ab
DW
100 if (config.center) {
101 this.centerDialogue();
102 }
bf7c86cf
DW
103 if (!config.visible) {
104 this.hide();
105 }
78686995
AN
106 this.set('COUNT', COUNT);
107
108 // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
109 // and allow setting of z-index in theme.
d61c96b6 110 bb = this.get('boundingBox');
bf7c86cf
DW
111
112 if (config.extraClasses) {
113 Y.Array.each(config.extraClasses, bb.addClass, bb);
114 }
115 if (config.visible) {
116 this.applyZIndex();
117 }
118 },
119
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;
131
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');
141
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 }
146
147 if (zindex) {
148 zindex = parseInt(zindex, 10);
149
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 },
161
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;
173
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 });
180
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);
d61c96b6 191 }
78686995 192 },
bf7c86cf
DW
193
194 /**
195 * Event listener for the visibility changed event.
196 *
197 * @method visibilityChanged
198 * @return void
199 */
78686995
AN
200 visibilityChanged : function(e) {
201 var titlebar;
202 if (e.attrName === 'visible') {
203 this.get('maskNode').addClass(CSS.LIGHTBOX);
d61c96b6
DW
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 }
bf7c86cf
DW
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 }
78686995
AN
227 if (this.get('center') && !e.prevVal && e.newVal) {
228 this.centerDialogue();
229 }
bf7c86cf 230 this.toggleDocumentScrolling();
78686995
AN
231 }
232 },
bf7c86cf
DW
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() {
78686995 241 var bb = this.get('boundingBox'),
bf7c86cf
DW
242 content;
243
244 if (this.shouldResizeFullscreen()) {
d61c96b6
DW
245 // Make this dialogue fullscreen on a small screen.
246 // Disable the page scrollbars.
bf7c86cf 247
d61c96b6
DW
248 // Size and position the fullscreen dialog.
249
250 bb.addClass(DIALOGUE_PREFIX+'-fullscreen');
bf7c86cf 251 bb.setStyles({'left' : null, 'top' : null, 'width' : null, 'height' : null});
d61c96b6
DW
252
253 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
254 content.setStyle('overflow', 'auto');
d61c96b6
DW
255 } else {
256 if (this.get('responsive')) {
257 // We must reset any of the fullscreen changes.
258 bb.removeClass(DIALOGUE_PREFIX+'-fullscreen')
bf7c86cf
DW
259 .setStyles({'overflow' : 'inherit',
260 'width' : this.get('width'),
261 'height' : this.get('height')});
d61c96b6
DW
262 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
263 content.setStyle('overflow', 'inherit');
264
d61c96b6 265 }
d61c96b6 266 }
bf7c86cf
DW
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;
279
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});
78686995
AN
290
291 if (hidden) {
bf7c86cf 292 bb.addClass(DIALOGUE_HIDDEN_CLASS);
78686995 293 }
d61c96b6 294 },
bf7c86cf
DW
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');
78686995
AN
306 }
307}, {
308 NAME : DIALOGUE_NAME,
309 CSS_PREFIX : DIALOGUE_PREFIX,
310 ATTRS : {
311 notificationBase : {
312
313 },
314
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 },
327
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 },
342
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 },
354
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 },
366
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 },
bf7c86cf
DW
378
379 /**
380 * Used to generate a unique id for the dialogue.
381 *
382 * @attribute COUNT
383 * @type Integer
384 * @default 0
385 */
78686995
AN
386 COUNT: {
387 value: 0
d61c96b6 388 },
bf7c86cf
DW
389
390 /**
391 * Used to disable the fullscreen resizing behaviour if required.
392 *
393 * @attribute responsive
394 * @type Boolean
395 * @default true
396 */
d61c96b6
DW
397 responsive : {
398 validator : Y.Lang.isBoolean,
399 value : true
400 },
bf7c86cf
DW
401
402 /**
403 * The width that this dialogue should be resized to fullscreen.
404 *
405 * @attribute responsiveWidth
406 * @type Integer
407 * @default 768
408 */
d61c96b6
DW
409 responsiveWidth : {
410 value : 768
78686995
AN
411 }
412 }
413});
414
415M.core.dialogue = DIALOGUE;