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