[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/yui/src/notification/js/ -> dialogue.js (source)

   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;


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1