[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 YUI.add('moodle-core-notification-dialogue', function (Y, NAME) { 2 3 /* eslint-disable no-unused-vars, no-unused-expressions */ 4 var DIALOGUE_PREFIX, 5 BASE, 6 CONFIRMYES, 7 CONFIRMNO, 8 TITLE, 9 QUESTION, 10 CSS; 11 12 DIALOGUE_PREFIX = 'moodle-dialogue', 13 BASE = 'notificationBase', 14 CONFIRMYES = 'yesLabel', 15 CONFIRMNO = 'noLabel', 16 TITLE = 'title', 17 QUESTION = 'question', 18 CSS = { 19 BASE: 'moodle-dialogue-base', 20 WRAP: 'moodle-dialogue-wrap', 21 HEADER: 'moodle-dialogue-hd', 22 BODY: 'moodle-dialogue-bd', 23 CONTENT: 'moodle-dialogue-content', 24 FOOTER: 'moodle-dialogue-ft', 25 HIDDEN: 'hidden', 26 LIGHTBOX: 'moodle-dialogue-lightbox' 27 }; 28 29 // Set up the namespace once. 30 M.core = M.core || {}; 31 /* global DIALOGUE_PREFIX, BASE */ 32 33 /** 34 * The generic dialogue class for use in Moodle. 35 * 36 * @module moodle-core-notification 37 * @submodule moodle-core-notification-dialogue 38 */ 39 40 var DIALOGUE_NAME = 'Moodle dialogue', 41 DIALOGUE, 42 DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen', 43 DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden', 44 DIALOGUE_SELECTOR = ' [role=dialog]', 45 MENUBAR_SELECTOR = '[role=menubar]', 46 DOT = '.', 47 HAS_ZINDEX = 'moodle-has-zindex', 48 CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]'; 49 50 /** 51 * A re-usable dialogue box with Moodle classes applied. 52 * 53 * @param {Object} c Object literal specifying the dialogue configuration properties. 54 * @constructor 55 * @class M.core.dialogue 56 * @extends Panel 57 */ 58 DIALOGUE = function(c) { 59 var config = Y.clone(c); 60 config.COUNT = Y.stamp(this); 61 var id = 'moodle-dialogue-' + config.COUNT; 62 config.notificationBase = 63 Y.Node.create('<div class="' + CSS.BASE + '">') 64 .append(Y.Node.create('<div id="' + id + '" role="dialog" ' + 65 'aria-labelledby="' + id + '-header-text" class="' + CSS.WRAP + '"></div>') 66 .append(Y.Node.create('<div id="' + id + '-header-text" class="' + CSS.HEADER + ' yui3-widget-hd"></div>')) 67 .append(Y.Node.create('<div class="' + CSS.BODY + ' yui3-widget-bd"></div>')) 68 .append(Y.Node.create('<div class="' + CSS.FOOTER + ' yui3-widget-ft"></div>'))); 69 Y.one(document.body).append(config.notificationBase); 70 71 if (config.additionalBaseClass) { 72 config.notificationBase.addClass(config.additionalBaseClass); 73 } 74 75 config.srcNode = '#' + id; 76 77 // closeButton param to keep the stable versions API. 78 if (config.closeButton === false) { 79 config.buttons = null; 80 } else { 81 config.buttons = [ 82 { 83 section: Y.WidgetStdMod.HEADER, 84 classNames: 'closebutton', 85 action: function() { 86 this.hide(); 87 } 88 } 89 ]; 90 } 91 DIALOGUE.superclass.constructor.apply(this, [config]); 92 93 if (config.closeButton !== false) { 94 // The buttons constructor does not allow custom attributes 95 this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle')); 96 } 97 }; 98 Y.extend(DIALOGUE, Y.Panel, { 99 // Window resize event listener. 100 _resizeevent: null, 101 // Orientation change event listener. 102 _orientationevent: null, 103 _calculatedzindex: false, 104 105 /** 106 * The original position of the dialogue before it was reposition to 107 * avoid browser jumping. 108 * 109 * @property _originalPosition 110 * @protected 111 * @type Array 112 */ 113 _originalPosition: null, 114 115 /** 116 * The list of elements that have been aria hidden when displaying 117 * this dialogue. 118 * 119 * @property _hiddenSiblings 120 * @protected 121 * @type Array 122 */ 123 _hiddenSiblings: null, 124 125 /** 126 * Initialise the dialogue. 127 * 128 * @method initializer 129 */ 130 initializer: function() { 131 var bb; 132 133 // Initialise the element cache. 134 this._hiddenSiblings = []; 135 136 if (this.get('render')) { 137 this.render(); 138 } 139 this.after('visibleChange', this.visibilityChanged, this); 140 if (this.get('center')) { 141 this.centerDialogue(); 142 } 143 144 if (this.get('modal')) { 145 // If we're a modal then make sure our container is ARIA 146 // hidden by default. ARIA visibility is managed for modal dialogues. 147 this.get(BASE).set('aria-hidden', 'true'); 148 this.plug(Y.M.core.LockScroll); 149 } 150 151 // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507 152 // and allow setting of z-index in theme. 153 bb = this.get('boundingBox'); 154 bb.addClass(HAS_ZINDEX); 155 156 // Add any additional classes that were specified. 157 Y.Array.each(this.get('extraClasses'), bb.addClass, bb); 158 159 if (this.get('visible')) { 160 this.applyZIndex(); 161 } 162 // Recalculate the zIndex every time the modal is altered. 163 this.on('maskShow', this.applyZIndex); 164 165 this.on('maskShow', function() { 166 // When the mask shows, position the boundingBox at the top-left of the window such that when it is 167 // focused, the position does not change. 168 var w = Y.one(Y.config.win), 169 bb = this.get('boundingBox'); 170 171 if (!this.get('center')) { 172 this._originalPosition = bb.getXY(); 173 } 174 175 if (bb.getStyle('position') !== 'fixed') { 176 // If the boundingBox has been positioned in a fixed manner, then it will not position correctly to scrollTop. 177 bb.setStyles({ 178 top: w.get('scrollTop'), 179 left: w.get('scrollLeft') 180 }); 181 } 182 }, this); 183 184 // Remove the dialogue from the DOM when it is destroyed. 185 this.after('destroyedChange', function() { 186 this.get(BASE).remove(true); 187 }, this); 188 }, 189 190 /** 191 * Either set the zindex to the supplied value, or set it to one more than the highest existing 192 * dialog in the page. 193 * 194 * @method applyZIndex 195 */ 196 applyZIndex: function() { 197 var highestzindex = 1, 198 zindexvalue = 1, 199 bb = this.get('boundingBox'), 200 ol = this.get('maskNode'), 201 zindex = this.get('zIndex'); 202 if (zindex !== 0 && !this._calculatedzindex) { 203 // The zindex was specified so we should use that. 204 bb.setStyle('zIndex', zindex); 205 } else { 206 // Determine the correct zindex by looking at all existing dialogs and menubars in the page. 207 Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) { 208 var zindex = this.findZIndex(node); 209 if (zindex > highestzindex) { 210 highestzindex = zindex; 211 } 212 }, this); 213 // Only set the zindex if we found a wrapper. 214 zindexvalue = (highestzindex + 1).toString(); 215 bb.setStyle('zIndex', zindexvalue); 216 this.set('zIndex', zindexvalue); 217 if (this.get('modal')) { 218 ol.setStyle('zIndex', zindexvalue); 219 220 // In IE8, the z-indexes do not take effect properly unless you toggle 221 // the lightbox from 'fixed' to 'static' and back. This code does so 222 // using the minimum setTimeouts that still actually work. 223 if (Y.UA.ie && Y.UA.compareVersions(Y.UA.ie, 9) < 0) { 224 setTimeout(function() { 225 ol.setStyle('position', 'static'); 226 setTimeout(function() { 227 ol.setStyle('position', 'fixed'); 228 }, 0); 229 }, 0); 230 } 231 } 232 this._calculatedzindex = true; 233 } 234 }, 235 236 /** 237 * Finds the zIndex of the given node or its parent. 238 * 239 * @method findZIndex 240 * @param {Node} node The Node to apply the zIndex to. 241 * @return {Number} Either the zIndex, or 0 if one was not found. 242 */ 243 findZIndex: function(node) { 244 // In most cases the zindex is set on the parent of the dialog. 245 var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex'); 246 if (zindex) { 247 return parseInt(zindex, 10); 248 } 249 return 0; 250 }, 251 252 /** 253 * Event listener for the visibility changed event. 254 * 255 * @method visibilityChanged 256 * @param {EventFacade} e 257 */ 258 visibilityChanged: function(e) { 259 var titlebar, bb; 260 if (e.attrName === 'visible') { 261 this.get('maskNode').addClass(CSS.LIGHTBOX); 262 // Going from visible to hidden. 263 if (e.prevVal && !e.newVal) { 264 bb = this.get('boundingBox'); 265 if (this._resizeevent) { 266 this._resizeevent.detach(); 267 this._resizeevent = null; 268 } 269 if (this._orientationevent) { 270 this._orientationevent.detach(); 271 this._orientationevent = null; 272 } 273 bb.detach('key', this.keyDelegation); 274 275 if (this.get('modal')) { 276 // Hide this dialogue from screen readers. 277 this.setAccessibilityHidden(); 278 } 279 } 280 // Going from hidden to visible. 281 if (!e.prevVal && e.newVal) { 282 // This needs to be done each time the dialog is shown as new dialogs may have been opened. 283 this.applyZIndex(); 284 // This needs to be done each time the dialog is shown as the window may have been resized. 285 this.makeResponsive(); 286 if (!this.shouldResizeFullscreen()) { 287 if (this.get('draggable')) { 288 titlebar = '#' + this.get('id') + ' .' + CSS.HEADER; 289 this.plug(Y.Plugin.Drag, {handles: [titlebar]}); 290 Y.one(titlebar).setStyle('cursor', 'move'); 291 } 292 } 293 this.keyDelegation(); 294 295 // Only do accessibility hiding for modals because the ARIA spec 296 // says that all ARIA dialogues should be modal. 297 if (this.get('modal')) { 298 // Make this dialogue visible to screen readers. 299 this.setAccessibilityVisible(); 300 } 301 } 302 if (this.get('center') && !e.prevVal && e.newVal) { 303 this.centerDialogue(); 304 } 305 } 306 }, 307 /** 308 * If the responsive attribute is set on the dialog, and the window size is 309 * smaller than the responsive width - make the dialog fullscreen. 310 * 311 * @method makeResponsive 312 */ 313 makeResponsive: function() { 314 var bb = this.get('boundingBox'); 315 316 if (this.shouldResizeFullscreen()) { 317 // Make this dialogue fullscreen on a small screen. 318 // Disable the page scrollbars. 319 320 // Size and position the fullscreen dialog. 321 322 bb.addClass(DIALOGUE_FULLSCREEN_CLASS); 323 bb.setStyles({'left': null, 324 'top': null, 325 'width': null, 326 'height': null, 327 'right': null, 328 'bottom': null}); 329 } else { 330 if (this.get('responsive')) { 331 // We must reset any of the fullscreen changes. 332 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS) 333 .setStyles({'width': this.get('width'), 334 'height': this.get('height')}); 335 } 336 } 337 338 // Update Lock scroll if the plugin is present. 339 if (this.lockScroll) { 340 this.lockScroll.updateScrollLock(this.shouldResizeFullscreen()); 341 } 342 }, 343 /** 344 * Center the dialog on the screen. 345 * 346 * @method centerDialogue 347 */ 348 centerDialogue: function() { 349 var bb = this.get('boundingBox'), 350 hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS), 351 x, 352 y; 353 354 // Don't adjust the position if we are in full screen mode. 355 if (this.shouldResizeFullscreen()) { 356 return; 357 } 358 if (hidden) { 359 bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS); 360 } 361 x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth')) / 2), 15); 362 y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight')) / 2), 15) + Y.one(window).get('scrollTop'); 363 bb.setStyles({'left': x, 'top': y}); 364 365 if (hidden) { 366 bb.addClass(DIALOGUE_HIDDEN_CLASS); 367 } 368 this.makeResponsive(); 369 }, 370 /** 371 * Return whether this dialogue should be fullscreen or not. 372 * 373 * Responsive attribute must be true and we should not be in an iframe and the screen width should 374 * be less than the responsive width. 375 * 376 * @method shouldResizeFullscreen 377 * @return {Boolean} 378 */ 379 shouldResizeFullscreen: function() { 380 return (window === window.parent) && this.get('responsive') && 381 Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth'); 382 }, 383 384 show: function() { 385 var result = null, 386 header = this.headerNode, 387 content = this.bodyNode, 388 focusSelector = this.get('focusOnShowSelector'), 389 focusNode = null; 390 391 result = DIALOGUE.superclass.show.call(this); 392 393 if (!this.get('center') && this._originalPosition) { 394 // Restore the dialogue position to it's location before it was moved at show time. 395 this.get('boundingBox').setXY(this._originalPosition); 396 } 397 398 // Try and find a node to focus on using the focusOnShowSelector attribute. 399 if (focusSelector !== null) { 400 focusNode = this.get('boundingBox').one(focusSelector); 401 } 402 if (!focusNode) { 403 // Fall back to the header or the content if no focus node was found yet. 404 if (header && header !== '') { 405 focusNode = header; 406 } else if (content && content !== '') { 407 focusNode = content; 408 } 409 } 410 if (focusNode) { 411 focusNode.focus(); 412 } 413 return result; 414 }, 415 416 hide: function(e) { 417 if (e) { 418 // If the event was closed by an escape key event, then we need to check that this 419 // dialogue is currently focused to prevent closing all dialogues in the stack. 420 if (e.type === 'key' && e.keyCode === 27 && !this.get('focused')) { 421 return; 422 } 423 } 424 425 // Unlock scroll if the plugin is present. 426 if (this.lockScroll) { 427 this.lockScroll.disableScrollLock(); 428 } 429 430 return DIALOGUE.superclass.hide.call(this, arguments); 431 }, 432 /** 433 * Setup key delegation to keep tabbing within the open dialogue. 434 * 435 * @method keyDelegation 436 */ 437 keyDelegation: function() { 438 var bb = this.get('boundingBox'); 439 bb.delegate('key', function(e) { 440 var target = e.target; 441 var direction = 'forward'; 442 if (e.shiftKey) { 443 direction = 'backward'; 444 } 445 if (this.trapFocus(target, direction)) { 446 e.preventDefault(); 447 } 448 }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this); 449 }, 450 451 /** 452 * Trap the tab focus within the open modal. 453 * 454 * @method trapFocus 455 * @param {string} target the element target 456 * @param {string} direction tab key for forward and tab+shift for backward 457 * @return {Boolean} The result of the focus action. 458 */ 459 trapFocus: function(target, direction) { 460 var bb = this.get('boundingBox'), 461 firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR), 462 lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop(); 463 464 if (target === lastitem && direction === 'forward') { // Tab key. 465 return firstitem.focus(); 466 } else if (target === firstitem && direction === 'backward') { // Tab+shift key. 467 return lastitem.focus(); 468 } 469 }, 470 471 /** 472 * Sets the appropriate aria attributes on this dialogue and the other 473 * elements in the DOM to ensure that screen readers are able to navigate 474 * the dialogue popup correctly. 475 * 476 * @method setAccessibilityVisible 477 */ 478 setAccessibilityVisible: function() { 479 // Get the element that contains this dialogue because we need it 480 // to filter out from the document.body child elements. 481 var container = this.get(BASE); 482 483 // We need to get a list containing each sibling element and the shallowest 484 // non-ancestral nodes in the DOM. We can shortcut this a little by leveraging 485 // the fact that this dialogue is always appended to the document body therefore 486 // it's siblings are the shallowest non-ancestral nodes. If that changes then 487 // this code should also be updated. 488 Y.one(document.body).get('children').each(function(node) { 489 // Skip the element that contains us. 490 if (node !== container) { 491 var hidden = node.get('aria-hidden'); 492 // If they are already hidden we can ignore them. 493 if (hidden !== 'true') { 494 // Save their current state. 495 node.setData('previous-aria-hidden', hidden); 496 this._hiddenSiblings.push(node); 497 498 // Hide this node from screen readers. 499 node.set('aria-hidden', 'true'); 500 } 501 } 502 }, this); 503 504 // Make us visible to screen readers. 505 container.set('aria-hidden', 'false'); 506 }, 507 508 /** 509 * Restores the aria visibility on the DOM elements changed when displaying 510 * the dialogue popup and makes the dialogue aria hidden to allow screen 511 * readers to navigate the main page correctly when the dialogue is closed. 512 * 513 * @method setAccessibilityHidden 514 */ 515 setAccessibilityHidden: function() { 516 var container = this.get(BASE); 517 container.set('aria-hidden', 'true'); 518 519 // Restore the sibling nodes back to their original values. 520 Y.Array.each(this._hiddenSiblings, function(node) { 521 var previousValue = node.getData('previous-aria-hidden'); 522 // If the element didn't previously have an aria-hidden attribute 523 // then we can just remove the one we set. 524 if (previousValue === null) { 525 node.removeAttribute('aria-hidden'); 526 } else { 527 // Otherwise set it back to the old value (which will be false). 528 node.set('aria-hidden', previousValue); 529 } 530 }); 531 532 // Clear the cache. No longer need to store these. 533 this._hiddenSiblings = []; 534 } 535 }, { 536 NAME: DIALOGUE_NAME, 537 CSS_PREFIX: DIALOGUE_PREFIX, 538 ATTRS: { 539 notificationBase: { 540 541 }, 542 543 /** 544 * Whether to display the dialogue modally and with a 545 * lightbox style. 546 * 547 * @attribute lightbox 548 * @type Boolean 549 * @default true 550 * @deprecated Since Moodle 2.7. Please use modal instead. 551 */ 552 lightbox: { 553 lazyAdd: false, 554 setter: function(value) { 555 Y.log("The lightbox attribute of M.core.dialogue has been deprecated since Moodle 2.7, " + 556 "please use the modal attribute instead", 557 'warn', 'moodle-core-notification-dialogue'); 558 this.set('modal', value); 559 } 560 }, 561 562 /** 563 * Whether to display a close button on the dialogue. 564 * 565 * Note, we do not recommend hiding the close button as this has 566 * potential accessibility concerns. 567 * 568 * @attribute closeButton 569 * @type Boolean 570 * @default true 571 */ 572 closeButton: { 573 validator: Y.Lang.isBoolean, 574 value: true 575 }, 576 577 /** 578 * The title for the close button if one is to be shown. 579 * 580 * @attribute closeButtonTitle 581 * @type String 582 * @default 'Close' 583 */ 584 closeButtonTitle: { 585 validator: Y.Lang.isString, 586 value: M.util.get_string('closebuttontitle', 'moodle') 587 }, 588 589 /** 590 * Whether to display the dialogue centrally on the screen. 591 * 592 * @attribute center 593 * @type Boolean 594 * @default true 595 */ 596 center: { 597 validator: Y.Lang.isBoolean, 598 value: true 599 }, 600 601 /** 602 * Whether to make the dialogue movable around the page. 603 * 604 * @attribute draggable 605 * @type Boolean 606 * @default false 607 */ 608 draggable: { 609 validator: Y.Lang.isBoolean, 610 value: false 611 }, 612 613 /** 614 * Used to generate a unique id for the dialogue. 615 * 616 * @attribute COUNT 617 * @type String 618 * @default null 619 */ 620 COUNT: { 621 value: null 622 }, 623 624 /** 625 * Used to disable the fullscreen resizing behaviour if required. 626 * 627 * @attribute responsive 628 * @type Boolean 629 * @default true 630 */ 631 responsive: { 632 validator: Y.Lang.isBoolean, 633 value: true 634 }, 635 636 /** 637 * The width that this dialogue should be resized to fullscreen. 638 * 639 * @attribute responsiveWidth 640 * @type Number 641 * @default 768 642 */ 643 responsiveWidth: { 644 value: 768 645 }, 646 647 /** 648 * Selector to a node that should recieve focus when this dialogue is shown. 649 * 650 * The default behaviour is to focus on the header. 651 * 652 * @attribute focusOnShowSelector 653 * @default null 654 * @type String 655 */ 656 focusOnShowSelector: { 657 value: null 658 } 659 660 } 661 }); 662 663 Y.Base.modifyAttrs(DIALOGUE, { 664 /** 665 * String with units, or number, representing the width of the Widget. 666 * If a number is provided, the default unit, defined by the Widgets 667 * DEF_UNIT, property is used. 668 * 669 * If a value of 'auto' is used, then an empty String is instead 670 * returned. 671 * 672 * @attribute width 673 * @default '400px' 674 * @type {String|Number} 675 */ 676 width: { 677 value: '400px', 678 setter: function(value) { 679 if (value === 'auto') { 680 return ''; 681 } 682 return value; 683 } 684 }, 685 686 /** 687 * Boolean indicating whether or not the Widget is visible. 688 * 689 * We override this from the default Widget attribute value. 690 * 691 * @attribute visible 692 * @default false 693 * @type Boolean 694 */ 695 visible: { 696 value: false 697 }, 698 699 /** 700 * A convenience Attribute, which can be used as a shortcut for the 701 * `align` Attribute. 702 * 703 * Note: We override this in Moodle such that it sets a value for the 704 * `center` attribute if set. The `centered` will always return false. 705 * 706 * @attribute centered 707 * @type Boolean|Node 708 * @default false 709 */ 710 centered: { 711 setter: function(value) { 712 if (value) { 713 this.set('center', true); 714 } 715 return false; 716 } 717 }, 718 719 /** 720 * Boolean determining whether to render the widget during initialisation. 721 * 722 * We override this to change the default from false to true for the dialogue. 723 * We then proceed to early render the dialogue during our initialisation rather than waiting 724 * for YUI to render it after that. 725 * 726 * @attribute render 727 * @type Boolean 728 * @default true 729 */ 730 render: { 731 value: true, 732 writeOnce: true 733 }, 734 735 /** 736 * Any additional classes to add to the boundingBox. 737 * 738 * @attribute extraClasses 739 * @type Array 740 * @default [] 741 */ 742 extraClasses: { 743 value: [] 744 } 745 }); 746 747 Y.Base.mix(DIALOGUE, [Y.M.core.WidgetFocusAfterHide]); 748 749 M.core.dialogue = DIALOGUE; 750 /* global DIALOGUE_PREFIX */ 751 752 /** 753 * A dialogue type designed to display informative messages to users. 754 * 755 * @module moodle-core-notification 756 */ 757 758 /** 759 * Extends core Dialogue to provide a type of dialogue which can be used 760 * for informative message which are modal, and centered. 761 * 762 * @param {Object} config Object literal specifying the dialogue configuration properties. 763 * @constructor 764 * @class M.core.notification.info 765 * @extends M.core.dialogue 766 */ 767 var INFO = function() { 768 INFO.superclass.constructor.apply(this, arguments); 769 }; 770 771 Y.extend(INFO, M.core.dialogue, { 772 initializer: function() { 773 this.show(); 774 } 775 }, { 776 NAME: 'Moodle information dialogue', 777 CSS_PREFIX: DIALOGUE_PREFIX 778 }); 779 780 Y.Base.modifyAttrs(INFO, { 781 /** 782 * Whether the widget should be modal or not. 783 * 784 * We override this to change the default from false to true for a subset of dialogues. 785 * 786 * @attribute modal 787 * @type Boolean 788 * @default true 789 */ 790 modal: { 791 validator: Y.Lang.isBoolean, 792 value: true 793 } 794 }); 795 796 M.core.notification = M.core.notification || {}; 797 M.core.notification.info = INFO; 798 799 800 }, '@VERSION@', { 801 "requires": [ 802 "base", 803 "node", 804 "panel", 805 "escape", 806 "event-key", 807 "dd-plugin", 808 "moodle-core-widget-focusafterclose", 809 "moodle-core-lockscroll" 810 ] 811 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |