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