Merge branch 'w13_MDL-44732_m27_clitask' of git://github.com/skodak/moodle
[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,
78686995
AN
5 CONFIRMYES,
6 CONFIRMNO,
7 TITLE,
8 QUESTION,
9 CSS;
10
11DIALOGUE_PREFIX = 'moodle-dialogue',
12BASE = 'notificationBase',
78686995
AN
13CONFIRMYES = 'yesLabel',
14CONFIRMNO = 'noLabel',
15TITLE = 'title',
16QUESTION = 'question',
17CSS = {
18 BASE : 'moodle-dialogue-base',
19 WRAP : 'moodle-dialogue-wrap',
20 HEADER : 'moodle-dialogue-hd',
21 BODY : 'moodle-dialogue-bd',
22 CONTENT : 'moodle-dialogue-content',
23 FOOTER : 'moodle-dialogue-ft',
24 HIDDEN : 'hidden',
25 LIGHTBOX : 'moodle-dialogue-lightbox'
26};
27
28// Set up the namespace once.
29M.core = M.core || {};
30/**
31 * The generic dialogue class for use in Moodle.
32 *
33 * @module moodle-core-notification
34 * @submodule moodle-core-notification-dialogue
35 */
36
37var DIALOGUE_NAME = 'Moodle dialogue',
bf7c86cf 38 DIALOGUE,
f2b235cb
SH
39 DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen',
40 DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden',
f2b235cb 41 DIALOGUE_SELECTOR =' [role=dialog]',
586d393f 42 MENUBAR_SELECTOR = '[role=menubar]',
1389bcd7 43 HAS_ZINDEX = '.moodle-has-zindex',
586d393f 44 CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]';
78686995
AN
45
46/**
47 * A re-usable dialogue box with Moodle classes applied.
48 *
4246e5c7 49 * @param {Object} c Object literal specifying the dialogue configuration properties.
78686995
AN
50 * @constructor
51 * @class M.core.dialogue
52 * @extends Y.Panel
53 */
4246e5c7
AN
54DIALOGUE = function(c) {
55 var config = Y.clone(c);
d10e6118
AN
56 config.COUNT = Y.stamp(this);
57 var id = 'moodle-dialogue-' + config.COUNT;
78686995
AN
58 config.notificationBase =
59 Y.Node.create('<div class="'+CSS.BASE+'">')
60 .append(Y.Node.create('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
baffb422 61 .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
78686995
AN
62 .append(Y.Node.create('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
63 .append(Y.Node.create('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
64 Y.one(document.body).append(config.notificationBase);
b59f2e3b
SH
65
66 if (config.additionalBaseClass) {
67 config.notificationBase.addClass(config.additionalBaseClass);
68 }
69
78686995 70 config.srcNode = '#'+id;
78686995 71
78686995
AN
72 // closeButton param to keep the stable versions API.
73 if (config.closeButton === false) {
74 config.buttons = null;
75 } else {
76 config.buttons = [
77 {
78 section: Y.WidgetStdMod.HEADER,
79 classNames: 'closebutton',
80 action: function () {
81 this.hide();
82 }
83 }
84 ];
85 }
86 DIALOGUE.superclass.constructor.apply(this, [config]);
87
88 if (config.closeButton !== false) {
89 // The buttons constructor does not allow custom attributes
90 this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
91 }
92};
93Y.extend(DIALOGUE, Y.Panel, {
d61c96b6
DW
94 // Window resize event listener.
95 _resizeevent : null,
96 // Orientation change event listener.
97 _orientationevent : null,
1389bcd7 98 _calculatedzindex : false,
bf7c86cf
DW
99 /**
100 * Initialise the dialogue.
101 *
102 * @method initializer
103 * @return void
104 */
2f5c1441 105 initializer : function() {
bf7c86cf
DW
106 var bb;
107
d9bf4be4
SH
108 if (this.get('render')) {
109 this.render();
110 }
ce5867a1 111 this.makeResponsive();
bf7c86cf 112 this.after('visibleChange', this.visibilityChanged, this);
2f5c1441 113 if (this.get('center')) {
dd66b6ab
DW
114 this.centerDialogue();
115 }
78686995 116
73747aea
AN
117 if (this.get('modal')) {
118 this.plug(Y.M.core.LockScroll);
119 }
120
78686995
AN
121 // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
122 // and allow setting of z-index in theme.
d61c96b6 123 bb = this.get('boundingBox');
bf7c86cf 124
2f5c1441
AN
125 // Add any additional classes that were specified.
126 Y.Array.each(this.get('extraClasses'), bb.addClass, bb);
127
128 if (this.get('visible')) {
bf7c86cf
DW
129 this.applyZIndex();
130 }
1389bcd7
JF
131 // Recalculate the zIndex every time the modal is altered.
132 this.on('maskShow', this.applyZIndex);
ce5867a1
DW
133 // We must show - after the dialogue has been positioned,
134 // either by centerDialogue or makeResonsive. This is because the show() will trigger
135 // a focus on the dialogue, which will scroll the page. If the dialogue has not
136 // been positioned it will scroll back to the top of the page.
2f5c1441 137 if (this.get('visible')) {
ce5867a1 138 this.show();
586d393f 139 this.keyDelegation();
ce5867a1 140 }
0860dd78
AN
141
142 // Remove the dialogue from the DOM when it is destroyed.
143 this.after('destroyedChange', function(){
144 this.get(BASE).remove(true);
145 }, this);
bf7c86cf
DW
146 },
147
148 /**
149 * Either set the zindex to the supplied value, or set it to one more than the highest existing
150 * dialog in the page.
151 *
152 * @method visibilityChanged
153 * @return void
154 */
155 applyZIndex : function() {
1389bcd7
JF
156 var highestzindex = 1,
157 zindexvalue = 1,
f2b235cb 158 bb = this.get('boundingBox'),
1389bcd7 159 ol = this.get('maskNode'),
f2b235cb 160 zindex = this.get('zIndex');
1389bcd7 161 if (zindex !== 0 && !this._calculatedzindex) {
bf7c86cf 162 // The zindex was specified so we should use that.
f2b235cb 163 bb.setStyle('zIndex', zindex);
bf7c86cf 164 } else {
f2b235cb 165 // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
1389bcd7 166 Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR+', '+HAS_ZINDEX).each(function (node) {
f2b235cb
SH
167 var zindex = this.findZIndex(node);
168 if (zindex > highestzindex) {
169 highestzindex = zindex;
bf7c86cf 170 }
f2b235cb 171 }, this);
bf7c86cf 172 // Only set the zindex if we found a wrapper.
1389bcd7
JF
173 zindexvalue = (highestzindex + 1).toString();
174 bb.setStyle('zIndex', zindexvalue);
175 ol.setStyle('zIndex', zindexvalue);
176 this.set('zIndex', zindexvalue);
177 this._calculatedzindex = true;
bf7c86cf
DW
178 }
179 },
180
f2b235cb
SH
181 /**
182 * Finds the zIndex of the given node or its parent.
183 *
184 * @method findZIndex
185 * @param Node node
186 * @returns int Return either the zIndex of 0 if one was not found.
187 */
188 findZIndex : function(node) {
189 // In most cases the zindex is set on the parent of the dialog.
190 var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
191 if (zindex) {
192 return parseInt(zindex, 10);
193 }
194 return 0;
195 },
196
bf7c86cf
DW
197 /**
198 * Event listener for the visibility changed event.
199 *
200 * @method visibilityChanged
201 * @return void
202 */
78686995 203 visibilityChanged : function(e) {
586d393f 204 var titlebar, bb;
78686995
AN
205 if (e.attrName === 'visible') {
206 this.get('maskNode').addClass(CSS.LIGHTBOX);
d61c96b6 207 if (e.prevVal && !e.newVal) {
586d393f 208 bb = this.get('boundingBox');
d61c96b6
DW
209 if (this._resizeevent) {
210 this._resizeevent.detach();
211 this._resizeevent = null;
212 }
213 if (this._orientationevent) {
214 this._orientationevent.detach();
215 this._orientationevent = null;
216 }
586d393f 217 bb.detach('key', this.keyDelegation);
d61c96b6 218 }
bf7c86cf
DW
219 if (!e.prevVal && e.newVal) {
220 // This needs to be done each time the dialog is shown as new dialogs may have been opened.
221 this.applyZIndex();
222 // This needs to be done each time the dialog is shown as the window may have been resized.
223 this.makeResponsive();
224 if (!this.shouldResizeFullscreen()) {
225 if (this.get('draggable')) {
226 titlebar = '#' + this.get('id') + ' .' + CSS.HEADER;
227 this.plug(Y.Plugin.Drag, {handles : [titlebar]});
228 Y.one(titlebar).setStyle('cursor', 'move');
229 }
230 }
586d393f 231 this.keyDelegation();
bf7c86cf 232 }
78686995
AN
233 if (this.get('center') && !e.prevVal && e.newVal) {
234 this.centerDialogue();
235 }
78686995
AN
236 }
237 },
bf7c86cf
DW
238 /**
239 * If the responsive attribute is set on the dialog, and the window size is
240 * smaller than the responsive width - make the dialog fullscreen.
241 *
242 * @method makeResponsive
243 * @return void
244 */
245 makeResponsive : function() {
78686995 246 var bb = this.get('boundingBox'),
bf7c86cf
DW
247 content;
248
249 if (this.shouldResizeFullscreen()) {
d61c96b6
DW
250 // Make this dialogue fullscreen on a small screen.
251 // Disable the page scrollbars.
bf7c86cf 252
d61c96b6
DW
253 // Size and position the fullscreen dialog.
254
2a808cef
DW
255 bb.addClass(DIALOGUE_FULLSCREEN_CLASS);
256 bb.setStyles({'left' : null,
257 'top' : null,
258 'width' : null,
259 'height' : null,
260 'right' : null,
261 'bottom' : null});
d61c96b6
DW
262
263 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6
DW
264 } else {
265 if (this.get('responsive')) {
266 // We must reset any of the fullscreen changes.
2a808cef
DW
267 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
268 .setStyles({'width' : this.get('width'),
bf7c86cf 269 'height' : this.get('height')});
d61c96b6 270 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6 271 }
d61c96b6 272 }
bf7c86cf
DW
273 },
274 /**
275 * Center the dialog on the screen.
276 *
277 * @method centerDialogue
278 * @return void
279 */
280 centerDialogue : function() {
281 var bb = this.get('boundingBox'),
282 hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
283 x,
284 y;
285
286 // Don't adjust the position if we are in full screen mode.
287 if (this.shouldResizeFullscreen()) {
288 return;
289 }
290 if (hidden) {
291 bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
292 }
293 x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
294 y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
295 bb.setStyles({ 'left' : x, 'top' : y});
78686995
AN
296
297 if (hidden) {
bf7c86cf 298 bb.addClass(DIALOGUE_HIDDEN_CLASS);
78686995 299 }
d61c96b6 300 },
bf7c86cf
DW
301 /**
302 * Return if this dialogue should be fullscreen or not.
303 * Responsive attribute must be true and we should not be in an iframe and the screen width should
304 * be less than the responsive width.
305 *
306 * @method shouldResizeFullscreen
307 * @return Boolean
308 */
309 shouldResizeFullscreen : function() {
310 return (window === window.parent) && this.get('responsive') &&
311 Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
2eaaae00
JF
312 },
313
314 /**
315 * Override the show method to set keyboard focus on the dialogue.
316 *
317 * @method show
318 * @return void
319 */
320 show : function() {
321 var result = null,
322 header = this.headerNode,
323 content = this.bodyNode;
324
b959e508
AN
325 result = DIALOGUE.superclass.show.call(this);
326
73747aea
AN
327 // Lock scroll if the plugin is present.
328 if (this.lockScroll) {
b959e508
AN
329 // We need to force the scroll locking for full screen dialogues, even if they have a small vertical size to
330 // prevent the background scrolling while the dialogue is open.
331 this.lockScroll.enableScrollLock(this.shouldResizeFullscreen());
73747aea
AN
332 }
333
2eaaae00
JF
334 if (header && header !== '') {
335 header.focus();
336 } else if (content && content !== '') {
337 content.focus();
338 }
339 return result;
586d393f 340 },
73747aea 341
bf24abd2
AN
342 hide: function(e) {
343 if (e) {
344 // If the event was closed by an escape key event, then we need to check that this
345 // dialogue is currently focused to prevent closing all dialogues in the stack.
346 if (e.type === 'key' && e.keyCode === 27 && !this.get('focused')) {
347 return;
348 }
349 }
350
73747aea
AN
351 // Unlock scroll if the plugin is present.
352 if (this.lockScroll) {
353 this.lockScroll.disableScrollLock();
354 }
355
356 return DIALOGUE.superclass.hide.call(this, arguments);
357 },
586d393f 358 /**
359 * Setup key delegation to keep tabbing within the open dialogue.
360 *
361 * @method keyDelegation
362 */
363 keyDelegation : function() {
364 var bb = this.get('boundingBox');
365 bb.delegate('key', function(e){
366 var target = e.target;
367 var direction = 'forward';
368 if (e.shiftKey) {
369 direction = 'backward';
370 }
371 if (this.trapFocus(target, direction)) {
372 e.preventDefault();
373 }
374 }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this);
375 },
376 /**
377 * Trap the tab focus within the open modal.
378 *
379 * @param string target the element target
380 * @param string direction tab key for forward and tab+shift for backward
381 * @returns bool
382 */
383 trapFocus : function(target, direction) {
384 var bb = this.get('boundingBox'),
385 firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR),
386 lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop();
387
388 if (target === lastitem && direction === 'forward') { // Tab key.
389 return firstitem.focus();
390 } else if (target === firstitem && direction === 'backward') { // Tab+shift key.
391 return lastitem.focus();
392 }
78686995
AN
393 }
394}, {
395 NAME : DIALOGUE_NAME,
396 CSS_PREFIX : DIALOGUE_PREFIX,
397 ATTRS : {
398 notificationBase : {
399
400 },
401
402 /**
403 * Whether to display the dialogue modally and with a
404 * lightbox style.
405 *
406 * @attribute lightbox
407 * @type Boolean
408 * @default true
cff3b8fe 409 * @deprecated Since Moodle 2.7. Please use modal instead.
78686995 410 */
cff3b8fe
AN
411 lightbox: {
412 lazyAdd: false,
413 setter: function(value) {
414 Y.log("The lightbox attribute of M.core.dialogue has been deprecated since Moodle 2.7, please use the modal attribute instead",
415 'warn', 'moodle-core-notification-dialogue');
416 this.set('modal', value);
417 }
78686995
AN
418 },
419
420 /**
421 * Whether to display a close button on the dialogue.
422 *
423 * Note, we do not recommend hiding the close button as this has
424 * potential accessibility concerns.
425 *
426 * @attribute closeButton
427 * @type Boolean
428 * @default true
429 */
430 closeButton : {
431 validator : Y.Lang.isBoolean,
432 value : true
433 },
434
435 /**
436 * The title for the close button if one is to be shown.
437 *
438 * @attribute closeButtonTitle
439 * @type String
440 * @default 'Close'
441 */
442 closeButtonTitle : {
443 validator : Y.Lang.isString,
444 value : 'Close'
445 },
446
447 /**
448 * Whether to display the dialogue centrally on the screen.
449 *
450 * @attribute center
451 * @type Boolean
452 * @default true
453 */
454 center : {
455 validator : Y.Lang.isBoolean,
456 value : true
457 },
458
459 /**
460 * Whether to make the dialogue movable around the page.
461 *
462 * @attribute draggable
463 * @type Boolean
464 * @default false
465 */
466 draggable : {
467 validator : Y.Lang.isBoolean,
468 value : false
469 },
bf7c86cf
DW
470
471 /**
472 * Used to generate a unique id for the dialogue.
473 *
474 * @attribute COUNT
d10e6118
AN
475 * @type String
476 * @default null
bf7c86cf 477 */
78686995 478 COUNT: {
d10e6118 479 value: null
d61c96b6 480 },
bf7c86cf
DW
481
482 /**
483 * Used to disable the fullscreen resizing behaviour if required.
484 *
485 * @attribute responsive
486 * @type Boolean
487 * @default true
488 */
d61c96b6
DW
489 responsive : {
490 validator : Y.Lang.isBoolean,
491 value : true
492 },
bf7c86cf
DW
493
494 /**
495 * The width that this dialogue should be resized to fullscreen.
496 *
497 * @attribute responsiveWidth
498 * @type Integer
499 * @default 768
500 */
d61c96b6
DW
501 responsiveWidth : {
502 value : 768
78686995
AN
503 }
504 }
505});
506
16d02434
AN
507Y.Base.modifyAttrs(DIALOGUE, {
508 /**
509 * String with units, or number, representing the width of the Widget.
510 * If a number is provided, the default unit, defined by the Widgets
511 * DEF_UNIT, property is used.
512 *
513 * If a value of 'auto' is used, then an empty String is instead
514 * returned.
515 *
516 * @attribute width
517 * @default '400px'
518 * @type {String|Number}
519 */
520 width: {
521 value: '400px',
522 setter: function(value) {
523 if (value === 'auto') {
524 return '';
525 }
526 return value;
527 }
c46cca4f
AN
528 },
529
530 /**
531 * Boolean indicating whether or not the Widget is visible.
532 *
533 * We override this from the default Widget attribute value.
534 *
535 * @attribute visible
536 * @default false
537 * @type Boolean
538 */
539 visible: {
540 value: false
a67233e7
AN
541 },
542
543 /**
544 * A convenience Attribute, which can be used as a shortcut for the
545 * `align` Attribute.
546 *
547 * Note: We override this in Moodle such that it sets a value for the
548 * `center` attribute if set. The `centered` will always return false.
549 *
550 * @attribute centered
551 * @type Boolean|Node
552 * @default false
553 */
554 centered: {
555 setter: function(value) {
556 if (value) {
557 this.set('center', true);
558 }
559 return false;
560 }
d9bf4be4
SH
561 },
562
563 /**
564 * Boolean determining whether to render the widget during initialisation.
565 *
566 * We override this to change the default from false to true for the dialogue.
567 * We then proceed to early render the dialogue during our initialisation rather than waiting
568 * for YUI to render it after that.
569 *
570 * @attribute render
571 * @type Boolean
572 * @default true
573 */
574 render : {
575 value : true,
576 writeOnce : true
2f5c1441
AN
577 },
578
579 /**
580 * Any additional classes to add to the boundingBox.
581 *
582 * @attributes extraClasses
583 * @type Array
584 * @default []
585 */
586 extraClasses: {
587 value: []
16d02434
AN
588 }
589});
590
29ee3cf7
AN
591Y.Base.mix(DIALOGUE, [Y.M.core.WidgetFocusAfterHide]);
592
78686995 593M.core.dialogue = DIALOGUE;
cfa770b4
AN
594/**
595 * A dialogue type designed to display informative messages to users.
596 *
597 * @module moodle-core-notification
598 */
599
600/**
601 * Extends core Dialogue to provide a type of dialogue which can be used
602 * for informative message which are modal, and centered.
603 *
604 * @param {Object} config Object literal specifying the dialogue configuration properties.
605 * @constructor
606 * @class M.core.notification.info
607 * @extends M.core.dialogue
608 */
609var INFO = function() {
610 INFO.superclass.constructor.apply(this, arguments);
611};
612
613Y.extend(INFO, M.core.dialogue, {
614}, {
615 NAME: 'Moodle information dialogue',
616 CSS_PREFIX: DIALOGUE_PREFIX
617});
618
619Y.Base.modifyAttrs(INFO, {
620 /**
621 * Boolean indicating whether or not the Widget is visible.
622 *
623 * We override this from the default M.core.dialogue attribute value.
624 *
625 * @attribute visible
626 * @default true
627 * @type Boolean
628 */
629 visible: {
630 value: true
631 },
632
633 /**
634 * Whether the widget should be modal or not.
635 *
636 * We override this to change the default from false to true for a subset of dialogues.
637 *
638 * @attribute modal
639 * @type Boolean
640 * @default true
641 */
642 modal: {
643 validator: Y.Lang.isBoolean,
644 value: true
645 }
646});
647
648M.core.notification = M.core.notification || {};
649M.core.notification.info = INFO;
78686995
AN
650
651
29ee3cf7
AN
652}, '@VERSION@', {
653 "requires": [
654 "base",
655 "node",
656 "panel",
657 "event-key",
658 "dd-plugin",
659 "moodle-core-widget-focusafterclose",
660 "moodle-core-lockscroll"
661 ]
662});