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