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