MDL-43262 JavaScript: Correct use of centered attribute override
[moodle.git] / lib / yui / build / moodle-core-notification-dialogue / moodle-core-notification-dialogue-debug.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',
f2b235cb 43 DIALOGUE_SELECTOR =' [role=dialog]',
586d393f 44 MENUBAR_SELECTOR = '[role=menubar]',
1389bcd7 45 HAS_ZINDEX = '.moodle-has-zindex',
586d393f 46 CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]';
78686995
AN
47
48/**
49 * A re-usable dialogue box with Moodle classes applied.
50 *
4246e5c7 51 * @param {Object} c Object literal specifying the dialogue configuration properties.
78686995
AN
52 * @constructor
53 * @class M.core.dialogue
54 * @extends Y.Panel
55 */
4246e5c7
AN
56DIALOGUE = function(c) {
57 var config = Y.clone(c);
78686995 58 COUNT++;
bf7c86cf 59 var id = 'moodle-dialogue-'+COUNT;
78686995
AN
60 config.notificationBase =
61 Y.Node.create('<div class="'+CSS.BASE+'">')
62 .append(Y.Node.create('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
baffb422 63 .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
78686995
AN
64 .append(Y.Node.create('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
65 .append(Y.Node.create('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
66 Y.one(document.body).append(config.notificationBase);
b59f2e3b
SH
67
68 if (config.additionalBaseClass) {
69 config.notificationBase.addClass(config.additionalBaseClass);
70 }
71
78686995 72 config.srcNode = '#'+id;
78686995
AN
73 config.COUNT = COUNT;
74
78686995
AN
75 // closeButton param to keep the stable versions API.
76 if (config.closeButton === false) {
77 config.buttons = null;
78 } else {
79 config.buttons = [
80 {
81 section: Y.WidgetStdMod.HEADER,
82 classNames: 'closebutton',
83 action: function () {
84 this.hide();
85 }
86 }
87 ];
88 }
89 DIALOGUE.superclass.constructor.apply(this, [config]);
90
91 if (config.closeButton !== false) {
92 // The buttons constructor does not allow custom attributes
93 this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
94 }
95};
96Y.extend(DIALOGUE, Y.Panel, {
d61c96b6
DW
97 // Window resize event listener.
98 _resizeevent : null,
99 // Orientation change event listener.
100 _orientationevent : null,
1389bcd7 101 _calculatedzindex : false,
bf7c86cf
DW
102 /**
103 * Initialise the dialogue.
104 *
105 * @method initializer
106 * @return void
107 */
108 initializer : function(config) {
109 var bb;
110
78686995 111 this.render();
ce5867a1 112 this.makeResponsive();
bf7c86cf 113 this.after('visibleChange', this.visibilityChanged, this);
dd66b6ab
DW
114 if (config.center) {
115 this.centerDialogue();
116 }
78686995
AN
117 this.set('COUNT', COUNT);
118
119 // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
120 // and allow setting of z-index in theme.
d61c96b6 121 bb = this.get('boundingBox');
bf7c86cf
DW
122
123 if (config.extraClasses) {
124 Y.Array.each(config.extraClasses, bb.addClass, bb);
125 }
126 if (config.visible) {
127 this.applyZIndex();
128 }
1389bcd7
JF
129 // Recalculate the zIndex every time the modal is altered.
130 this.on('maskShow', this.applyZIndex);
ce5867a1
DW
131 // We must show - after the dialogue has been positioned,
132 // either by centerDialogue or makeResonsive. This is because the show() will trigger
133 // a focus on the dialogue, which will scroll the page. If the dialogue has not
134 // been positioned it will scroll back to the top of the page.
135 if (config.visible) {
136 this.show();
586d393f 137 this.keyDelegation();
ce5867a1 138 }
bf7c86cf
DW
139 },
140
141 /**
142 * Either set the zindex to the supplied value, or set it to one more than the highest existing
143 * dialog in the page.
144 *
145 * @method visibilityChanged
146 * @return void
147 */
148 applyZIndex : function() {
1389bcd7
JF
149 var highestzindex = 1,
150 zindexvalue = 1,
f2b235cb 151 bb = this.get('boundingBox'),
1389bcd7 152 ol = this.get('maskNode'),
f2b235cb 153 zindex = this.get('zIndex');
1389bcd7 154 if (zindex !== 0 && !this._calculatedzindex) {
bf7c86cf 155 // The zindex was specified so we should use that.
f2b235cb 156 bb.setStyle('zIndex', zindex);
bf7c86cf 157 } else {
f2b235cb 158 // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
1389bcd7 159 Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR+', '+HAS_ZINDEX).each(function (node) {
f2b235cb
SH
160 var zindex = this.findZIndex(node);
161 if (zindex > highestzindex) {
162 highestzindex = zindex;
bf7c86cf 163 }
f2b235cb 164 }, this);
bf7c86cf 165 // Only set the zindex if we found a wrapper.
1389bcd7
JF
166 zindexvalue = (highestzindex + 1).toString();
167 bb.setStyle('zIndex', zindexvalue);
168 ol.setStyle('zIndex', zindexvalue);
169 this.set('zIndex', zindexvalue);
170 this._calculatedzindex = true;
bf7c86cf
DW
171 }
172 },
173
f2b235cb
SH
174 /**
175 * Finds the zIndex of the given node or its parent.
176 *
177 * @method findZIndex
178 * @param Node node
179 * @returns int Return either the zIndex of 0 if one was not found.
180 */
181 findZIndex : function(node) {
182 // In most cases the zindex is set on the parent of the dialog.
183 var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
184 if (zindex) {
185 return parseInt(zindex, 10);
186 }
187 return 0;
188 },
189
bf7c86cf
DW
190 /**
191 * Event listener for the visibility changed event.
192 *
193 * @method visibilityChanged
194 * @return void
195 */
78686995 196 visibilityChanged : function(e) {
586d393f 197 var titlebar, bb;
78686995
AN
198 if (e.attrName === 'visible') {
199 this.get('maskNode').addClass(CSS.LIGHTBOX);
d61c96b6 200 if (e.prevVal && !e.newVal) {
586d393f 201 bb = this.get('boundingBox');
d61c96b6
DW
202 if (this._resizeevent) {
203 this._resizeevent.detach();
204 this._resizeevent = null;
205 }
206 if (this._orientationevent) {
207 this._orientationevent.detach();
208 this._orientationevent = null;
209 }
586d393f 210 bb.detach('key', this.keyDelegation);
d61c96b6 211 }
bf7c86cf
DW
212 if (!e.prevVal && e.newVal) {
213 // This needs to be done each time the dialog is shown as new dialogs may have been opened.
214 this.applyZIndex();
215 // This needs to be done each time the dialog is shown as the window may have been resized.
216 this.makeResponsive();
217 if (!this.shouldResizeFullscreen()) {
218 if (this.get('draggable')) {
219 titlebar = '#' + this.get('id') + ' .' + CSS.HEADER;
220 this.plug(Y.Plugin.Drag, {handles : [titlebar]});
221 Y.one(titlebar).setStyle('cursor', 'move');
222 }
223 }
586d393f 224 this.keyDelegation();
bf7c86cf 225 }
78686995
AN
226 if (this.get('center') && !e.prevVal && e.newVal) {
227 this.centerDialogue();
228 }
78686995
AN
229 }
230 },
bf7c86cf
DW
231 /**
232 * If the responsive attribute is set on the dialog, and the window size is
233 * smaller than the responsive width - make the dialog fullscreen.
234 *
235 * @method makeResponsive
236 * @return void
237 */
238 makeResponsive : function() {
78686995 239 var bb = this.get('boundingBox'),
bf7c86cf
DW
240 content;
241
242 if (this.shouldResizeFullscreen()) {
d61c96b6
DW
243 // Make this dialogue fullscreen on a small screen.
244 // Disable the page scrollbars.
bf7c86cf 245
d61c96b6
DW
246 // Size and position the fullscreen dialog.
247
2a808cef
DW
248 bb.addClass(DIALOGUE_FULLSCREEN_CLASS);
249 bb.setStyles({'left' : null,
250 'top' : null,
251 'width' : null,
252 'height' : null,
253 'right' : null,
254 'bottom' : null});
d61c96b6
DW
255
256 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6
DW
257 } else {
258 if (this.get('responsive')) {
259 // We must reset any of the fullscreen changes.
2a808cef
DW
260 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
261 .setStyles({'width' : this.get('width'),
bf7c86cf 262 'height' : this.get('height')});
d61c96b6 263 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
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;
586d393f 325 },
326 /**
327 * Setup key delegation to keep tabbing within the open dialogue.
328 *
329 * @method keyDelegation
330 */
331 keyDelegation : function() {
332 var bb = this.get('boundingBox');
333 bb.delegate('key', function(e){
334 var target = e.target;
335 var direction = 'forward';
336 if (e.shiftKey) {
337 direction = 'backward';
338 }
339 if (this.trapFocus(target, direction)) {
340 e.preventDefault();
341 }
342 }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this);
343 },
344 /**
345 * Trap the tab focus within the open modal.
346 *
347 * @param string target the element target
348 * @param string direction tab key for forward and tab+shift for backward
349 * @returns bool
350 */
351 trapFocus : function(target, direction) {
352 var bb = this.get('boundingBox'),
353 firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR),
354 lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop();
355
356 if (target === lastitem && direction === 'forward') { // Tab key.
357 return firstitem.focus();
358 } else if (target === firstitem && direction === 'backward') { // Tab+shift key.
359 return lastitem.focus();
360 }
78686995
AN
361 }
362}, {
363 NAME : DIALOGUE_NAME,
364 CSS_PREFIX : DIALOGUE_PREFIX,
365 ATTRS : {
366 notificationBase : {
367
368 },
369
370 /**
371 * Whether to display the dialogue modally and with a
372 * lightbox style.
373 *
374 * @attribute lightbox
375 * @type Boolean
376 * @default true
cff3b8fe 377 * @deprecated Since Moodle 2.7. Please use modal instead.
78686995 378 */
cff3b8fe
AN
379 lightbox: {
380 lazyAdd: false,
381 setter: function(value) {
382 Y.log("The lightbox attribute of M.core.dialogue has been deprecated since Moodle 2.7, please use the modal attribute instead",
383 'warn', 'moodle-core-notification-dialogue');
384 this.set('modal', value);
385 }
78686995
AN
386 },
387
388 /**
389 * Whether to display a close button on the dialogue.
390 *
391 * Note, we do not recommend hiding the close button as this has
392 * potential accessibility concerns.
393 *
394 * @attribute closeButton
395 * @type Boolean
396 * @default true
397 */
398 closeButton : {
399 validator : Y.Lang.isBoolean,
400 value : true
401 },
402
403 /**
404 * The title for the close button if one is to be shown.
405 *
406 * @attribute closeButtonTitle
407 * @type String
408 * @default 'Close'
409 */
410 closeButtonTitle : {
411 validator : Y.Lang.isString,
412 value : 'Close'
413 },
414
415 /**
416 * Whether to display the dialogue centrally on the screen.
417 *
418 * @attribute center
419 * @type Boolean
420 * @default true
421 */
422 center : {
423 validator : Y.Lang.isBoolean,
424 value : true
425 },
426
427 /**
428 * Whether to make the dialogue movable around the page.
429 *
430 * @attribute draggable
431 * @type Boolean
432 * @default false
433 */
434 draggable : {
435 validator : Y.Lang.isBoolean,
436 value : false
437 },
bf7c86cf
DW
438
439 /**
440 * Used to generate a unique id for the dialogue.
441 *
442 * @attribute COUNT
443 * @type Integer
444 * @default 0
445 */
78686995
AN
446 COUNT: {
447 value: 0
d61c96b6 448 },
bf7c86cf
DW
449
450 /**
451 * Used to disable the fullscreen resizing behaviour if required.
452 *
453 * @attribute responsive
454 * @type Boolean
455 * @default true
456 */
d61c96b6
DW
457 responsive : {
458 validator : Y.Lang.isBoolean,
459 value : true
460 },
bf7c86cf
DW
461
462 /**
463 * The width that this dialogue should be resized to fullscreen.
464 *
465 * @attribute responsiveWidth
466 * @type Integer
467 * @default 768
468 */
d61c96b6
DW
469 responsiveWidth : {
470 value : 768
78686995
AN
471 }
472 }
473});
474
16d02434
AN
475Y.Base.modifyAttrs(DIALOGUE, {
476 /**
477 * String with units, or number, representing the width of the Widget.
478 * If a number is provided, the default unit, defined by the Widgets
479 * DEF_UNIT, property is used.
480 *
481 * If a value of 'auto' is used, then an empty String is instead
482 * returned.
483 *
484 * @attribute width
485 * @default '400px'
486 * @type {String|Number}
487 */
488 width: {
489 value: '400px',
490 setter: function(value) {
491 if (value === 'auto') {
492 return '';
493 }
494 return value;
495 }
c46cca4f
AN
496 },
497
498 /**
499 * Boolean indicating whether or not the Widget is visible.
500 *
501 * We override this from the default Widget attribute value.
502 *
503 * @attribute visible
504 * @default false
505 * @type Boolean
506 */
507 visible: {
508 value: false
a67233e7
AN
509 },
510
511 /**
512 * A convenience Attribute, which can be used as a shortcut for the
513 * `align` Attribute.
514 *
515 * Note: We override this in Moodle such that it sets a value for the
516 * `center` attribute if set. The `centered` will always return false.
517 *
518 * @attribute centered
519 * @type Boolean|Node
520 * @default false
521 */
522 centered: {
523 setter: function(value) {
524 if (value) {
525 this.set('center', true);
526 }
527 return false;
528 }
16d02434
AN
529 }
530});
531
78686995
AN
532M.core.dialogue = DIALOGUE;
533
534
535}, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin"]});