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