[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/resources/lib/oojs-ui/ -> oojs-ui.js (source)

   1  /*!
   2   * OOjs UI v0.1.0-pre (f2c3f12959)
   3   * https://www.mediawiki.org/wiki/OOjs_UI
   4   *
   5   * Copyright 2011–2014 OOjs Team and other contributors.
   6   * Released under the MIT license
   7   * http://oojs.mit-license.org
   8   *
   9   * Date: 2014-09-18T23:22:20Z
  10   */
  11  ( function ( OO ) {
  12  
  13  'use strict';
  14  
  15  /**
  16   * Namespace for all classes, static methods and static properties.
  17   *
  18   * @class
  19   * @singleton
  20   */
  21  OO.ui = {};
  22  
  23  OO.ui.bind = $.proxy;
  24  
  25  /**
  26   * @property {Object}
  27   */
  28  OO.ui.Keys = {
  29      UNDEFINED: 0,
  30      BACKSPACE: 8,
  31      DELETE: 46,
  32      LEFT: 37,
  33      RIGHT: 39,
  34      UP: 38,
  35      DOWN: 40,
  36      ENTER: 13,
  37      END: 35,
  38      HOME: 36,
  39      TAB: 9,
  40      PAGEUP: 33,
  41      PAGEDOWN: 34,
  42      ESCAPE: 27,
  43      SHIFT: 16,
  44      SPACE: 32
  45  };
  46  
  47  /**
  48   * Get the user's language and any fallback languages.
  49   *
  50   * These language codes are used to localize user interface elements in the user's language.
  51   *
  52   * In environments that provide a localization system, this function should be overridden to
  53   * return the user's language(s). The default implementation returns English (en) only.
  54   *
  55   * @return {string[]} Language codes, in descending order of priority
  56   */
  57  OO.ui.getUserLanguages = function () {
  58      return [ 'en' ];
  59  };
  60  
  61  /**
  62   * Get a value in an object keyed by language code.
  63   *
  64   * @param {Object.<string,Mixed>} obj Object keyed by language code
  65   * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
  66   * @param {string} [fallback] Fallback code, used if no matching language can be found
  67   * @return {Mixed} Local value
  68   */
  69  OO.ui.getLocalValue = function ( obj, lang, fallback ) {
  70      var i, len, langs;
  71  
  72      // Requested language
  73      if ( obj[lang] ) {
  74          return obj[lang];
  75      }
  76      // Known user language
  77      langs = OO.ui.getUserLanguages();
  78      for ( i = 0, len = langs.length; i < len; i++ ) {
  79          lang = langs[i];
  80          if ( obj[lang] ) {
  81              return obj[lang];
  82          }
  83      }
  84      // Fallback language
  85      if ( obj[fallback] ) {
  86          return obj[fallback];
  87      }
  88      // First existing language
  89      for ( lang in obj ) {
  90          return obj[lang];
  91      }
  92  
  93      return undefined;
  94  };
  95  
  96  ( function () {
  97      /**
  98       * Message store for the default implementation of OO.ui.msg
  99       *
 100       * Environments that provide a localization system should not use this, but should override
 101       * OO.ui.msg altogether.
 102       *
 103       * @private
 104       */
 105      var messages = {
 106          // Tool tip for a button that moves items in a list down one place
 107          'ooui-outline-control-move-down': 'Move item down',
 108          // Tool tip for a button that moves items in a list up one place
 109          'ooui-outline-control-move-up': 'Move item up',
 110          // Tool tip for a button that removes items from a list
 111          'ooui-outline-control-remove': 'Remove item',
 112          // Label for the toolbar group that contains a list of all other available tools
 113          'ooui-toolbar-more': 'More',
 114          // Default label for the accept button of a confirmation dialog
 115          'ooui-dialog-message-accept': 'OK',
 116          // Default label for the reject button of a confirmation dialog
 117          'ooui-dialog-message-reject': 'Cancel',
 118          // Title for process dialog error description
 119          'ooui-dialog-process-error': 'Something went wrong',
 120          // Label for process dialog dismiss error button, visible when describing errors
 121          'ooui-dialog-process-dismiss': 'Dismiss',
 122          // Label for process dialog retry action button, visible when describing recoverable errors
 123          'ooui-dialog-process-retry': 'Try again'
 124      };
 125  
 126      /**
 127       * Get a localized message.
 128       *
 129       * In environments that provide a localization system, this function should be overridden to
 130       * return the message translated in the user's language. The default implementation always returns
 131       * English messages.
 132       *
 133       * After the message key, message parameters may optionally be passed. In the default implementation,
 134       * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
 135       * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
 136       * they support unnamed, ordered message parameters.
 137       *
 138       * @abstract
 139       * @param {string} key Message key
 140       * @param {Mixed...} [params] Message parameters
 141       * @return {string} Translated message with parameters substituted
 142       */
 143      OO.ui.msg = function ( key ) {
 144          var message = messages[key], params = Array.prototype.slice.call( arguments, 1 );
 145          if ( typeof message === 'string' ) {
 146              // Perform $1 substitution
 147              message = message.replace( /\$(\d+)/g, function ( unused, n ) {
 148                  var i = parseInt( n, 10 );
 149                  return params[i - 1] !== undefined ? params[i - 1] : '$' + n;
 150              } );
 151          } else {
 152              // Return placeholder if message not found
 153              message = '[' + key + ']';
 154          }
 155          return message;
 156      };
 157  
 158      /**
 159       * Package a message and arguments for deferred resolution.
 160       *
 161       * Use this when you are statically specifying a message and the message may not yet be present.
 162       *
 163       * @param {string} key Message key
 164       * @param {Mixed...} [params] Message parameters
 165       * @return {Function} Function that returns the resolved message when executed
 166       */
 167      OO.ui.deferMsg = function () {
 168          var args = arguments;
 169          return function () {
 170              return OO.ui.msg.apply( OO.ui, args );
 171          };
 172      };
 173  
 174      /**
 175       * Resolve a message.
 176       *
 177       * If the message is a function it will be executed, otherwise it will pass through directly.
 178       *
 179       * @param {Function|string} msg Deferred message, or message text
 180       * @return {string} Resolved message
 181       */
 182      OO.ui.resolveMsg = function ( msg ) {
 183          if ( $.isFunction( msg ) ) {
 184              return msg();
 185          }
 186          return msg;
 187      };
 188  
 189  } )();
 190  
 191  /**
 192   * Element that can be marked as pending.
 193   *
 194   * @abstract
 195   * @class
 196   *
 197   * @constructor
 198   * @param {Object} [config] Configuration options
 199   */
 200  OO.ui.PendingElement = function OoUiPendingElement( config ) {
 201      // Config initialisation
 202      config = config || {};
 203  
 204      // Properties
 205      this.pending = 0;
 206      this.$pending = null;
 207  
 208      // Initialisation
 209      this.setPendingElement( config.$pending || this.$element );
 210  };
 211  
 212  /* Setup */
 213  
 214  OO.initClass( OO.ui.PendingElement );
 215  
 216  /* Methods */
 217  
 218  /**
 219   * Set the pending element (and clean up any existing one).
 220   *
 221   * @param {jQuery} $pending The element to set to pending.
 222   */
 223  OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) {
 224      if ( this.$pending ) {
 225          this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
 226      }
 227  
 228      this.$pending = $pending;
 229      if ( this.pending > 0 ) {
 230          this.$pending.addClass( 'oo-ui-pendingElement-pending' );
 231      }
 232  };
 233  
 234  /**
 235   * Check if input is pending.
 236   *
 237   * @return {boolean}
 238   */
 239  OO.ui.PendingElement.prototype.isPending = function () {
 240      return !!this.pending;
 241  };
 242  
 243  /**
 244   * Increase the pending stack.
 245   *
 246   * @chainable
 247   */
 248  OO.ui.PendingElement.prototype.pushPending = function () {
 249      if ( this.pending === 0 ) {
 250          this.$pending.addClass( 'oo-ui-pendingElement-pending' );
 251      }
 252      this.pending++;
 253  
 254      return this;
 255  };
 256  
 257  /**
 258   * Reduce the pending stack.
 259   *
 260   * Clamped at zero.
 261   *
 262   * @chainable
 263   */
 264  OO.ui.PendingElement.prototype.popPending = function () {
 265      if ( this.pending === 1 ) {
 266          this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
 267      }
 268      this.pending = Math.max( 0, this.pending - 1 );
 269  
 270      return this;
 271  };
 272  
 273  /**
 274   * List of actions.
 275   *
 276   * @abstract
 277   * @class
 278   * @mixins OO.EventEmitter
 279   *
 280   * @constructor
 281   * @param {Object} [config] Configuration options
 282   */
 283  OO.ui.ActionSet = function OoUiActionSet( config ) {
 284      // Configuration intialization
 285      config = config || {};
 286  
 287      // Mixin constructors
 288      OO.EventEmitter.call( this );
 289  
 290      // Properties
 291      this.list = [];
 292      this.categories = {
 293          actions: 'getAction',
 294          flags: 'getFlags',
 295          modes: 'getModes'
 296      };
 297      this.categorized = {};
 298      this.special = {};
 299      this.others = [];
 300      this.organized = false;
 301      this.changing = false;
 302      this.changed = false;
 303  };
 304  
 305  /* Setup */
 306  
 307  OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
 308  
 309  /* Static Properties */
 310  
 311  /**
 312   * Symbolic name of dialog.
 313   *
 314   * @abstract
 315   * @static
 316   * @inheritable
 317   * @property {string}
 318   */
 319  OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
 320  
 321  /* Events */
 322  
 323  /**
 324   * @event click
 325   * @param {OO.ui.ActionWidget} action Action that was clicked
 326   */
 327  
 328  /**
 329   * @event resize
 330   * @param {OO.ui.ActionWidget} action Action that was resized
 331   */
 332  
 333  /**
 334   * @event add
 335   * @param {OO.ui.ActionWidget[]} added Actions added
 336   */
 337  
 338  /**
 339   * @event remove
 340   * @param {OO.ui.ActionWidget[]} added Actions removed
 341   */
 342  
 343  /**
 344   * @event change
 345   */
 346  
 347  /* Methods */
 348  
 349  /**
 350   * Handle action change events.
 351   *
 352   * @fires change
 353   */
 354  OO.ui.ActionSet.prototype.onActionChange = function () {
 355      this.organized = false;
 356      if ( this.changing ) {
 357          this.changed = true;
 358      } else {
 359          this.emit( 'change' );
 360      }
 361  };
 362  
 363  /**
 364   * Check if a action is one of the special actions.
 365   *
 366   * @param {OO.ui.ActionWidget} action Action to check
 367   * @return {boolean} Action is special
 368   */
 369  OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
 370      var flag;
 371  
 372      for ( flag in this.special ) {
 373          if ( action === this.special[flag] ) {
 374              return true;
 375          }
 376      }
 377  
 378      return false;
 379  };
 380  
 381  /**
 382   * Get actions.
 383   *
 384   * @param {Object} [filters] Filters to use, omit to get all actions
 385   * @param {string|string[]} [filters.actions] Actions that actions must have
 386   * @param {string|string[]} [filters.flags] Flags that actions must have
 387   * @param {string|string[]} [filters.modes] Modes that actions must have
 388   * @param {boolean} [filters.visible] Actions must be visible
 389   * @param {boolean} [filters.disabled] Actions must be disabled
 390   * @return {OO.ui.ActionWidget[]} Actions matching all criteria
 391   */
 392  OO.ui.ActionSet.prototype.get = function ( filters ) {
 393      var i, len, list, category, actions, index, match, matches;
 394  
 395      if ( filters ) {
 396          this.organize();
 397  
 398          // Collect category candidates
 399          matches = [];
 400          for ( category in this.categorized ) {
 401              list = filters[category];
 402              if ( list ) {
 403                  if ( !Array.isArray( list ) ) {
 404                      list = [ list ];
 405                  }
 406                  for ( i = 0, len = list.length; i < len; i++ ) {
 407                      actions = this.categorized[category][list[i]];
 408                      if ( Array.isArray( actions ) ) {
 409                          matches.push.apply( matches, actions );
 410                      }
 411                  }
 412              }
 413          }
 414          // Remove by boolean filters
 415          for ( i = 0, len = matches.length; i < len; i++ ) {
 416              match = matches[i];
 417              if (
 418                  ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
 419                  ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
 420              ) {
 421                  matches.splice( i, 1 );
 422                  len--;
 423                  i--;
 424              }
 425          }
 426          // Remove duplicates
 427          for ( i = 0, len = matches.length; i < len; i++ ) {
 428              match = matches[i];
 429              index = matches.lastIndexOf( match );
 430              while ( index !== i ) {
 431                  matches.splice( index, 1 );
 432                  len--;
 433                  index = matches.lastIndexOf( match );
 434              }
 435          }
 436          return matches;
 437      }
 438      return this.list.slice();
 439  };
 440  
 441  /**
 442   * Get special actions.
 443   *
 444   * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
 445   * Special flags can be configured by changing #static-specialFlags in a subclass.
 446   *
 447   * @return {OO.ui.ActionWidget|null} Safe action
 448   */
 449  OO.ui.ActionSet.prototype.getSpecial = function () {
 450      this.organize();
 451      return $.extend( {}, this.special );
 452  };
 453  
 454  /**
 455   * Get other actions.
 456   *
 457   * Other actions include all non-special visible actions.
 458   *
 459   * @return {OO.ui.ActionWidget[]} Other actions
 460   */
 461  OO.ui.ActionSet.prototype.getOthers = function () {
 462      this.organize();
 463      return this.others.slice();
 464  };
 465  
 466  /**
 467   * Toggle actions based on their modes.
 468   *
 469   * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
 470   * visibility; matching actions will be shown, non-matching actions will be hidden.
 471   *
 472   * @param {string} mode Mode actions must have
 473   * @chainable
 474   * @fires toggle
 475   * @fires change
 476   */
 477  OO.ui.ActionSet.prototype.setMode = function ( mode ) {
 478      var i, len, action;
 479  
 480      this.changing = true;
 481      for ( i = 0, len = this.list.length; i < len; i++ ) {
 482          action = this.list[i];
 483          action.toggle( action.hasMode( mode ) );
 484      }
 485  
 486      this.organized = false;
 487      this.changing = false;
 488      this.emit( 'change' );
 489  
 490      return this;
 491  };
 492  
 493  /**
 494   * Change which actions are able to be performed.
 495   *
 496   * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
 497   *
 498   * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
 499   *   indicate actions are able to be performed
 500   * @chainable
 501   */
 502  OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
 503      var i, len, action, item;
 504  
 505      for ( i = 0, len = this.list.length; i < len; i++ ) {
 506          item = this.list[i];
 507          action = item.getAction();
 508          if ( actions[action] !== undefined ) {
 509              item.setDisabled( !actions[action] );
 510          }
 511      }
 512  
 513      return this;
 514  };
 515  
 516  /**
 517   * Executes a function once per action.
 518   *
 519   * When making changes to multiple actions, use this method instead of iterating over the actions
 520   * manually to defer emitting a change event until after all actions have been changed.
 521   *
 522   * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
 523   * @param {Function} callback Callback to run for each action; callback is invoked with three
 524   *   arguments: the action, the action's index, the list of actions being iterated over
 525   * @chainable
 526   */
 527  OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
 528      this.changed = false;
 529      this.changing = true;
 530      this.get( filter ).forEach( callback );
 531      this.changing = false;
 532      if ( this.changed ) {
 533          this.emit( 'change' );
 534      }
 535  
 536      return this;
 537  };
 538  
 539  /**
 540   * Add actions.
 541   *
 542   * @param {OO.ui.ActionWidget[]} actions Actions to add
 543   * @chainable
 544   * @fires add
 545   * @fires change
 546   */
 547  OO.ui.ActionSet.prototype.add = function ( actions ) {
 548      var i, len, action;
 549  
 550      this.changing = true;
 551      for ( i = 0, len = actions.length; i < len; i++ ) {
 552          action = actions[i];
 553          action.connect( this, {
 554              click: [ 'emit', 'click', action ],
 555              resize: [ 'emit', 'resize', action ],
 556              toggle: [ 'onActionChange' ]
 557          } );
 558          this.list.push( action );
 559      }
 560      this.organized = false;
 561      this.emit( 'add', actions );
 562      this.changing = false;
 563      this.emit( 'change' );
 564  
 565      return this;
 566  };
 567  
 568  /**
 569   * Remove actions.
 570   *
 571   * @param {OO.ui.ActionWidget[]} actions Actions to remove
 572   * @chainable
 573   * @fires remove
 574   * @fires change
 575   */
 576  OO.ui.ActionSet.prototype.remove = function ( actions ) {
 577      var i, len, index, action;
 578  
 579      this.changing = true;
 580      for ( i = 0, len = actions.length; i < len; i++ ) {
 581          action = actions[i];
 582          index = this.list.indexOf( action );
 583          if ( index !== -1 ) {
 584              action.disconnect( this );
 585              this.list.splice( index, 1 );
 586          }
 587      }
 588      this.organized = false;
 589      this.emit( 'remove', actions );
 590      this.changing = false;
 591      this.emit( 'change' );
 592  
 593      return this;
 594  };
 595  
 596  /**
 597   * Remove all actions.
 598   *
 599   * @chainable
 600   * @fires remove
 601   * @fires change
 602   */
 603  OO.ui.ActionSet.prototype.clear = function () {
 604      var i, len, action,
 605          removed = this.list.slice();
 606  
 607      this.changing = true;
 608      for ( i = 0, len = this.list.length; i < len; i++ ) {
 609          action = this.list[i];
 610          action.disconnect( this );
 611      }
 612  
 613      this.list = [];
 614  
 615      this.organized = false;
 616      this.emit( 'remove', removed );
 617      this.changing = false;
 618      this.emit( 'change' );
 619  
 620      return this;
 621  };
 622  
 623  /**
 624   * Organize actions.
 625   *
 626   * This is called whenver organized information is requested. It will only reorganize the actions
 627   * if something has changed since the last time it ran.
 628   *
 629   * @private
 630   * @chainable
 631   */
 632  OO.ui.ActionSet.prototype.organize = function () {
 633      var i, iLen, j, jLen, flag, action, category, list, item, special,
 634          specialFlags = this.constructor.static.specialFlags;
 635  
 636      if ( !this.organized ) {
 637          this.categorized = {};
 638          this.special = {};
 639          this.others = [];
 640          for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
 641              action = this.list[i];
 642              if ( action.isVisible() ) {
 643                  // Populate catgeories
 644                  for ( category in this.categories ) {
 645                      if ( !this.categorized[category] ) {
 646                          this.categorized[category] = {};
 647                      }
 648                      list = action[this.categories[category]]();
 649                      if ( !Array.isArray( list ) ) {
 650                          list = [ list ];
 651                      }
 652                      for ( j = 0, jLen = list.length; j < jLen; j++ ) {
 653                          item = list[j];
 654                          if ( !this.categorized[category][item] ) {
 655                              this.categorized[category][item] = [];
 656                          }
 657                          this.categorized[category][item].push( action );
 658                      }
 659                  }
 660                  // Populate special/others
 661                  special = false;
 662                  for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
 663                      flag = specialFlags[j];
 664                      if ( !this.special[flag] && action.hasFlag( flag ) ) {
 665                          this.special[flag] = action;
 666                          special = true;
 667                          break;
 668                      }
 669                  }
 670                  if ( !special ) {
 671                      this.others.push( action );
 672                  }
 673              }
 674          }
 675          this.organized = true;
 676      }
 677  
 678      return this;
 679  };
 680  
 681  /**
 682   * DOM element abstraction.
 683   *
 684   * @abstract
 685   * @class
 686   *
 687   * @constructor
 688   * @param {Object} [config] Configuration options
 689   * @cfg {Function} [$] jQuery for the frame the widget is in
 690   * @cfg {string[]} [classes] CSS class names
 691   * @cfg {string} [text] Text to insert
 692   * @cfg {jQuery} [$content] Content elements to append (after text)
 693   */
 694  OO.ui.Element = function OoUiElement( config ) {
 695      // Configuration initialization
 696      config = config || {};
 697  
 698      // Properties
 699      this.$ = config.$ || OO.ui.Element.getJQuery( document );
 700      this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
 701      this.elementGroup = null;
 702  
 703      // Initialization
 704      if ( $.isArray( config.classes ) ) {
 705          this.$element.addClass( config.classes.join( ' ' ) );
 706      }
 707      if ( config.text ) {
 708          this.$element.text( config.text );
 709      }
 710      if ( config.$content ) {
 711          this.$element.append( config.$content );
 712      }
 713  };
 714  
 715  /* Setup */
 716  
 717  OO.initClass( OO.ui.Element );
 718  
 719  /* Static Properties */
 720  
 721  /**
 722   * HTML tag name.
 723   *
 724   * This may be ignored if getTagName is overridden.
 725   *
 726   * @static
 727   * @inheritable
 728   * @property {string}
 729   */
 730  OO.ui.Element.static.tagName = 'div';
 731  
 732  /* Static Methods */
 733  
 734  /**
 735   * Get a jQuery function within a specific document.
 736   *
 737   * @static
 738   * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
 739   * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
 740   *   not in an iframe
 741   * @return {Function} Bound jQuery function
 742   */
 743  OO.ui.Element.getJQuery = function ( context, $iframe ) {
 744  	function wrapper( selector ) {
 745          return $( selector, wrapper.context );
 746      }
 747  
 748      wrapper.context = this.getDocument( context );
 749  
 750      if ( $iframe ) {
 751          wrapper.$iframe = $iframe;
 752      }
 753  
 754      return wrapper;
 755  };
 756  
 757  /**
 758   * Get the document of an element.
 759   *
 760   * @static
 761   * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
 762   * @return {HTMLDocument|null} Document object
 763   */
 764  OO.ui.Element.getDocument = function ( obj ) {
 765      // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
 766      return ( obj[0] && obj[0].ownerDocument ) ||
 767          // Empty jQuery selections might have a context
 768          obj.context ||
 769          // HTMLElement
 770          obj.ownerDocument ||
 771          // Window
 772          obj.document ||
 773          // HTMLDocument
 774          ( obj.nodeType === 9 && obj ) ||
 775          null;
 776  };
 777  
 778  /**
 779   * Get the window of an element or document.
 780   *
 781   * @static
 782   * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
 783   * @return {Window} Window object
 784   */
 785  OO.ui.Element.getWindow = function ( obj ) {
 786      var doc = this.getDocument( obj );
 787      return doc.parentWindow || doc.defaultView;
 788  };
 789  
 790  /**
 791   * Get the direction of an element or document.
 792   *
 793   * @static
 794   * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
 795   * @return {string} Text direction, either `ltr` or `rtl`
 796   */
 797  OO.ui.Element.getDir = function ( obj ) {
 798      var isDoc, isWin;
 799  
 800      if ( obj instanceof jQuery ) {
 801          obj = obj[0];
 802      }
 803      isDoc = obj.nodeType === 9;
 804      isWin = obj.document !== undefined;
 805      if ( isDoc || isWin ) {
 806          if ( isWin ) {
 807              obj = obj.document;
 808          }
 809          obj = obj.body;
 810      }
 811      return $( obj ).css( 'direction' );
 812  };
 813  
 814  /**
 815   * Get the offset between two frames.
 816   *
 817   * TODO: Make this function not use recursion.
 818   *
 819   * @static
 820   * @param {Window} from Window of the child frame
 821   * @param {Window} [to=window] Window of the parent frame
 822   * @param {Object} [offset] Offset to start with, used internally
 823   * @return {Object} Offset object, containing left and top properties
 824   */
 825  OO.ui.Element.getFrameOffset = function ( from, to, offset ) {
 826      var i, len, frames, frame, rect;
 827  
 828      if ( !to ) {
 829          to = window;
 830      }
 831      if ( !offset ) {
 832          offset = { top: 0, left: 0 };
 833      }
 834      if ( from.parent === from ) {
 835          return offset;
 836      }
 837  
 838      // Get iframe element
 839      frames = from.parent.document.getElementsByTagName( 'iframe' );
 840      for ( i = 0, len = frames.length; i < len; i++ ) {
 841          if ( frames[i].contentWindow === from ) {
 842              frame = frames[i];
 843              break;
 844          }
 845      }
 846  
 847      // Recursively accumulate offset values
 848      if ( frame ) {
 849          rect = frame.getBoundingClientRect();
 850          offset.left += rect.left;
 851          offset.top += rect.top;
 852          if ( from !== to ) {
 853              this.getFrameOffset( from.parent, offset );
 854          }
 855      }
 856      return offset;
 857  };
 858  
 859  /**
 860   * Get the offset between two elements.
 861   *
 862   * @static
 863   * @param {jQuery} $from
 864   * @param {jQuery} $to
 865   * @return {Object} Translated position coordinates, containing top and left properties
 866   */
 867  OO.ui.Element.getRelativePosition = function ( $from, $to ) {
 868      var from = $from.offset(),
 869          to = $to.offset();
 870      return { top: Math.round( from.top - to.top ), left: Math.round( from.left - to.left ) };
 871  };
 872  
 873  /**
 874   * Get element border sizes.
 875   *
 876   * @static
 877   * @param {HTMLElement} el Element to measure
 878   * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
 879   */
 880  OO.ui.Element.getBorders = function ( el ) {
 881      var doc = el.ownerDocument,
 882          win = doc.parentWindow || doc.defaultView,
 883          style = win && win.getComputedStyle ?
 884              win.getComputedStyle( el, null ) :
 885              el.currentStyle,
 886          $el = $( el ),
 887          top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
 888          left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
 889          bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
 890          right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
 891  
 892      return {
 893          top: Math.round( top ),
 894          left: Math.round( left ),
 895          bottom: Math.round( bottom ),
 896          right: Math.round( right )
 897      };
 898  };
 899  
 900  /**
 901   * Get dimensions of an element or window.
 902   *
 903   * @static
 904   * @param {HTMLElement|Window} el Element to measure
 905   * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
 906   */
 907  OO.ui.Element.getDimensions = function ( el ) {
 908      var $el, $win,
 909          doc = el.ownerDocument || el.document,
 910          win = doc.parentWindow || doc.defaultView;
 911  
 912      if ( win === el || el === doc.documentElement ) {
 913          $win = $( win );
 914          return {
 915              borders: { top: 0, left: 0, bottom: 0, right: 0 },
 916              scroll: {
 917                  top: $win.scrollTop(),
 918                  left: $win.scrollLeft()
 919              },
 920              scrollbar: { right: 0, bottom: 0 },
 921              rect: {
 922                  top: 0,
 923                  left: 0,
 924                  bottom: $win.innerHeight(),
 925                  right: $win.innerWidth()
 926              }
 927          };
 928      } else {
 929          $el = $( el );
 930          return {
 931              borders: this.getBorders( el ),
 932              scroll: {
 933                  top: $el.scrollTop(),
 934                  left: $el.scrollLeft()
 935              },
 936              scrollbar: {
 937                  right: $el.innerWidth() - el.clientWidth,
 938                  bottom: $el.innerHeight() - el.clientHeight
 939              },
 940              rect: el.getBoundingClientRect()
 941          };
 942      }
 943  };
 944  
 945  /**
 946   * Get closest scrollable container.
 947   *
 948   * Traverses up until either a scrollable element or the root is reached, in which case the window
 949   * will be returned.
 950   *
 951   * @static
 952   * @param {HTMLElement} el Element to find scrollable container for
 953   * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
 954   * @return {HTMLElement} Closest scrollable container
 955   */
 956  OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) {
 957      var i, val,
 958          props = [ 'overflow' ],
 959          $parent = $( el ).parent();
 960  
 961      if ( dimension === 'x' || dimension === 'y' ) {
 962          props.push( 'overflow-' + dimension );
 963      }
 964  
 965      while ( $parent.length ) {
 966          if ( $parent[0] === el.ownerDocument.body ) {
 967              return $parent[0];
 968          }
 969          i = props.length;
 970          while ( i-- ) {
 971              val = $parent.css( props[i] );
 972              if ( val === 'auto' || val === 'scroll' ) {
 973                  return $parent[0];
 974              }
 975          }
 976          $parent = $parent.parent();
 977      }
 978      return this.getDocument( el ).body;
 979  };
 980  
 981  /**
 982   * Scroll element into view.
 983   *
 984   * @static
 985   * @param {HTMLElement} el Element to scroll into view
 986   * @param {Object} [config={}] Configuration config
 987   * @param {string} [config.duration] jQuery animation duration value
 988   * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
 989   *  to scroll in both directions
 990   * @param {Function} [config.complete] Function to call when scrolling completes
 991   */
 992  OO.ui.Element.scrollIntoView = function ( el, config ) {
 993      // Configuration initialization
 994      config = config || {};
 995  
 996      var rel, anim = {},
 997          callback = typeof config.complete === 'function' && config.complete,
 998          sc = this.getClosestScrollableContainer( el, config.direction ),
 999          $sc = $( sc ),
1000          eld = this.getDimensions( el ),
1001          scd = this.getDimensions( sc ),
1002          $win = $( this.getWindow( el ) );
1003  
1004      // Compute the distances between the edges of el and the edges of the scroll viewport
1005      if ( $sc.is( 'body' ) ) {
1006          // If the scrollable container is the <body> this is easy
1007          rel = {
1008              top: eld.rect.top,
1009              bottom: $win.innerHeight() - eld.rect.bottom,
1010              left: eld.rect.left,
1011              right: $win.innerWidth() - eld.rect.right
1012          };
1013      } else {
1014          // Otherwise, we have to subtract el's coordinates from sc's coordinates
1015          rel = {
1016              top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1017              bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1018              left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1019              right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1020          };
1021      }
1022  
1023      if ( !config.direction || config.direction === 'y' ) {
1024          if ( rel.top < 0 ) {
1025              anim.scrollTop = scd.scroll.top + rel.top;
1026          } else if ( rel.top > 0 && rel.bottom < 0 ) {
1027              anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1028          }
1029      }
1030      if ( !config.direction || config.direction === 'x' ) {
1031          if ( rel.left < 0 ) {
1032              anim.scrollLeft = scd.scroll.left + rel.left;
1033          } else if ( rel.left > 0 && rel.right < 0 ) {
1034              anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1035          }
1036      }
1037      if ( !$.isEmptyObject( anim ) ) {
1038          $sc.stop( true ).animate( anim, config.duration || 'fast' );
1039          if ( callback ) {
1040              $sc.queue( function ( next ) {
1041                  callback();
1042                  next();
1043              } );
1044          }
1045      } else {
1046          if ( callback ) {
1047              callback();
1048          }
1049      }
1050  };
1051  
1052  /* Methods */
1053  
1054  /**
1055   * Get the HTML tag name.
1056   *
1057   * Override this method to base the result on instance information.
1058   *
1059   * @return {string} HTML tag name
1060   */
1061  OO.ui.Element.prototype.getTagName = function () {
1062      return this.constructor.static.tagName;
1063  };
1064  
1065  /**
1066   * Check if the element is attached to the DOM
1067   * @return {boolean} The element is attached to the DOM
1068   */
1069  OO.ui.Element.prototype.isElementAttached = function () {
1070      return $.contains( this.getElementDocument(), this.$element[0] );
1071  };
1072  
1073  /**
1074   * Get the DOM document.
1075   *
1076   * @return {HTMLDocument} Document object
1077   */
1078  OO.ui.Element.prototype.getElementDocument = function () {
1079      return OO.ui.Element.getDocument( this.$element );
1080  };
1081  
1082  /**
1083   * Get the DOM window.
1084   *
1085   * @return {Window} Window object
1086   */
1087  OO.ui.Element.prototype.getElementWindow = function () {
1088      return OO.ui.Element.getWindow( this.$element );
1089  };
1090  
1091  /**
1092   * Get closest scrollable container.
1093   */
1094  OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1095      return OO.ui.Element.getClosestScrollableContainer( this.$element[0] );
1096  };
1097  
1098  /**
1099   * Get group element is in.
1100   *
1101   * @return {OO.ui.GroupElement|null} Group element, null if none
1102   */
1103  OO.ui.Element.prototype.getElementGroup = function () {
1104      return this.elementGroup;
1105  };
1106  
1107  /**
1108   * Set group element is in.
1109   *
1110   * @param {OO.ui.GroupElement|null} group Group element, null if none
1111   * @chainable
1112   */
1113  OO.ui.Element.prototype.setElementGroup = function ( group ) {
1114      this.elementGroup = group;
1115      return this;
1116  };
1117  
1118  /**
1119   * Scroll element into view.
1120   *
1121   * @param {Object} [config={}]
1122   */
1123  OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1124      return OO.ui.Element.scrollIntoView( this.$element[0], config );
1125  };
1126  
1127  /**
1128   * Bind a handler for an event on this.$element
1129   *
1130   * @deprecated Use jQuery#on instead.
1131   * @param {string} event
1132   * @param {Function} callback
1133   */
1134  OO.ui.Element.prototype.onDOMEvent = function ( event, callback ) {
1135      OO.ui.Element.onDOMEvent( this.$element, event, callback );
1136  };
1137  
1138  /**
1139   * Unbind a handler bound with #offDOMEvent
1140   *
1141   * @deprecated Use jQuery#off instead.
1142   * @param {string} event
1143   * @param {Function} callback
1144   */
1145  OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) {
1146      OO.ui.Element.offDOMEvent( this.$element, event, callback );
1147  };
1148  
1149  ( function () {
1150      /**
1151       * Bind a handler for an event on a DOM element.
1152       *
1153       * Used to be for working around a jQuery bug (jqbug.com/14180),
1154       * but obsolete as of jQuery 1.11.0.
1155       *
1156       * @static
1157       * @deprecated Use jQuery#on instead.
1158       * @param {HTMLElement|jQuery} el DOM element
1159       * @param {string} event Event to bind
1160       * @param {Function} callback Callback to call when the event fires
1161       */
1162      OO.ui.Element.onDOMEvent = function ( el, event, callback ) {
1163          $( el ).on( event, callback );
1164      };
1165  
1166      /**
1167       * Unbind a handler bound with #static-method-onDOMEvent.
1168       *
1169       * @deprecated Use jQuery#off instead.
1170       * @static
1171       * @param {HTMLElement|jQuery} el DOM element
1172       * @param {string} event Event to unbind
1173       * @param {Function} [callback] Callback to unbind
1174       */
1175      OO.ui.Element.offDOMEvent = function ( el, event, callback ) {
1176          $( el ).off( event, callback );
1177      };
1178  }() );
1179  
1180  /**
1181   * Container for elements.
1182   *
1183   * @abstract
1184   * @class
1185   * @extends OO.ui.Element
1186   * @mixins OO.EventEmitter
1187   *
1188   * @constructor
1189   * @param {Object} [config] Configuration options
1190   */
1191  OO.ui.Layout = function OoUiLayout( config ) {
1192      // Initialize config
1193      config = config || {};
1194  
1195      // Parent constructor
1196      OO.ui.Layout.super.call( this, config );
1197  
1198      // Mixin constructors
1199      OO.EventEmitter.call( this );
1200  
1201      // Initialization
1202      this.$element.addClass( 'oo-ui-layout' );
1203  };
1204  
1205  /* Setup */
1206  
1207  OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1208  OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1209  
1210  /**
1211   * User interface control.
1212   *
1213   * @abstract
1214   * @class
1215   * @extends OO.ui.Element
1216   * @mixins OO.EventEmitter
1217   *
1218   * @constructor
1219   * @param {Object} [config] Configuration options
1220   * @cfg {boolean} [disabled=false] Disable
1221   */
1222  OO.ui.Widget = function OoUiWidget( config ) {
1223      // Initialize config
1224      config = $.extend( { disabled: false }, config );
1225  
1226      // Parent constructor
1227      OO.ui.Widget.super.call( this, config );
1228  
1229      // Mixin constructors
1230      OO.EventEmitter.call( this );
1231  
1232      // Properties
1233      this.visible = true;
1234      this.disabled = null;
1235      this.wasDisabled = null;
1236  
1237      // Initialization
1238      this.$element.addClass( 'oo-ui-widget' );
1239      this.setDisabled( !!config.disabled );
1240  };
1241  
1242  /* Setup */
1243  
1244  OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1245  OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1246  
1247  /* Events */
1248  
1249  /**
1250   * @event disable
1251   * @param {boolean} disabled Widget is disabled
1252   */
1253  
1254  /**
1255   * @event toggle
1256   * @param {boolean} visible Widget is visible
1257   */
1258  
1259  /* Methods */
1260  
1261  /**
1262   * Check if the widget is disabled.
1263   *
1264   * @param {boolean} Button is disabled
1265   */
1266  OO.ui.Widget.prototype.isDisabled = function () {
1267      return this.disabled;
1268  };
1269  
1270  /**
1271   * Check if widget is visible.
1272   *
1273   * @return {boolean} Widget is visible
1274   */
1275  OO.ui.Widget.prototype.isVisible = function () {
1276      return this.visible;
1277  };
1278  
1279  /**
1280   * Set the disabled state of the widget.
1281   *
1282   * This should probably change the widgets' appearance and prevent it from being used.
1283   *
1284   * @param {boolean} disabled Disable widget
1285   * @chainable
1286   */
1287  OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1288      var isDisabled;
1289  
1290      this.disabled = !!disabled;
1291      isDisabled = this.isDisabled();
1292      if ( isDisabled !== this.wasDisabled ) {
1293          this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1294          this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1295          this.emit( 'disable', isDisabled );
1296      }
1297      this.wasDisabled = isDisabled;
1298  
1299      return this;
1300  };
1301  
1302  /**
1303   * Toggle visibility of widget.
1304   *
1305   * @param {boolean} [show] Make widget visible, omit to toggle visibility
1306   * @fires visible
1307   * @chainable
1308   */
1309  OO.ui.Widget.prototype.toggle = function ( show ) {
1310      show = show === undefined ? !this.visible : !!show;
1311  
1312      if ( show !== this.isVisible() ) {
1313          this.visible = show;
1314          this.$element.toggle( show );
1315          this.emit( 'toggle', show );
1316      }
1317  
1318      return this;
1319  };
1320  
1321  /**
1322   * Update the disabled state, in case of changes in parent widget.
1323   *
1324   * @chainable
1325   */
1326  OO.ui.Widget.prototype.updateDisabled = function () {
1327      this.setDisabled( this.disabled );
1328      return this;
1329  };
1330  
1331  /**
1332   * Container for elements in a child frame.
1333   *
1334   * Use together with OO.ui.WindowManager.
1335   *
1336   * @abstract
1337   * @class
1338   * @extends OO.ui.Element
1339   * @mixins OO.EventEmitter
1340   *
1341   * When a window is opened, the setup and ready processes are executed. Similarly, the hold and
1342   * teardown processes are executed when the window is closed.
1343   *
1344   * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening
1345   * - Window manager begins opening window
1346   * - {@link #getSetupProcess} method is called and its result executed
1347   * - {@link #getReadyProcess} method is called and its result executed
1348   * - Window is now open
1349   *
1350   * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing
1351   * - Window manager begins closing window
1352   * - {@link #getHoldProcess} method is called and its result executed
1353   * - {@link #getTeardownProcess} method is called and its result executed
1354   * - Window is now closed
1355   *
1356   * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding
1357   * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and
1358   * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchonous
1359   * processing can complete. Always assume window processes are executed asychronously. See
1360   * OO.ui.Process for more details about how to work with processes. Some events, as well as the
1361   * #open and #close methods, provide promises which are resolved when the window enters a new state.
1362   *
1363   * Sizing of windows is specified using symbolic names which are interpreted by the window manager.
1364   * If the requested size is not recognized, the window manager will choose a sensible fallback.
1365   *
1366   * @constructor
1367   * @param {Object} [config] Configuration options
1368   * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to
1369   *   use #static-size
1370   * @fires initialize
1371   */
1372  OO.ui.Window = function OoUiWindow( config ) {
1373      // Configuration initialization
1374      config = config || {};
1375  
1376      // Parent constructor
1377      OO.ui.Window.super.call( this, config );
1378  
1379      // Mixin constructors
1380      OO.EventEmitter.call( this );
1381  
1382      // Properties
1383      this.manager = null;
1384      this.initialized = false;
1385      this.visible = false;
1386      this.opening = null;
1387      this.closing = null;
1388      this.opened = null;
1389      this.timing = null;
1390      this.loading = null;
1391      this.size = config.size || this.constructor.static.size;
1392      this.$frame = this.$( '<div>' );
1393  
1394      // Initialization
1395      this.$element
1396          .addClass( 'oo-ui-window' )
1397          .append( this.$frame );
1398      this.$frame.addClass( 'oo-ui-window-frame' );
1399  
1400      // NOTE: Additional intitialization will occur when #setManager is called
1401  };
1402  
1403  /* Setup */
1404  
1405  OO.inheritClass( OO.ui.Window, OO.ui.Element );
1406  OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1407  
1408  /* Events */
1409  
1410  /**
1411   * @event resize
1412   * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1413   */
1414  
1415  /* Static Properties */
1416  
1417  /**
1418   * Symbolic name of size.
1419   *
1420   * Size is used if no size is configured during construction.
1421   *
1422   * @static
1423   * @inheritable
1424   * @property {string}
1425   */
1426  OO.ui.Window.static.size = 'medium';
1427  
1428  /* Static Methods */
1429  
1430  /**
1431   * Transplant the CSS styles from as parent document to a frame's document.
1432   *
1433   * This loops over the style sheets in the parent document, and copies their nodes to the
1434   * frame's document. It then polls the document to see when all styles have loaded, and once they
1435   * have, resolves the promise.
1436   *
1437   * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
1438   * and resolve the promise anyway. This protects against cases like a display: none; iframe in
1439   * Firefox, where the styles won't load until the iframe becomes visible.
1440   *
1441   * For details of how we arrived at the strategy used in this function, see #load.
1442   *
1443   * @static
1444   * @inheritable
1445   * @param {HTMLDocument} parentDoc Document to transplant styles from
1446   * @param {HTMLDocument} frameDoc Document to transplant styles to
1447   * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
1448   * @return {jQuery.Promise} Promise resolved when styles have loaded
1449   */
1450  OO.ui.Window.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) {
1451      var i, numSheets, styleNode, styleText, newNode, timeoutID, pollNodeId, $pendingPollNodes,
1452          $pollNodes = $( [] ),
1453          // Fake font-family value
1454          fontFamily = 'oo-ui-frame-transplantStyles-loaded',
1455          nextIndex = parentDoc.oouiFrameTransplantStylesNextIndex || 0,
1456          deferred = $.Deferred();
1457  
1458      for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
1459          styleNode = parentDoc.styleSheets[i].ownerNode;
1460          if ( styleNode.disabled ) {
1461              continue;
1462          }
1463  
1464          if ( styleNode.nodeName.toLowerCase() === 'link' ) {
1465              // External stylesheet; use @import
1466              styleText = '@import url(' + styleNode.href + ');';
1467          } else {
1468              // Internal stylesheet; just copy the text
1469              // For IE10 we need to fall back to .cssText, BUT that's undefined in
1470              // other browsers, so fall back to '' rather than 'undefined'
1471              styleText = styleNode.textContent || parentDoc.styleSheets[i].cssText || '';
1472          }
1473  
1474          // Create a node with a unique ID that we're going to monitor to see when the CSS
1475          // has loaded
1476          if ( styleNode.oouiFrameTransplantStylesId ) {
1477              // If we're nesting transplantStyles operations and this node already has
1478              // a CSS rule to wait for loading, reuse it
1479              pollNodeId = styleNode.oouiFrameTransplantStylesId;
1480          } else {
1481              // Otherwise, create a new ID
1482              pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + nextIndex;
1483              nextIndex++;
1484  
1485              // Add #pollNodeId { font-family: ... } to the end of the stylesheet / after the @import
1486              // The font-family rule will only take effect once the @import finishes
1487              styleText += '\n' + '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
1488          }
1489  
1490          // Create a node with id=pollNodeId
1491          $pollNodes = $pollNodes.add( $( '<div>', frameDoc )
1492              .attr( 'id', pollNodeId )
1493              .appendTo( frameDoc.body )
1494          );
1495  
1496          // Add our modified CSS as a <style> tag
1497          newNode = frameDoc.createElement( 'style' );
1498          newNode.textContent = styleText;
1499          newNode.oouiFrameTransplantStylesId = pollNodeId;
1500          frameDoc.head.appendChild( newNode );
1501      }
1502      frameDoc.oouiFrameTransplantStylesNextIndex = nextIndex;
1503  
1504      // Poll every 100ms until all external stylesheets have loaded
1505      $pendingPollNodes = $pollNodes;
1506      timeoutID = setTimeout( function pollExternalStylesheets() {
1507          while (
1508              $pendingPollNodes.length > 0 &&
1509              $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
1510          ) {
1511              $pendingPollNodes = $pendingPollNodes.slice( 1 );
1512          }
1513  
1514          if ( $pendingPollNodes.length === 0 ) {
1515              // We're done!
1516              if ( timeoutID !== null ) {
1517                  timeoutID = null;
1518                  $pollNodes.remove();
1519                  deferred.resolve();
1520              }
1521          } else {
1522              timeoutID = setTimeout( pollExternalStylesheets, 100 );
1523          }
1524      }, 100 );
1525      // ...but give up after a while
1526      if ( timeout !== 0 ) {
1527          setTimeout( function () {
1528              if ( timeoutID ) {
1529                  clearTimeout( timeoutID );
1530                  timeoutID = null;
1531                  $pollNodes.remove();
1532                  deferred.reject();
1533              }
1534          }, timeout || 5000 );
1535      }
1536  
1537      return deferred.promise();
1538  };
1539  
1540  /* Methods */
1541  
1542  /**
1543   * Handle mouse down events.
1544   *
1545   * @param {jQuery.Event} e Mouse down event
1546   */
1547  OO.ui.Window.prototype.onMouseDown = function ( e ) {
1548      // Prevent clicking on the click-block from stealing focus
1549      if ( e.target === this.$element[0] ) {
1550          return false;
1551      }
1552  };
1553  
1554  /**
1555   * Check if window has been initialized.
1556   *
1557   * @return {boolean} Window has been initialized
1558   */
1559  OO.ui.Window.prototype.isInitialized = function () {
1560      return this.initialized;
1561  };
1562  
1563  /**
1564   * Check if window is visible.
1565   *
1566   * @return {boolean} Window is visible
1567   */
1568  OO.ui.Window.prototype.isVisible = function () {
1569      return this.visible;
1570  };
1571  
1572  /**
1573   * Check if window is loading.
1574   *
1575   * @return {boolean} Window is loading
1576   */
1577  OO.ui.Window.prototype.isLoading = function () {
1578      return this.loading && this.loading.state() === 'pending';
1579  };
1580  
1581  /**
1582   * Check if window is loaded.
1583   *
1584   * @return {boolean} Window is loaded
1585   */
1586  OO.ui.Window.prototype.isLoaded = function () {
1587      return this.loading && this.loading.state() === 'resolved';
1588  };
1589  
1590  /**
1591   * Check if window is opening.
1592   *
1593   * This is a wrapper around OO.ui.WindowManager#isOpening.
1594   *
1595   * @return {boolean} Window is opening
1596   */
1597  OO.ui.Window.prototype.isOpening = function () {
1598      return this.manager.isOpening( this );
1599  };
1600  
1601  /**
1602   * Check if window is closing.
1603   *
1604   * This is a wrapper around OO.ui.WindowManager#isClosing.
1605   *
1606   * @return {boolean} Window is closing
1607   */
1608  OO.ui.Window.prototype.isClosing = function () {
1609      return this.manager.isClosing( this );
1610  };
1611  
1612  /**
1613   * Check if window is opened.
1614   *
1615   * This is a wrapper around OO.ui.WindowManager#isOpened.
1616   *
1617   * @return {boolean} Window is opened
1618   */
1619  OO.ui.Window.prototype.isOpened = function () {
1620      return this.manager.isOpened( this );
1621  };
1622  
1623  /**
1624   * Get the window manager.
1625   *
1626   * @return {OO.ui.WindowManager} Manager of window
1627   */
1628  OO.ui.Window.prototype.getManager = function () {
1629      return this.manager;
1630  };
1631  
1632  /**
1633   * Get the window size.
1634   *
1635   * @return {string} Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1636   */
1637  OO.ui.Window.prototype.getSize = function () {
1638      return this.size;
1639  };
1640  
1641  /**
1642   * Get the height of the dialog contents.
1643   *
1644   * @return {number} Content height
1645   */
1646  OO.ui.Window.prototype.getContentHeight = function () {
1647      // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements
1648      var bodyHeight, oldHeight = this.$frame[0].style.height;
1649      this.$frame[0].style.height = '1px';
1650      bodyHeight = this.getBodyHeight();
1651      this.$frame[0].style.height = oldHeight;
1652  
1653      return Math.round(
1654          // Add buffer for border
1655          ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1656          // Use combined heights of children
1657          ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1658      );
1659  };
1660  
1661  /**
1662   * Get the height of the dialog contents.
1663   *
1664   * When this function is called, the dialog will temporarily have been resized
1665   * to height=1px, so .scrollHeight measurements can be taken accurately.
1666   *
1667   * @return {number} Height of content
1668   */
1669  OO.ui.Window.prototype.getBodyHeight = function () {
1670      return this.$body[0].scrollHeight;
1671  };
1672  
1673  /**
1674   * Get the directionality of the frame
1675   *
1676   * @return {string} Directionality, 'ltr' or 'rtl'
1677   */
1678  OO.ui.Window.prototype.getDir = function () {
1679      return this.dir;
1680  };
1681  
1682  /**
1683   * Get a process for setting up a window for use.
1684   *
1685   * Each time the window is opened this process will set it up for use in a particular context, based
1686   * on the `data` argument.
1687   *
1688   * When you override this method, you can add additional setup steps to the process the parent
1689   * method provides using the 'first' and 'next' methods.
1690   *
1691   * @abstract
1692   * @param {Object} [data] Window opening data
1693   * @return {OO.ui.Process} Setup process
1694   */
1695  OO.ui.Window.prototype.getSetupProcess = function () {
1696      return new OO.ui.Process();
1697  };
1698  
1699  /**
1700   * Get a process for readying a window for use.
1701   *
1702   * Each time the window is open and setup, this process will ready it up for use in a particular
1703   * context, based on the `data` argument.
1704   *
1705   * When you override this method, you can add additional setup steps to the process the parent
1706   * method provides using the 'first' and 'next' methods.
1707   *
1708   * @abstract
1709   * @param {Object} [data] Window opening data
1710   * @return {OO.ui.Process} Setup process
1711   */
1712  OO.ui.Window.prototype.getReadyProcess = function () {
1713      return new OO.ui.Process();
1714  };
1715  
1716  /**
1717   * Get a process for holding a window from use.
1718   *
1719   * Each time the window is closed, this process will hold it from use in a particular context, based
1720   * on the `data` argument.
1721   *
1722   * When you override this method, you can add additional setup steps to the process the parent
1723   * method provides using the 'first' and 'next' methods.
1724   *
1725   * @abstract
1726   * @param {Object} [data] Window closing data
1727   * @return {OO.ui.Process} Hold process
1728   */
1729  OO.ui.Window.prototype.getHoldProcess = function () {
1730      return new OO.ui.Process();
1731  };
1732  
1733  /**
1734   * Get a process for tearing down a window after use.
1735   *
1736   * Each time the window is closed this process will tear it down and do something with the user's
1737   * interactions within the window, based on the `data` argument.
1738   *
1739   * When you override this method, you can add additional teardown steps to the process the parent
1740   * method provides using the 'first' and 'next' methods.
1741   *
1742   * @abstract
1743   * @param {Object} [data] Window closing data
1744   * @return {OO.ui.Process} Teardown process
1745   */
1746  OO.ui.Window.prototype.getTeardownProcess = function () {
1747      return new OO.ui.Process();
1748  };
1749  
1750  /**
1751   * Toggle visibility of window.
1752   *
1753   * If the window is isolated and hasn't fully loaded yet, the visiblity property will be used
1754   * instead of display.
1755   *
1756   * @param {boolean} [show] Make window visible, omit to toggle visibility
1757   * @fires visible
1758   * @chainable
1759   */
1760  OO.ui.Window.prototype.toggle = function ( show ) {
1761      show = show === undefined ? !this.visible : !!show;
1762  
1763      if ( show !== this.isVisible() ) {
1764          this.visible = show;
1765  
1766          if ( this.isolated && !this.isLoaded() ) {
1767              // Hide the window using visibility instead of display until loading is complete
1768              // Can't use display: none; because that prevents the iframe from loading in Firefox
1769              this.$element.css( 'visibility', show ? 'visible' : 'hidden' );
1770          } else {
1771              this.$element.toggle( show ).css( 'visibility', '' );
1772          }
1773          this.emit( 'toggle', show );
1774      }
1775  
1776      return this;
1777  };
1778  
1779  /**
1780   * Set the window manager.
1781   *
1782   * This must be called before initialize. Calling it more than once will cause an error.
1783   *
1784   * @param {OO.ui.WindowManager} manager Manager for this window
1785   * @throws {Error} If called more than once
1786   * @chainable
1787   */
1788  OO.ui.Window.prototype.setManager = function ( manager ) {
1789      if ( this.manager ) {
1790          throw new Error( 'Cannot set window manager, window already has a manager' );
1791      }
1792  
1793      // Properties
1794      this.manager = manager;
1795      this.isolated = manager.shouldIsolate();
1796  
1797      // Initialization
1798      if ( this.isolated ) {
1799          this.$iframe = this.$( '<iframe>' );
1800          this.$iframe.attr( { frameborder: 0, scrolling: 'no' } );
1801          this.$frame.append( this.$iframe );
1802          this.$ = function () {
1803              throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
1804          };
1805          // WARNING: Do not use this.$ again until #initialize is called
1806      } else {
1807          this.$content = this.$( '<div>' );
1808          this.$document = $( this.getElementDocument() );
1809          this.$content.addClass( 'oo-ui-window-content' );
1810          this.$frame.append( this.$content );
1811      }
1812      this.toggle( false );
1813  
1814      // Figure out directionality:
1815      this.dir = OO.ui.Element.getDir( this.$iframe || this.$content ) || 'ltr';
1816  
1817      return this;
1818  };
1819  
1820  /**
1821   * Set the window size.
1822   *
1823   * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1824   * @chainable
1825   */
1826  OO.ui.Window.prototype.setSize = function ( size ) {
1827      this.size = size;
1828      this.manager.updateWindowSize( this );
1829      return this;
1830  };
1831  
1832  /**
1833   * Set window dimensions.
1834   *
1835   * Properties are applied to the frame container.
1836   *
1837   * @param {Object} dim CSS dimension properties
1838   * @param {string|number} [dim.width] Width
1839   * @param {string|number} [dim.minWidth] Minimum width
1840   * @param {string|number} [dim.maxWidth] Maximum width
1841   * @param {string|number} [dim.width] Height, omit to set based on height of contents
1842   * @param {string|number} [dim.minWidth] Minimum height
1843   * @param {string|number} [dim.maxWidth] Maximum height
1844   * @chainable
1845   */
1846  OO.ui.Window.prototype.setDimensions = function ( dim ) {
1847      // Apply width before height so height is not based on wrapping content using the wrong width
1848      this.$frame.css( {
1849          width: dim.width || '',
1850          minWidth: dim.minWidth || '',
1851          maxWidth: dim.maxWidth || ''
1852      } );
1853      this.$frame.css( {
1854          height: ( dim.height !== undefined ? dim.height : this.getContentHeight() ) || '',
1855          minHeight: dim.minHeight || '',
1856          maxHeight: dim.maxHeight || ''
1857      } );
1858      return this;
1859  };
1860  
1861  /**
1862   * Initialize window contents.
1863   *
1864   * The first time the window is opened, #initialize is called when it's safe to begin populating
1865   * its contents. See #getSetupProcess for a way to make changes each time the window opens.
1866   *
1867   * Once this method is called, this.$ can be used to create elements within the frame.
1868   *
1869   * @throws {Error} If not attached to a manager
1870   * @chainable
1871   */
1872  OO.ui.Window.prototype.initialize = function () {
1873      if ( !this.manager ) {
1874          throw new Error( 'Cannot initialize window, must be attached to a manager' );
1875      }
1876  
1877      // Properties
1878      this.$head = this.$( '<div>' );
1879      this.$body = this.$( '<div>' );
1880      this.$foot = this.$( '<div>' );
1881      this.$overlay = this.$( '<div>' );
1882  
1883      // Events
1884      this.$element.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
1885  
1886      // Initialization
1887      this.$head.addClass( 'oo-ui-window-head' );
1888      this.$body.addClass( 'oo-ui-window-body' );
1889      this.$foot.addClass( 'oo-ui-window-foot' );
1890      this.$overlay.addClass( 'oo-ui-window-overlay' );
1891      this.$content.append( this.$head, this.$body, this.$foot, this.$overlay );
1892  
1893      return this;
1894  };
1895  
1896  /**
1897   * Open window.
1898   *
1899   * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
1900   * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
1901   *
1902   * @param {Object} [data] Window opening data
1903   * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
1904   *   first argument will be a promise which will be resolved when the window begins closing
1905   */
1906  OO.ui.Window.prototype.open = function ( data ) {
1907      return this.manager.openWindow( this, data );
1908  };
1909  
1910  /**
1911   * Close window.
1912   *
1913   * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
1914   * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
1915   *
1916   * @param {Object} [data] Window closing data
1917   * @return {jQuery.Promise} Promise resolved when window is closed
1918   */
1919  OO.ui.Window.prototype.close = function ( data ) {
1920      return this.manager.closeWindow( this, data );
1921  };
1922  
1923  /**
1924   * Setup window.
1925   *
1926   * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
1927   * by other systems.
1928   *
1929   * @param {Object} [data] Window opening data
1930   * @return {jQuery.Promise} Promise resolved when window is setup
1931   */
1932  OO.ui.Window.prototype.setup = function ( data ) {
1933      var win = this,
1934          deferred = $.Deferred();
1935  
1936      this.$element.show();
1937      this.visible = true;
1938      this.getSetupProcess( data ).execute().done( function () {
1939          // Force redraw by asking the browser to measure the elements' widths
1940          win.$element.addClass( 'oo-ui-window-setup' ).width();
1941          win.$content.addClass( 'oo-ui-window-content-setup' ).width();
1942          deferred.resolve();
1943      } );
1944  
1945      return deferred.promise();
1946  };
1947  
1948  /**
1949   * Ready window.
1950   *
1951   * This is called by OO.ui.WindowManager durring window opening, and should not be called directly
1952   * by other systems.
1953   *
1954   * @param {Object} [data] Window opening data
1955   * @return {jQuery.Promise} Promise resolved when window is ready
1956   */
1957  OO.ui.Window.prototype.ready = function ( data ) {
1958      var win = this,
1959          deferred = $.Deferred();
1960  
1961      this.$content.focus();
1962      this.getReadyProcess( data ).execute().done( function () {
1963          // Force redraw by asking the browser to measure the elements' widths
1964          win.$element.addClass( 'oo-ui-window-ready' ).width();
1965          win.$content.addClass( 'oo-ui-window-content-ready' ).width();
1966          deferred.resolve();
1967      } );
1968  
1969      return deferred.promise();
1970  };
1971  
1972  /**
1973   * Hold window.
1974   *
1975   * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
1976   * by other systems.
1977   *
1978   * @param {Object} [data] Window closing data
1979   * @return {jQuery.Promise} Promise resolved when window is held
1980   */
1981  OO.ui.Window.prototype.hold = function ( data ) {
1982      var win = this,
1983          deferred = $.Deferred();
1984  
1985      this.getHoldProcess( data ).execute().done( function () {
1986          // Get the focused element within the window's content
1987          var $focus = win.$content.find( OO.ui.Element.getDocument( win.$content ).activeElement );
1988  
1989          // Blur the focused element
1990          if ( $focus.length ) {
1991              $focus[0].blur();
1992          }
1993  
1994          // Force redraw by asking the browser to measure the elements' widths
1995          win.$element.removeClass( 'oo-ui-window-ready' ).width();
1996          win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
1997          deferred.resolve();
1998      } );
1999  
2000      return deferred.promise();
2001  };
2002  
2003  /**
2004   * Teardown window.
2005   *
2006   * This is called by OO.ui.WindowManager durring window closing, and should not be called directly
2007   * by other systems.
2008   *
2009   * @param {Object} [data] Window closing data
2010   * @return {jQuery.Promise} Promise resolved when window is torn down
2011   */
2012  OO.ui.Window.prototype.teardown = function ( data ) {
2013      var win = this,
2014          deferred = $.Deferred();
2015  
2016      this.getTeardownProcess( data ).execute().done( function () {
2017          // Force redraw by asking the browser to measure the elements' widths
2018          win.$element.removeClass( 'oo-ui-window-setup' ).width();
2019          win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2020          win.$element.hide();
2021          win.visible = false;
2022          deferred.resolve();
2023      } );
2024  
2025      return deferred.promise();
2026  };
2027  
2028  /**
2029   * Load the frame contents.
2030   *
2031   * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned
2032   * promise will be resolved. Calling while loading will return a promise but not trigger a new
2033   * loading cycle. Calling after loading is complete will return a promise that's already been
2034   * resolved.
2035   *
2036   * Sounds simple right? Read on...
2037   *
2038   * When you create a dynamic iframe using open/write/close, the window.load event for the
2039   * iframe is triggered when you call close, and there's no further load event to indicate that
2040   * everything is actually loaded.
2041   *
2042   * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
2043   * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
2044   * are added to document.styleSheets immediately, and the only way you can determine whether they've
2045   * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
2046   * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
2047   *
2048   * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>`
2049   * tags. Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets
2050   * until the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the
2051   * `@import` has finished. And because the contents of the `<style>` tag are from the same origin,
2052   * accessing .cssRules is allowed.
2053   *
2054   * However, now that we control the styles we're injecting, we might as well do away with
2055   * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
2056   * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
2057   * and wait for its font-family to change to someValue. Because `@import` is blocking, the
2058   * font-family rule is not applied until after the `@import` finishes.
2059   *
2060   * All this stylesheet injection and polling magic is in #transplantStyles.
2061   *
2062   * @return {jQuery.Promise} Promise resolved when loading is complete
2063   * @fires load
2064   */
2065  OO.ui.Window.prototype.load = function () {
2066      var sub, doc, loading,
2067          win = this;
2068  
2069      // Non-isolated windows are already "loaded"
2070      if ( !this.loading && !this.isolated ) {
2071          this.loading = $.Deferred().resolve();
2072          this.initialize();
2073          // Set initialized state after so sub-classes aren't confused by it being set by calling
2074          // their parent initialize method
2075          this.initialized = true;
2076      }
2077  
2078      // Return existing promise if already loading or loaded
2079      if ( this.loading ) {
2080          return this.loading.promise();
2081      }
2082  
2083      // Load the frame
2084      loading = this.loading = $.Deferred();
2085      sub = this.$iframe.prop( 'contentWindow' );
2086      doc = sub.document;
2087  
2088      // Initialize contents
2089      doc.open();
2090      doc.write(
2091          '<!doctype html>' +
2092          '<html>' +
2093              '<body class="oo-ui-window-isolated oo-ui-' + this.dir + '"' +
2094                  ' style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
2095                  '<div class="oo-ui-window-content"></div>' +
2096              '</body>' +
2097          '</html>'
2098      );
2099      doc.close();
2100  
2101      // Properties
2102      this.$ = OO.ui.Element.getJQuery( doc, this.$element );
2103      this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
2104      this.$document = this.$( doc );
2105  
2106      // Initialization
2107      this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
2108          .always( function () {
2109              // Initialize isolated windows
2110              win.initialize();
2111              // Set initialized state after so sub-classes aren't confused by it being set by calling
2112              // their parent initialize method
2113              win.initialized = true;
2114              // Undo the visibility: hidden; hack and apply display: none;
2115              // We can do this safely now that the iframe has initialized
2116              // (don't do this from within #initialize because it has to happen
2117              // after the all subclasses have been handled as well).
2118              win.toggle( win.isVisible() );
2119  
2120              loading.resolve();
2121          } );
2122  
2123      return loading.promise();
2124  };
2125  
2126  /**
2127   * Base class for all dialogs.
2128   *
2129   * Logic:
2130   * - Manage the window (open and close, etc.).
2131   * - Store the internal name and display title.
2132   * - A stack to track one or more pending actions.
2133   * - Manage a set of actions that can be performed.
2134   * - Configure and create action widgets.
2135   *
2136   * User interface:
2137   * - Close the dialog with Escape key.
2138   * - Visually lock the dialog while an action is in
2139   *   progress (aka "pending").
2140   *
2141   * Subclass responsibilities:
2142   * - Display the title somewhere.
2143   * - Add content to the dialog.
2144   * - Provide a UI to close the dialog.
2145   * - Display the action widgets somewhere.
2146   *
2147   * @abstract
2148   * @class
2149   * @extends OO.ui.Window
2150   * @mixins OO.ui.PendingElement
2151   *
2152   * @constructor
2153   * @param {Object} [config] Configuration options
2154   */
2155  OO.ui.Dialog = function OoUiDialog( config ) {
2156      // Parent constructor
2157      OO.ui.Dialog.super.call( this, config );
2158  
2159      // Mixin constructors
2160      OO.ui.PendingElement.call( this );
2161  
2162      // Properties
2163      this.actions = new OO.ui.ActionSet();
2164      this.attachedActions = [];
2165      this.currentAction = null;
2166  
2167      // Events
2168      this.actions.connect( this, {
2169          click: 'onActionClick',
2170          resize: 'onActionResize',
2171          change: 'onActionsChange'
2172      } );
2173  
2174      // Initialization
2175      this.$element
2176          .addClass( 'oo-ui-dialog' )
2177          .attr( 'role', 'dialog' );
2178  };
2179  
2180  /* Setup */
2181  
2182  OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2183  OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement );
2184  
2185  /* Static Properties */
2186  
2187  /**
2188   * Symbolic name of dialog.
2189   *
2190   * @abstract
2191   * @static
2192   * @inheritable
2193   * @property {string}
2194   */
2195  OO.ui.Dialog.static.name = '';
2196  
2197  /**
2198   * Dialog title.
2199   *
2200   * @abstract
2201   * @static
2202   * @inheritable
2203   * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
2204   */
2205  OO.ui.Dialog.static.title = '';
2206  
2207  /**
2208   * List of OO.ui.ActionWidget configuration options.
2209   *
2210   * @static
2211   * inheritable
2212   * @property {Object[]}
2213   */
2214  OO.ui.Dialog.static.actions = [];
2215  
2216  /**
2217   * Close dialog when the escape key is pressed.
2218   *
2219   * @static
2220   * @abstract
2221   * @inheritable
2222   * @property {boolean}
2223   */
2224  OO.ui.Dialog.static.escapable = true;
2225  
2226  /* Methods */
2227  
2228  /**
2229   * Handle frame document key down events.
2230   *
2231   * @param {jQuery.Event} e Key down event
2232   */
2233  OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2234      if ( e.which === OO.ui.Keys.ESCAPE ) {
2235          this.close();
2236          return false;
2237      }
2238  };
2239  
2240  /**
2241   * Handle action resized events.
2242   *
2243   * @param {OO.ui.ActionWidget} action Action that was resized
2244   */
2245  OO.ui.Dialog.prototype.onActionResize = function () {
2246      // Override in subclass
2247  };
2248  
2249  /**
2250   * Handle action click events.
2251   *
2252   * @param {OO.ui.ActionWidget} action Action that was clicked
2253   */
2254  OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2255      if ( !this.isPending() ) {
2256          this.currentAction = action;
2257          this.executeAction( action.getAction() );
2258      }
2259  };
2260  
2261  /**
2262   * Handle actions change event.
2263   */
2264  OO.ui.Dialog.prototype.onActionsChange = function () {
2265      this.detachActions();
2266      if ( !this.isClosing() ) {
2267          this.attachActions();
2268      }
2269  };
2270  
2271  /**
2272   * Get set of actions.
2273   *
2274   * @return {OO.ui.ActionSet}
2275   */
2276  OO.ui.Dialog.prototype.getActions = function () {
2277      return this.actions;
2278  };
2279  
2280  /**
2281   * Get a process for taking action.
2282   *
2283   * When you override this method, you can add additional accept steps to the process the parent
2284   * method provides using the 'first' and 'next' methods.
2285   *
2286   * @abstract
2287   * @param {string} [action] Symbolic name of action
2288   * @return {OO.ui.Process} Action process
2289   */
2290  OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2291      return new OO.ui.Process()
2292          .next( function () {
2293              if ( !action ) {
2294                  // An empty action always closes the dialog without data, which should always be
2295                  // safe and make no changes
2296                  this.close();
2297              }
2298          }, this );
2299  };
2300  
2301  /**
2302   * @inheritdoc
2303   *
2304   * @param {Object} [data] Dialog opening data
2305   * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2306   * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2307   *   action item, omit to use #static-actions
2308   */
2309  OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2310      data = data || {};
2311  
2312      // Parent method
2313      return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2314          .next( function () {
2315              var i, len,
2316                  items = [],
2317                  config = this.constructor.static,
2318                  actions = data.actions !== undefined ? data.actions : config.actions;
2319  
2320              this.title.setLabel(
2321                  data.title !== undefined ? data.title : this.constructor.static.title
2322              );
2323              for ( i = 0, len = actions.length; i < len; i++ ) {
2324                  items.push(
2325                      new OO.ui.ActionWidget( $.extend( { $: this.$ }, actions[i] ) )
2326                  );
2327              }
2328              this.actions.add( items );
2329          }, this );
2330  };
2331  
2332  /**
2333   * @inheritdoc
2334   */
2335  OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2336      // Parent method
2337      return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2338          .first( function () {
2339              this.actions.clear();
2340              this.currentAction = null;
2341          }, this );
2342  };
2343  
2344  /**
2345   * @inheritdoc
2346   */
2347  OO.ui.Dialog.prototype.initialize = function () {
2348      // Parent method
2349      OO.ui.Dialog.super.prototype.initialize.call( this );
2350  
2351      // Properties
2352      this.title = new OO.ui.LabelWidget( { $: this.$ } );
2353  
2354      // Events
2355      if ( this.constructor.static.escapable ) {
2356          this.$document.on( 'keydown', OO.ui.bind( this.onDocumentKeyDown, this ) );
2357      }
2358  
2359      // Initialization
2360      this.$content.addClass( 'oo-ui-dialog-content' );
2361      this.setPendingElement( this.$head );
2362  };
2363  
2364  /**
2365   * Attach action actions.
2366   */
2367  OO.ui.Dialog.prototype.attachActions = function () {
2368      // Remember the list of potentially attached actions
2369      this.attachedActions = this.actions.get();
2370  };
2371  
2372  /**
2373   * Detach action actions.
2374   *
2375   * @chainable
2376   */
2377  OO.ui.Dialog.prototype.detachActions = function () {
2378      var i, len;
2379  
2380      // Detach all actions that may have been previously attached
2381      for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2382          this.attachedActions[i].$element.detach();
2383      }
2384      this.attachedActions = [];
2385  };
2386  
2387  /**
2388   * Execute an action.
2389   *
2390   * @param {string} action Symbolic name of action to execute
2391   * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2392   */
2393  OO.ui.Dialog.prototype.executeAction = function ( action ) {
2394      this.pushPending();
2395      return this.getActionProcess( action ).execute()
2396          .always( OO.ui.bind( this.popPending, this ) );
2397  };
2398  
2399  /**
2400   * Collection of windows.
2401   *
2402   * @class
2403   * @extends OO.ui.Element
2404   * @mixins OO.EventEmitter
2405   *
2406   * Managed windows are mutually exclusive. If a window is opened while there is a current window
2407   * already opening or opened, the current window will be closed without data. Empty closing data
2408   * should always result in the window being closed without causing constructive or destructive
2409   * action.
2410   *
2411   * As a window is opened and closed, it passes through several stages and the manager emits several
2412   * corresponding events.
2413   *
2414   * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening
2415   * - {@link #event-opening} is emitted with `opening` promise
2416   * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution
2417   * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed
2418   * - `setup` progress notification is emitted from opening promise
2419   * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution
2420   * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed
2421   * - `ready` progress notification is emitted from opening promise
2422   * - `opening` promise is resolved with `opened` promise
2423   * - Window is now open
2424   *
2425   * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing
2426   * - `opened` promise is resolved with `closing` promise
2427   * - {@link #event-closing} is emitted with `closing` promise
2428   * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution
2429   * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed
2430   * - `hold` progress notification is emitted from opening promise
2431   * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution
2432   * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed
2433   * - `teardown` progress notification is emitted from opening promise
2434   * - Closing promise is resolved
2435   * - Window is now closed
2436   *
2437   * @constructor
2438   * @param {Object} [config] Configuration options
2439   * @cfg {boolean} [isolate] Configure managed windows to isolate their content using inline frames
2440   * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2441   * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2442   */
2443  OO.ui.WindowManager = function OoUiWindowManager( config ) {
2444      // Configuration initialization
2445      config = config || {};
2446  
2447      // Parent constructor
2448      OO.ui.WindowManager.super.call( this, config );
2449  
2450      // Mixin constructors
2451      OO.EventEmitter.call( this );
2452  
2453      // Properties
2454      this.factory = config.factory;
2455      this.modal = config.modal === undefined || !!config.modal;
2456      this.isolate = !!config.isolate;
2457      this.windows = {};
2458      this.opening = null;
2459      this.opened = null;
2460      this.closing = null;
2461      this.preparingToOpen = null;
2462      this.preparingToClose = null;
2463      this.size = null;
2464      this.currentWindow = null;
2465      this.$ariaHidden = null;
2466      this.requestedSize = null;
2467      this.onWindowResizeTimeout = null;
2468      this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this );
2469      this.afterWindowResizeHandler = OO.ui.bind( this.afterWindowResize, this );
2470      this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this );
2471      this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this );
2472  
2473      // Initialization
2474      this.$element
2475          .addClass( 'oo-ui-windowManager' )
2476          .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2477  };
2478  
2479  /* Setup */
2480  
2481  OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2482  OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2483  
2484  /* Events */
2485  
2486  /**
2487   * Window is opening.
2488   *
2489   * Fired when the window begins to be opened.
2490   *
2491   * @event opening
2492   * @param {OO.ui.Window} win Window that's being opened
2493   * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2494   *   resolved the first argument will be a promise which will be resolved when the window begins
2495   *   closing, the second argument will be the opening data; progress notifications will be fired on
2496   *   the promise for `setup` and `ready` when those processes are completed respectively.
2497   * @param {Object} data Window opening data
2498   */
2499  
2500  /**
2501   * Window is closing.
2502   *
2503   * Fired when the window begins to be closed.
2504   *
2505   * @event closing
2506   * @param {OO.ui.Window} win Window that's being closed
2507   * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
2508   *   is resolved the first argument will be a the closing data; progress notifications will be fired
2509   *   on the promise for `hold` and `teardown` when those processes are completed respectively.
2510   * @param {Object} data Window closing data
2511   */
2512  
2513  /* Static Properties */
2514  
2515  /**
2516   * Map of symbolic size names and CSS properties.
2517   *
2518   * @static
2519   * @inheritable
2520   * @property {Object}
2521   */
2522  OO.ui.WindowManager.static.sizes = {
2523      small: {
2524          width: 300
2525      },
2526      medium: {
2527          width: 500
2528      },
2529      large: {
2530          width: 700
2531      },
2532      full: {
2533          // These can be non-numeric because they are never used in calculations
2534          width: '100%',
2535          height: '100%'
2536      }
2537  };
2538  
2539  /**
2540   * Symbolic name of default size.
2541   *
2542   * Default size is used if the window's requested size is not recognized.
2543   *
2544   * @static
2545   * @inheritable
2546   * @property {string}
2547   */
2548  OO.ui.WindowManager.static.defaultSize = 'medium';
2549  
2550  /* Methods */
2551  
2552  /**
2553   * Handle window resize events.
2554   *
2555   * @param {jQuery.Event} e Window resize event
2556   */
2557  OO.ui.WindowManager.prototype.onWindowResize = function () {
2558      clearTimeout( this.onWindowResizeTimeout );
2559      this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
2560  };
2561  
2562  /**
2563   * Handle window resize events.
2564   *
2565   * @param {jQuery.Event} e Window resize event
2566   */
2567  OO.ui.WindowManager.prototype.afterWindowResize = function () {
2568      if ( this.currentWindow ) {
2569          this.updateWindowSize( this.currentWindow );
2570      }
2571  };
2572  
2573  /**
2574   * Handle window mouse wheel events.
2575   *
2576   * @param {jQuery.Event} e Mouse wheel event
2577   */
2578  OO.ui.WindowManager.prototype.onWindowMouseWheel = function () {
2579      return false;
2580  };
2581  
2582  /**
2583   * Handle document key down events.
2584   *
2585   * @param {jQuery.Event} e Key down event
2586   */
2587  OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) {
2588      switch ( e.which ) {
2589          case OO.ui.Keys.PAGEUP:
2590          case OO.ui.Keys.PAGEDOWN:
2591          case OO.ui.Keys.END:
2592          case OO.ui.Keys.HOME:
2593          case OO.ui.Keys.LEFT:
2594          case OO.ui.Keys.UP:
2595          case OO.ui.Keys.RIGHT:
2596          case OO.ui.Keys.DOWN:
2597              // Prevent any key events that might cause scrolling
2598              return false;
2599      }
2600  };
2601  
2602  /**
2603   * Check if window is opening.
2604   *
2605   * @return {boolean} Window is opening
2606   */
2607  OO.ui.WindowManager.prototype.isOpening = function ( win ) {
2608      return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
2609  };
2610  
2611  /**
2612   * Check if window is closing.
2613   *
2614   * @return {boolean} Window is closing
2615   */
2616  OO.ui.WindowManager.prototype.isClosing = function ( win ) {
2617      return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
2618  };
2619  
2620  /**
2621   * Check if window is opened.
2622   *
2623   * @return {boolean} Window is opened
2624   */
2625  OO.ui.WindowManager.prototype.isOpened = function ( win ) {
2626      return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
2627  };
2628  
2629  /**
2630   * Check if window contents should be isolated.
2631   *
2632   * Window content isolation is done using inline frames.
2633   *
2634   * @return {boolean} Window contents should be isolated
2635   */
2636  OO.ui.WindowManager.prototype.shouldIsolate = function () {
2637      return this.isolate;
2638  };
2639  
2640  /**
2641   * Check if a window is being managed.
2642   *
2643   * @param {OO.ui.Window} win Window to check
2644   * @return {boolean} Window is being managed
2645   */
2646  OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
2647      var name;
2648  
2649      for ( name in this.windows ) {
2650          if ( this.windows[name] === win ) {
2651              return true;
2652          }
2653      }
2654  
2655      return false;
2656  };
2657  
2658  /**
2659   * Get the number of milliseconds to wait between beginning opening and executing setup process.
2660   *
2661   * @param {OO.ui.Window} win Window being opened
2662   * @param {Object} [data] Window opening data
2663   * @return {number} Milliseconds to wait
2664   */
2665  OO.ui.WindowManager.prototype.getSetupDelay = function () {
2666      return 0;
2667  };
2668  
2669  /**
2670   * Get the number of milliseconds to wait between finishing setup and executing ready process.
2671   *
2672   * @param {OO.ui.Window} win Window being opened
2673   * @param {Object} [data] Window opening data
2674   * @return {number} Milliseconds to wait
2675   */
2676  OO.ui.WindowManager.prototype.getReadyDelay = function () {
2677      return 0;
2678  };
2679  
2680  /**
2681   * Get the number of milliseconds to wait between beginning closing and executing hold process.
2682   *
2683   * @param {OO.ui.Window} win Window being closed
2684   * @param {Object} [data] Window closing data
2685   * @return {number} Milliseconds to wait
2686   */
2687  OO.ui.WindowManager.prototype.getHoldDelay = function () {
2688      return 0;
2689  };
2690  
2691  /**
2692   * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2693   *
2694   * @param {OO.ui.Window} win Window being closed
2695   * @param {Object} [data] Window closing data
2696   * @return {number} Milliseconds to wait
2697   */
2698  OO.ui.WindowManager.prototype.getTeardownDelay = function () {
2699      return this.modal ? 250 : 0;
2700  };
2701  
2702  /**
2703   * Get managed window by symbolic name.
2704   *
2705   * If window is not yet instantiated, it will be instantiated and added automatically.
2706   *
2707   * @param {string} name Symbolic window name
2708   * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2709   * @throws {Error} If the symbolic name is unrecognized by the factory
2710   * @throws {Error} If the symbolic name unrecognized as a managed window
2711   */
2712  OO.ui.WindowManager.prototype.getWindow = function ( name ) {
2713      var deferred = $.Deferred(),
2714          win = this.windows[name];
2715  
2716      if ( !( win instanceof OO.ui.Window ) ) {
2717          if ( this.factory ) {
2718              if ( !this.factory.lookup( name ) ) {
2719                  deferred.reject( new OO.ui.Error(
2720                      'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2721                  ) );
2722              } else {
2723                  win = this.factory.create( name, this, { $: this.$ } );
2724                  this.addWindows( [ win ] );
2725                  deferred.resolve( win );
2726              }
2727          } else {
2728              deferred.reject( new OO.ui.Error(
2729                  'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2730              ) );
2731          }
2732      } else {
2733          deferred.resolve( win );
2734      }
2735  
2736      return deferred.promise();
2737  };
2738  
2739  /**
2740   * Get current window.
2741   *
2742   * @return {OO.ui.Window|null} Currently opening/opened/closing window
2743   */
2744  OO.ui.WindowManager.prototype.getCurrentWindow = function () {
2745      return this.currentWindow;
2746  };
2747  
2748  /**
2749   * Open a window.
2750   *
2751   * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2752   * @param {Object} [data] Window opening data
2753   * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2754   *   for more details about the `opening` promise
2755   * @fires opening
2756   */
2757  OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
2758      var manager = this,
2759          preparing = [],
2760          opening = $.Deferred();
2761  
2762      // Argument handling
2763      if ( typeof win === 'string' ) {
2764          return this.getWindow( win ).then( function ( win ) {
2765              return manager.openWindow( win, data );
2766          } );
2767      }
2768  
2769      // Error handling
2770      if ( !this.hasWindow( win ) ) {
2771          opening.reject( new OO.ui.Error(
2772              'Cannot open window: window is not attached to manager'
2773          ) );
2774      } else if ( this.preparingToOpen || this.opening || this.opened ) {
2775          opening.reject( new OO.ui.Error(
2776              'Cannot open window: another window is opening or open'
2777          ) );
2778      }
2779  
2780      // Window opening
2781      if ( opening.state() !== 'rejected' ) {
2782          // Begin loading the window if it's not loading or loaded already - may take noticable time
2783          // and we want to do this in paralell with any other preparatory actions
2784          if ( !win.isLoading() && !win.isLoaded() ) {
2785              // Finish initializing the window (must be done after manager is attached to DOM)
2786              win.setManager( this );
2787              preparing.push( win.load() );
2788          }
2789  
2790          if ( this.closing ) {
2791              // If a window is currently closing, wait for it to complete
2792              preparing.push( this.closing );
2793          }
2794  
2795          this.preparingToOpen = $.when.apply( $, preparing );
2796          // Ensure handlers get called after preparingToOpen is set
2797          this.preparingToOpen.done( function () {
2798              if ( manager.modal ) {
2799                  manager.toggleGlobalEvents( true );
2800                  manager.toggleAriaIsolation( true );
2801              }
2802              manager.currentWindow = win;
2803              manager.opening = opening;
2804              manager.preparingToOpen = null;
2805              manager.emit( 'opening', win, opening, data );
2806              setTimeout( function () {
2807                  win.setup( data ).then( function () {
2808                      manager.updateWindowSize( win );
2809                      manager.opening.notify( { state: 'setup' } );
2810                      setTimeout( function () {
2811                          win.ready( data ).then( function () {
2812                              manager.opening.notify( { state: 'ready' } );
2813                              manager.opening = null;
2814                              manager.opened = $.Deferred();
2815                              opening.resolve( manager.opened.promise(), data );
2816                          } );
2817                      }, manager.getReadyDelay() );
2818                  } );
2819              }, manager.getSetupDelay() );
2820          } );
2821      }
2822  
2823      return opening.promise();
2824  };
2825  
2826  /**
2827   * Close a window.
2828   *
2829   * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
2830   * @param {Object} [data] Window closing data
2831   * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
2832   *   for more details about the `closing` promise
2833   * @throws {Error} If no window by that name is being managed
2834   * @fires closing
2835   */
2836  OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
2837      var manager = this,
2838          preparing = [],
2839          closing = $.Deferred(),
2840          opened = this.opened;
2841  
2842      // Argument handling
2843      if ( typeof win === 'string' ) {
2844          win = this.windows[win];
2845      } else if ( !this.hasWindow( win ) ) {
2846          win = null;
2847      }
2848  
2849      // Error handling
2850      if ( !win ) {
2851          closing.reject( new OO.ui.Error(
2852              'Cannot close window: window is not attached to manager'
2853          ) );
2854      } else if ( win !== this.currentWindow ) {
2855          closing.reject( new OO.ui.Error(
2856              'Cannot close window: window already closed with different data'
2857          ) );
2858      } else if ( this.preparingToClose || this.closing ) {
2859          closing.reject( new OO.ui.Error(
2860              'Cannot close window: window already closing with different data'
2861          ) );
2862      }
2863  
2864      // Window closing
2865      if ( closing.state() !== 'rejected' ) {
2866          if ( this.opening ) {
2867              // If the window is currently opening, close it when it's done
2868              preparing.push( this.opening );
2869          }
2870  
2871          this.preparingToClose = $.when.apply( $, preparing );
2872          // Ensure handlers get called after preparingToClose is set
2873          this.preparingToClose.done( function () {
2874              manager.closing = closing;
2875              manager.preparingToClose = null;
2876              manager.emit( 'closing', win, closing, data );
2877              manager.opened = null;
2878              opened.resolve( closing.promise(), data );
2879              setTimeout( function () {
2880                  win.hold( data ).then( function () {
2881                      closing.notify( { state: 'hold' } );
2882                      setTimeout( function () {
2883                          win.teardown( data ).then( function () {
2884                              closing.notify( { state: 'teardown' } );
2885                              if ( manager.modal ) {
2886                                  manager.toggleGlobalEvents( false );
2887                                  manager.toggleAriaIsolation( false );
2888                              }
2889                              manager.closing = null;
2890                              manager.currentWindow = null;
2891                              closing.resolve( data );
2892                          } );
2893                      }, manager.getTeardownDelay() );
2894                  } );
2895              }, manager.getHoldDelay() );
2896          } );
2897      }
2898  
2899      return closing.promise();
2900  };
2901  
2902  /**
2903   * Add windows.
2904   *
2905   * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
2906   * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
2907   *   a statically configured symbolic name
2908   */
2909  OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
2910      var i, len, win, name, list;
2911  
2912      if ( $.isArray( windows ) ) {
2913          // Convert to map of windows by looking up symbolic names from static configuration
2914          list = {};
2915          for ( i = 0, len = windows.length; i < len; i++ ) {
2916              name = windows[i].constructor.static.name;
2917              if ( typeof name !== 'string' ) {
2918                  throw new Error( 'Cannot add window' );
2919              }
2920              list[name] = windows[i];
2921          }
2922      } else if ( $.isPlainObject( windows ) ) {
2923          list = windows;
2924      }
2925  
2926      // Add windows
2927      for ( name in list ) {
2928          win = list[name];
2929          this.windows[name] = win;
2930          this.$element.append( win.$element );
2931      }
2932  };
2933  
2934  /**
2935   * Remove windows.
2936   *
2937   * Windows will be closed before they are removed.
2938   *
2939   * @param {string} name Symbolic name of window to remove
2940   * @return {jQuery.Promise} Promise resolved when window is closed and removed
2941   * @throws {Error} If windows being removed are not being managed
2942   */
2943  OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
2944      var i, len, win, name,
2945          manager = this,
2946          promises = [],
2947          cleanup = function ( name, win ) {
2948              delete manager.windows[name];
2949              win.$element.detach();
2950          };
2951  
2952      for ( i = 0, len = names.length; i < len; i++ ) {
2953          name = names[i];
2954          win = this.windows[name];
2955          if ( !win ) {
2956              throw new Error( 'Cannot remove window' );
2957          }
2958          promises.push( this.closeWindow( name ).then( OO.ui.bind( cleanup, null, name, win ) ) );
2959      }
2960  
2961      return $.when.apply( $, promises );
2962  };
2963  
2964  /**
2965   * Remove all windows.
2966   *
2967   * Windows will be closed before they are removed.
2968   *
2969   * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
2970   */
2971  OO.ui.WindowManager.prototype.clearWindows = function () {
2972      return this.removeWindows( Object.keys( this.windows ) );
2973  };
2974  
2975  /**
2976   * Set dialog size.
2977   *
2978   * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
2979   *
2980   * @chainable
2981   */
2982  OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
2983      // Bypass for non-current, and thus invisible, windows
2984      if ( win !== this.currentWindow ) {
2985          return;
2986      }
2987  
2988      var viewport = OO.ui.Element.getDimensions( win.getElementWindow() ),
2989          sizes = this.constructor.static.sizes,
2990          size = win.getSize();
2991  
2992      if ( !sizes[size] ) {
2993          size = this.constructor.static.defaultSize;
2994      }
2995      if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[size].width ) {
2996          size = 'full';
2997      }
2998  
2999      this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
3000      this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
3001      win.setDimensions( sizes[size] );
3002  
3003      return this;
3004  };
3005  
3006  /**
3007   * Bind or unbind global events for scrolling.
3008   *
3009   * @param {boolean} [on] Bind global events
3010   * @chainable
3011   */
3012  OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3013      on = on === undefined ? !!this.globalEvents : !!on;
3014  
3015      if ( on ) {
3016          if ( !this.globalEvents ) {
3017              this.$( this.getElementDocument() ).on( {
3018                  // Prevent scrolling by keys in top-level window
3019                  keydown: this.onDocumentKeyDownHandler
3020              } );
3021              this.$( this.getElementWindow() ).on( {
3022                  // Prevent scrolling by wheel in top-level window
3023                  mousewheel: this.onWindowMouseWheelHandler,
3024                  // Start listening for top-level window dimension changes
3025                  'orientationchange resize': this.onWindowResizeHandler
3026              } );
3027              this.globalEvents = true;
3028          }
3029      } else if ( this.globalEvents ) {
3030          // Unbind global events
3031          this.$( this.getElementDocument() ).off( {
3032              // Allow scrolling by keys in top-level window
3033              keydown: this.onDocumentKeyDownHandler
3034          } );
3035          this.$( this.getElementWindow() ).off( {
3036              // Allow scrolling by wheel in top-level window
3037              mousewheel: this.onWindowMouseWheelHandler,
3038              // Stop listening for top-level window dimension changes
3039              'orientationchange resize': this.onWindowResizeHandler
3040          } );
3041          this.globalEvents = false;
3042      }
3043  
3044      return this;
3045  };
3046  
3047  /**
3048   * Toggle screen reader visibility of content other than the window manager.
3049   *
3050   * @param {boolean} [isolate] Make only the window manager visible to screen readers
3051   * @chainable
3052   */
3053  OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3054      isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3055  
3056      if ( isolate ) {
3057          if ( !this.$ariaHidden ) {
3058              // Hide everything other than the window manager from screen readers
3059              this.$ariaHidden = $( 'body' )
3060                  .children()
3061                  .not( this.$element.parentsUntil( 'body' ).last() )
3062                  .attr( 'aria-hidden', '' );
3063          }
3064      } else if ( this.$ariaHidden ) {
3065          // Restore screen reader visiblity
3066          this.$ariaHidden.removeAttr( 'aria-hidden' );
3067          this.$ariaHidden = null;
3068      }
3069  
3070      return this;
3071  };
3072  
3073  /**
3074   * Destroy window manager.
3075   *
3076   * Windows will not be closed, only removed from the DOM.
3077   */
3078  OO.ui.WindowManager.prototype.destroy = function () {
3079      this.toggleGlobalEvents( false );
3080      this.toggleAriaIsolation( false );
3081      this.$element.remove();
3082  };
3083  
3084  /**
3085   * @abstract
3086   * @class
3087   *
3088   * @constructor
3089   * @param {string|jQuery} message Description of error
3090   * @param {Object} [config] Configuration options
3091   * @cfg {boolean} [recoverable=true] Error is recoverable
3092   */
3093  OO.ui.Error = function OoUiElement( message, config ) {
3094      // Configuration initialization
3095      config = config || {};
3096  
3097      // Properties
3098      this.message = message instanceof jQuery ? message : String( message );
3099      this.recoverable = config.recoverable === undefined || !!config.recoverable;
3100  };
3101  
3102  /* Setup */
3103  
3104  OO.initClass( OO.ui.Error );
3105  
3106  /* Methods */
3107  
3108  /**
3109   * Check if error can be recovered from.
3110   *
3111   * @return {boolean} Error is recoverable
3112   */
3113  OO.ui.Error.prototype.isRecoverable = function () {
3114      return this.recoverable;
3115  };
3116  
3117  /**
3118   * Get error message as DOM nodes.
3119   *
3120   * @return {jQuery} Error message in DOM nodes
3121   */
3122  OO.ui.Error.prototype.getMessage = function () {
3123      return this.message instanceof jQuery ?
3124          this.message.clone() :
3125          $( '<div>' ).text( this.message ).contents();
3126  };
3127  
3128  /**
3129   * Get error message as text.
3130   *
3131   * @return {string} Error message
3132   */
3133  OO.ui.Error.prototype.getMessageText = function () {
3134      return this.message instanceof jQuery ? this.message.text() : this.message;
3135  };
3136  
3137  /**
3138   * A list of functions, called in sequence.
3139   *
3140   * If a function added to a process returns boolean false the process will stop; if it returns an
3141   * object with a `promise` method the process will use the promise to either continue to the next
3142   * step when the promise is resolved or stop when the promise is rejected.
3143   *
3144   * @class
3145   *
3146   * @constructor
3147   * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3148   *   call, see #createStep for more information
3149   * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3150   *   or a promise
3151   * @return {Object} Step object, with `callback` and `context` properties
3152   */
3153  OO.ui.Process = function ( step, context ) {
3154      // Properties
3155      this.steps = [];
3156  
3157      // Initialization
3158      if ( step !== undefined ) {
3159          this.next( step, context );
3160      }
3161  };
3162  
3163  /* Setup */
3164  
3165  OO.initClass( OO.ui.Process );
3166  
3167  /* Methods */
3168  
3169  /**
3170   * Start the process.
3171   *
3172   * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3173   *   any of the steps return boolean false or a promise which gets rejected; upon stopping the
3174   *   process, the remaining steps will not be taken
3175   */
3176  OO.ui.Process.prototype.execute = function () {
3177      var i, len, promise;
3178  
3179      /**
3180       * Continue execution.
3181       *
3182       * @ignore
3183       * @param {Array} step A function and the context it should be called in
3184       * @return {Function} Function that continues the process
3185       */
3186  	function proceed( step ) {
3187          return function () {
3188              // Execute step in the correct context
3189              var deferred,
3190                  result = step.callback.call( step.context );
3191  
3192              if ( result === false ) {
3193                  // Use rejected promise for boolean false results
3194                  return $.Deferred().reject( [] ).promise();
3195              }
3196              if ( typeof result === 'number' ) {
3197                  if ( result < 0 ) {
3198                      throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3199                  }
3200                  // Use a delayed promise for numbers, expecting them to be in milliseconds
3201                  deferred = $.Deferred();
3202                  setTimeout( deferred.resolve, result );
3203                  return deferred.promise();
3204              }
3205              if ( result instanceof OO.ui.Error ) {
3206                  // Use rejected promise for error
3207                  return $.Deferred().reject( [ result ] ).promise();
3208              }
3209              if ( $.isArray( result ) && result.length && result[0] instanceof OO.ui.Error ) {
3210                  // Use rejected promise for list of errors
3211                  return $.Deferred().reject( result ).promise();
3212              }
3213              // Duck-type the object to see if it can produce a promise
3214              if ( result && $.isFunction( result.promise ) ) {
3215                  // Use a promise generated from the result
3216                  return result.promise();
3217              }
3218              // Use resolved promise for other results
3219              return $.Deferred().resolve().promise();
3220          };
3221      }
3222  
3223      if ( this.steps.length ) {
3224          // Generate a chain reaction of promises
3225          promise = proceed( this.steps[0] )();
3226          for ( i = 1, len = this.steps.length; i < len; i++ ) {
3227              promise = promise.then( proceed( this.steps[i] ) );
3228          }
3229      } else {
3230          promise = $.Deferred().resolve().promise();
3231      }
3232  
3233      return promise;
3234  };
3235  
3236  /**
3237   * Create a process step.
3238   *
3239   * @private
3240   * @param {number|jQuery.Promise|Function} step
3241   *
3242   * - Number of milliseconds to wait; or
3243   * - Promise to wait to be resolved; or
3244   * - Function to execute
3245   *   - If it returns boolean false the process will stop
3246   *   - If it returns an object with a `promise` method the process will use the promise to either
3247   *     continue to the next step when the promise is resolved or stop when the promise is rejected
3248   *   - If it returns a number, the process will wait for that number of milliseconds before
3249   *     proceeding
3250   * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3251   *   or a promise
3252   * @return {Object} Step object, with `callback` and `context` properties
3253   */
3254  OO.ui.Process.prototype.createStep = function ( step, context ) {
3255      if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3256          return {
3257              callback: function () {
3258                  return step;
3259              },
3260              context: null
3261          };
3262      }
3263      if ( $.isFunction( step ) ) {
3264          return {
3265              callback: step,
3266              context: context
3267          };
3268      }
3269      throw new Error( 'Cannot create process step: number, promise or function expected' );
3270  };
3271  
3272  /**
3273   * Add step to the beginning of the process.
3274   *
3275   * @inheritdoc #createStep
3276   * @return {OO.ui.Process} this
3277   * @chainable
3278   */
3279  OO.ui.Process.prototype.first = function ( step, context ) {
3280      this.steps.unshift( this.createStep( step, context ) );
3281      return this;
3282  };
3283  
3284  /**
3285   * Add step to the end of the process.
3286   *
3287   * @inheritdoc #createStep
3288   * @return {OO.ui.Process} this
3289   * @chainable
3290   */
3291  OO.ui.Process.prototype.next = function ( step, context ) {
3292      this.steps.push( this.createStep( step, context ) );
3293      return this;
3294  };
3295  
3296  /**
3297   * Factory for tools.
3298   *
3299   * @class
3300   * @extends OO.Factory
3301   * @constructor
3302   */
3303  OO.ui.ToolFactory = function OoUiToolFactory() {
3304      // Parent constructor
3305      OO.ui.ToolFactory.super.call( this );
3306  };
3307  
3308  /* Setup */
3309  
3310  OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3311  
3312  /* Methods */
3313  
3314  /** */
3315  OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3316      var i, len, included, promoted, demoted,
3317          auto = [],
3318          used = {};
3319  
3320      // Collect included and not excluded tools
3321      included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3322  
3323      // Promotion
3324      promoted = this.extract( promote, used );
3325      demoted = this.extract( demote, used );
3326  
3327      // Auto
3328      for ( i = 0, len = included.length; i < len; i++ ) {
3329          if ( !used[included[i]] ) {
3330              auto.push( included[i] );
3331          }
3332      }
3333  
3334      return promoted.concat( auto ).concat( demoted );
3335  };
3336  
3337  /**
3338   * Get a flat list of names from a list of names or groups.
3339   *
3340   * Tools can be specified in the following ways:
3341   *
3342   * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3343   * - All tools in a group: `{ group: 'group-name' }`
3344   * - All tools: `'*'`
3345   *
3346   * @private
3347   * @param {Array|string} collection List of tools
3348   * @param {Object} [used] Object with names that should be skipped as properties; extracted
3349   *  names will be added as properties
3350   * @return {string[]} List of extracted names
3351   */
3352  OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3353      var i, len, item, name, tool,
3354          names = [];
3355  
3356      if ( collection === '*' ) {
3357          for ( name in this.registry ) {
3358              tool = this.registry[name];
3359              if (
3360                  // Only add tools by group name when auto-add is enabled
3361                  tool.static.autoAddToCatchall &&
3362                  // Exclude already used tools
3363                  ( !used || !used[name] )
3364              ) {
3365                  names.push( name );
3366                  if ( used ) {
3367                      used[name] = true;
3368                  }
3369              }
3370          }
3371      } else if ( $.isArray( collection ) ) {
3372          for ( i = 0, len = collection.length; i < len; i++ ) {
3373              item = collection[i];
3374              // Allow plain strings as shorthand for named tools
3375              if ( typeof item === 'string' ) {
3376                  item = { name: item };
3377              }
3378              if ( OO.isPlainObject( item ) ) {
3379                  if ( item.group ) {
3380                      for ( name in this.registry ) {
3381                          tool = this.registry[name];
3382                          if (
3383                              // Include tools with matching group
3384                              tool.static.group === item.group &&
3385                              // Only add tools by group name when auto-add is enabled
3386                              tool.static.autoAddToGroup &&
3387                              // Exclude already used tools
3388                              ( !used || !used[name] )
3389                          ) {
3390                              names.push( name );
3391                              if ( used ) {
3392                                  used[name] = true;
3393                              }
3394                          }
3395                      }
3396                  // Include tools with matching name and exclude already used tools
3397                  } else if ( item.name && ( !used || !used[item.name] ) ) {
3398                      names.push( item.name );
3399                      if ( used ) {
3400                          used[item.name] = true;
3401                      }
3402                  }
3403              }
3404          }
3405      }
3406      return names;
3407  };
3408  
3409  /**
3410   * Factory for tool groups.
3411   *
3412   * @class
3413   * @extends OO.Factory
3414   * @constructor
3415   */
3416  OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3417      // Parent constructor
3418      OO.Factory.call( this );
3419  
3420      var i, l,
3421          defaultClasses = this.constructor.static.getDefaultClasses();
3422  
3423      // Register default toolgroups
3424      for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3425          this.register( defaultClasses[i] );
3426      }
3427  };
3428  
3429  /* Setup */
3430  
3431  OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3432  
3433  /* Static Methods */
3434  
3435  /**
3436   * Get a default set of classes to be registered on construction
3437   *
3438   * @return {Function[]} Default classes
3439   */
3440  OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3441      return [
3442          OO.ui.BarToolGroup,
3443          OO.ui.ListToolGroup,
3444          OO.ui.MenuToolGroup
3445      ];
3446  };
3447  
3448  /**
3449   * Element with a button.
3450   *
3451   * Buttons are used for controls which can be clicked. They can be configured to use tab indexing
3452   * and access keys for accessibility purposes.
3453   *
3454   * @abstract
3455   * @class
3456   *
3457   * @constructor
3458   * @param {Object} [config] Configuration options
3459   * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3460   * @cfg {boolean} [framed=true] Render button with a frame
3461   * @cfg {number} [tabIndex=0] Button's tab index, use null to have no tabIndex
3462   * @cfg {string} [accessKey] Button's access key
3463   */
3464  OO.ui.ButtonElement = function OoUiButtonElement( config ) {
3465      // Configuration initialization
3466      config = config || {};
3467  
3468      // Properties
3469      this.$button = null;
3470      this.framed = null;
3471      this.tabIndex = null;
3472      this.accessKey = null;
3473      this.active = false;
3474      this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
3475      this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this );
3476  
3477      // Initialization
3478      this.$element.addClass( 'oo-ui-buttonElement' );
3479      this.toggleFramed( config.framed === undefined || config.framed );
3480      this.setTabIndex( config.tabIndex || 0 );
3481      this.setAccessKey( config.accessKey );
3482      this.setButtonElement( config.$button || this.$( '<a>' ) );
3483  };
3484  
3485  /* Setup */
3486  
3487  OO.initClass( OO.ui.ButtonElement );
3488  
3489  /* Static Properties */
3490  
3491  /**
3492   * Cancel mouse down events.
3493   *
3494   * @static
3495   * @inheritable
3496   * @property {boolean}
3497   */
3498  OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
3499  
3500  /* Methods */
3501  
3502  /**
3503   * Set the button element.
3504   *
3505   * If an element is already set, it will be cleaned up before setting up the new element.
3506   *
3507   * @param {jQuery} $button Element to use as button
3508   */
3509  OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
3510      if ( this.$button ) {
3511          this.$button
3512              .removeClass( 'oo-ui-buttonElement-button' )
3513              .removeAttr( 'role accesskey tabindex' )
3514              .off( this.onMouseDownHandler );
3515      }
3516  
3517      this.$button = $button
3518          .addClass( 'oo-ui-buttonElement-button' )
3519          .attr( { role: 'button', accesskey: this.accessKey, tabindex: this.tabIndex } )
3520          .on( 'mousedown', this.onMouseDownHandler );
3521  };
3522  
3523  /**
3524   * Handles mouse down events.
3525   *
3526   * @param {jQuery.Event} e Mouse down event
3527   */
3528  OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
3529      if ( this.isDisabled() || e.which !== 1 ) {
3530          return false;
3531      }
3532      // Remove the tab-index while the button is down to prevent the button from stealing focus
3533      this.$button.removeAttr( 'tabindex' );
3534      this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3535      // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3536      // reliably reapply the tabindex and remove the pressed class
3537      this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
3538      // Prevent change of focus unless specifically configured otherwise
3539      if ( this.constructor.static.cancelButtonMouseDownEvents ) {
3540          return false;
3541      }
3542  };
3543  
3544  /**
3545   * Handles mouse up events.
3546   *
3547   * @param {jQuery.Event} e Mouse up event
3548   */
3549  OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
3550      if ( this.isDisabled() || e.which !== 1 ) {
3551          return false;
3552      }
3553      // Restore the tab-index after the button is up to restore the button's accesssibility
3554      this.$button.attr( 'tabindex', this.tabIndex );
3555      this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3556      // Stop listening for mouseup, since we only needed this once
3557      this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
3558  };
3559  
3560  /**
3561   * Toggle frame.
3562   *
3563   * @param {boolean} [framed] Make button framed, omit to toggle
3564   * @chainable
3565   */
3566  OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) {
3567      framed = framed === undefined ? !this.framed : !!framed;
3568      if ( framed !== this.framed ) {
3569          this.framed = framed;
3570          this.$element
3571              .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
3572              .toggleClass( 'oo-ui-buttonElement-framed', framed );
3573      }
3574  
3575      return this;
3576  };
3577  
3578  /**
3579   * Set tab index.
3580   *
3581   * @param {number|null} tabIndex Button's tab index, use null to remove
3582   * @chainable
3583   */
3584  OO.ui.ButtonElement.prototype.setTabIndex = function ( tabIndex ) {
3585      tabIndex = typeof tabIndex === 'number' && tabIndex >= 0 ? tabIndex : null;
3586  
3587      if ( this.tabIndex !== tabIndex ) {
3588          if ( this.$button ) {
3589              if ( tabIndex !== null ) {
3590                  this.$button.attr( 'tabindex', tabIndex );
3591              } else {
3592                  this.$button.removeAttr( 'tabindex' );
3593              }
3594          }
3595          this.tabIndex = tabIndex;
3596      }
3597  
3598      return this;
3599  };
3600  
3601  /**
3602   * Set access key.
3603   *
3604   * @param {string} accessKey Button's access key, use empty string to remove
3605   * @chainable
3606   */
3607  OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
3608      accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
3609  
3610      if ( this.accessKey !== accessKey ) {
3611          if ( this.$button ) {
3612              if ( accessKey !== null ) {
3613                  this.$button.attr( 'accesskey', accessKey );
3614              } else {
3615                  this.$button.removeAttr( 'accesskey' );
3616              }
3617          }
3618          this.accessKey = accessKey;
3619      }
3620  
3621      return this;
3622  };
3623  
3624  /**
3625   * Set active state.
3626   *
3627   * @param {boolean} [value] Make button active
3628   * @chainable
3629   */
3630  OO.ui.ButtonElement.prototype.setActive = function ( value ) {
3631      this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
3632      return this;
3633  };
3634  
3635  /**
3636   * Element containing a sequence of child elements.
3637   *
3638   * @abstract
3639   * @class
3640   *
3641   * @constructor
3642   * @param {Object} [config] Configuration options
3643   * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
3644   */
3645  OO.ui.GroupElement = function OoUiGroupElement( config ) {
3646      // Configuration
3647      config = config || {};
3648  
3649      // Properties
3650      this.$group = null;
3651      this.items = [];
3652      this.aggregateItemEvents = {};
3653  
3654      // Initialization
3655      this.setGroupElement( config.$group || this.$( '<div>' ) );
3656  };
3657  
3658  /* Methods */
3659  
3660  /**
3661   * Set the group element.
3662   *
3663   * If an element is already set, items will be moved to the new element.
3664   *
3665   * @param {jQuery} $group Element to use as group
3666   */
3667  OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) {
3668      var i, len;
3669  
3670      this.$group = $group;
3671      for ( i = 0, len = this.items.length; i < len; i++ ) {
3672          this.$group.append( this.items[i].$element );
3673      }
3674  };
3675  
3676  /**
3677   * Check if there are no items.
3678   *
3679   * @return {boolean} Group is empty
3680   */
3681  OO.ui.GroupElement.prototype.isEmpty = function () {
3682      return !this.items.length;
3683  };
3684  
3685  /**
3686   * Get items.
3687   *
3688   * @return {OO.ui.Element[]} Items
3689   */
3690  OO.ui.GroupElement.prototype.getItems = function () {
3691      return this.items.slice( 0 );
3692  };
3693  
3694  /**
3695   * Add an aggregate item event.
3696   *
3697   * Aggregated events are listened to on each item and then emitted by the group under a new name,
3698   * and with an additional leading parameter containing the item that emitted the original event.
3699   * Other arguments that were emitted from the original event are passed through.
3700   *
3701   * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
3702   *   event, use null value to remove aggregation
3703   * @throws {Error} If aggregation already exists
3704   */
3705  OO.ui.GroupElement.prototype.aggregate = function ( events ) {
3706      var i, len, item, add, remove, itemEvent, groupEvent;
3707  
3708      for ( itemEvent in events ) {
3709          groupEvent = events[itemEvent];
3710  
3711          // Remove existing aggregated event
3712          if ( itemEvent in this.aggregateItemEvents ) {
3713              // Don't allow duplicate aggregations
3714              if ( groupEvent ) {
3715                  throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
3716              }
3717              // Remove event aggregation from existing items
3718              for ( i = 0, len = this.items.length; i < len; i++ ) {
3719                  item = this.items[i];
3720                  if ( item.connect && item.disconnect ) {
3721                      remove = {};
3722                      remove[itemEvent] = [ 'emit', groupEvent, item ];
3723                      item.disconnect( this, remove );
3724                  }
3725              }
3726              // Prevent future items from aggregating event
3727              delete this.aggregateItemEvents[itemEvent];
3728          }
3729  
3730          // Add new aggregate event
3731          if ( groupEvent ) {
3732              // Make future items aggregate event
3733              this.aggregateItemEvents[itemEvent] = groupEvent;
3734              // Add event aggregation to existing items
3735              for ( i = 0, len = this.items.length; i < len; i++ ) {
3736                  item = this.items[i];
3737                  if ( item.connect && item.disconnect ) {
3738                      add = {};
3739                      add[itemEvent] = [ 'emit', groupEvent, item ];
3740                      item.connect( this, add );
3741                  }
3742              }
3743          }
3744      }
3745  };
3746  
3747  /**
3748   * Add items.
3749   *
3750   * Adding an existing item (by value) will move it.
3751   *
3752   * @param {OO.ui.Element[]} items Item
3753   * @param {number} [index] Index to insert items at
3754   * @chainable
3755   */
3756  OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
3757      var i, len, item, event, events, currentIndex,
3758          itemElements = [];
3759  
3760      for ( i = 0, len = items.length; i < len; i++ ) {
3761          item = items[i];
3762  
3763          // Check if item exists then remove it first, effectively "moving" it
3764          currentIndex = $.inArray( item, this.items );
3765          if ( currentIndex >= 0 ) {
3766              this.removeItems( [ item ] );
3767              // Adjust index to compensate for removal
3768              if ( currentIndex < index ) {
3769                  index--;
3770              }
3771          }
3772          // Add the item
3773          if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
3774              events = {};
3775              for ( event in this.aggregateItemEvents ) {
3776                  events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
3777              }
3778              item.connect( this, events );
3779          }
3780          item.setElementGroup( this );
3781          itemElements.push( item.$element.get( 0 ) );
3782      }
3783  
3784      if ( index === undefined || index < 0 || index >= this.items.length ) {
3785          this.$group.append( itemElements );
3786          this.items.push.apply( this.items, items );
3787      } else if ( index === 0 ) {
3788          this.$group.prepend( itemElements );
3789          this.items.unshift.apply( this.items, items );
3790      } else {
3791          this.items[index].$element.before( itemElements );
3792          this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
3793      }
3794  
3795      return this;
3796  };
3797  
3798  /**
3799   * Remove items.
3800   *
3801   * Items will be detached, not removed, so they can be used later.
3802   *
3803   * @param {OO.ui.Element[]} items Items to remove
3804   * @chainable
3805   */
3806  OO.ui.GroupElement.prototype.removeItems = function ( items ) {
3807      var i, len, item, index, remove, itemEvent;
3808  
3809      // Remove specific items
3810      for ( i = 0, len = items.length; i < len; i++ ) {
3811          item = items[i];
3812          index = $.inArray( item, this.items );
3813          if ( index !== -1 ) {
3814              if (
3815                  item.connect && item.disconnect &&
3816                  !$.isEmptyObject( this.aggregateItemEvents )
3817              ) {
3818                  remove = {};
3819                  if ( itemEvent in this.aggregateItemEvents ) {
3820                      remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
3821                  }
3822                  item.disconnect( this, remove );
3823              }
3824              item.setElementGroup( null );
3825              this.items.splice( index, 1 );
3826              item.$element.detach();
3827          }
3828      }
3829  
3830      return this;
3831  };
3832  
3833  /**
3834   * Clear all items.
3835   *
3836   * Items will be detached, not removed, so they can be used later.
3837   *
3838   * @chainable
3839   */
3840  OO.ui.GroupElement.prototype.clearItems = function () {
3841      var i, len, item, remove, itemEvent;
3842  
3843      // Remove all items
3844      for ( i = 0, len = this.items.length; i < len; i++ ) {
3845          item = this.items[i];
3846          if (
3847              item.connect && item.disconnect &&
3848              !$.isEmptyObject( this.aggregateItemEvents )
3849          ) {
3850              remove = {};
3851              if ( itemEvent in this.aggregateItemEvents ) {
3852                  remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
3853              }
3854              item.disconnect( this, remove );
3855          }
3856          item.setElementGroup( null );
3857          item.$element.detach();
3858      }
3859  
3860      this.items = [];
3861      return this;
3862  };
3863  
3864  /**
3865   * Element containing an icon.
3866   *
3867   * Icons are graphics, about the size of normal text. They can be used to aid the user in locating
3868   * a control or convey information in a more space efficient way. Icons should rarely be used
3869   * without labels; such as in a toolbar where space is at a premium or within a context where the
3870   * meaning is very clear to the user.
3871   *
3872   * @abstract
3873   * @class
3874   *
3875   * @constructor
3876   * @param {Object} [config] Configuration options
3877   * @cfg {jQuery} [$icon] Icon node, assigned to #$icon, omit to use a generated `<span>`
3878   * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
3879   *  use the 'default' key to specify the icon to be used when there is no icon in the user's
3880   *  language
3881   * @cfg {string} [iconTitle] Icon title text or a function that returns text
3882   */
3883  OO.ui.IconElement = function OoUiIconElement( config ) {
3884      // Config intialization
3885      config = config || {};
3886  
3887      // Properties
3888      this.$icon = null;
3889      this.icon = null;
3890      this.iconTitle = null;
3891  
3892      // Initialization
3893      this.setIcon( config.icon || this.constructor.static.icon );
3894      this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
3895      this.setIconElement( config.$icon || this.$( '<span>' ) );
3896  };
3897  
3898  /* Setup */
3899  
3900  OO.initClass( OO.ui.IconElement );
3901  
3902  /* Static Properties */
3903  
3904  /**
3905   * Icon.
3906   *
3907   * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
3908   *
3909   * For i18n purposes, this property can be an object containing a `default` icon name property and
3910   * additional icon names keyed by language code.
3911   *
3912   * Example of i18n icon definition:
3913   *     { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
3914   *
3915   * @static
3916   * @inheritable
3917   * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
3918   *  use the 'default' key to specify the icon to be used when there is no icon in the user's
3919   *  language
3920   */
3921  OO.ui.IconElement.static.icon = null;
3922  
3923  /**
3924   * Icon title.
3925   *
3926   * @static
3927   * @inheritable
3928   * @property {string|Function|null} Icon title text, a function that returns text or null for no
3929   *  icon title
3930   */
3931  OO.ui.IconElement.static.iconTitle = null;
3932  
3933  /* Methods */
3934  
3935  /**
3936   * Set the icon element.
3937   *
3938   * If an element is already set, it will be cleaned up before setting up the new element.
3939   *
3940   * @param {jQuery} $icon Element to use as icon
3941   */
3942  OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
3943      if ( this.$icon ) {
3944          this.$icon
3945              .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
3946              .removeAttr( 'title' );
3947      }
3948  
3949      this.$icon = $icon
3950          .addClass( 'oo-ui-iconElement-icon' )
3951          .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
3952      if ( this.iconTitle !== null ) {
3953          this.$icon.attr( 'title', this.iconTitle );
3954      }
3955  };
3956  
3957  /**
3958   * Set icon.
3959   *
3960   * @param {Object|string|null} icon Symbolic icon name, or map of icon names keyed by language ID;
3961   *  use the 'default' key to specify the icon to be used when there is no icon in the user's
3962   *  language, use null to remove icon
3963   * @chainable
3964   */
3965  OO.ui.IconElement.prototype.setIcon = function ( icon ) {
3966      icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
3967      icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
3968  
3969      if ( this.icon !== icon ) {
3970          if ( this.$icon ) {
3971              if ( this.icon !== null ) {
3972                  this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
3973              }
3974              if ( icon !== null ) {
3975                  this.$icon.addClass( 'oo-ui-icon-' + icon );
3976              }
3977          }
3978          this.icon = icon;
3979      }
3980  
3981      this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
3982  
3983      return this;
3984  };
3985  
3986  /**
3987   * Set icon title.
3988   *
3989   * @param {string|Function|null} icon Icon title text, a function that returns text or null
3990   *  for no icon title
3991   * @chainable
3992   */
3993  OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
3994      iconTitle = typeof iconTitle === 'function' ||
3995          ( typeof iconTitle === 'string' && iconTitle.length ) ?
3996              OO.ui.resolveMsg( iconTitle ) : null;
3997  
3998      if ( this.iconTitle !== iconTitle ) {
3999          this.iconTitle = iconTitle;
4000          if ( this.$icon ) {
4001              if ( this.iconTitle !== null ) {
4002                  this.$icon.attr( 'title', iconTitle );
4003              } else {
4004                  this.$icon.removeAttr( 'title' );
4005              }
4006          }
4007      }
4008  
4009      return this;
4010  };
4011  
4012  /**
4013   * Get icon.
4014   *
4015   * @return {string} Icon
4016   */
4017  OO.ui.IconElement.prototype.getIcon = function () {
4018      return this.icon;
4019  };
4020  
4021  /**
4022   * Element containing an indicator.
4023   *
4024   * Indicators are graphics, smaller than normal text. They can be used to describe unique status or
4025   * behavior. Indicators should only be used in exceptional cases; such as a button that opens a menu
4026   * instead of performing an action directly, or an item in a list which has errors that need to be
4027   * resolved.
4028   *
4029   * @abstract
4030   * @class
4031   *
4032   * @constructor
4033   * @param {Object} [config] Configuration options
4034   * @cfg {jQuery} [$indicator] Indicator node, assigned to #$indicator, omit to use a generated
4035   *   `<span>`
4036   * @cfg {string} [indicator] Symbolic indicator name
4037   * @cfg {string} [indicatorTitle] Indicator title text or a function that returns text
4038   */
4039  OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
4040      // Config intialization
4041      config = config || {};
4042  
4043      // Properties
4044      this.$indicator = null;
4045      this.indicator = null;
4046      this.indicatorTitle = null;
4047  
4048      // Initialization
4049      this.setIndicator( config.indicator || this.constructor.static.indicator );
4050      this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
4051      this.setIndicatorElement( config.$indicator || this.$( '<span>' ) );
4052  };
4053  
4054  /* Setup */
4055  
4056  OO.initClass( OO.ui.IndicatorElement );
4057  
4058  /* Static Properties */
4059  
4060  /**
4061   * indicator.
4062   *
4063   * @static
4064   * @inheritable
4065   * @property {string|null} Symbolic indicator name or null for no indicator
4066   */
4067  OO.ui.IndicatorElement.static.indicator = null;
4068  
4069  /**
4070   * Indicator title.
4071   *
4072   * @static
4073   * @inheritable
4074   * @property {string|Function|null} Indicator title text, a function that returns text or null for no
4075   *  indicator title
4076   */
4077  OO.ui.IndicatorElement.static.indicatorTitle = null;
4078  
4079  /* Methods */
4080  
4081  /**
4082   * Set the indicator element.
4083   *
4084   * If an element is already set, it will be cleaned up before setting up the new element.
4085   *
4086   * @param {jQuery} $indicator Element to use as indicator
4087   */
4088  OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
4089      if ( this.$indicator ) {
4090          this.$indicator
4091              .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
4092              .removeAttr( 'title' );
4093      }
4094  
4095      this.$indicator = $indicator
4096          .addClass( 'oo-ui-indicatorElement-indicator' )
4097          .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
4098      if ( this.indicatorTitle !== null ) {
4099          this.$indicatorTitle.attr( 'title', this.indicatorTitle );
4100      }
4101  };
4102  
4103  /**
4104   * Set indicator.
4105   *
4106   * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
4107   * @chainable
4108   */
4109  OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
4110      indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
4111  
4112      if ( this.indicator !== indicator ) {
4113          if ( this.$indicator ) {
4114              if ( this.indicator !== null ) {
4115                  this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
4116              }
4117              if ( indicator !== null ) {
4118                  this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
4119              }
4120          }
4121          this.indicator = indicator;
4122      }
4123  
4124      this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
4125  
4126      return this;
4127  };
4128  
4129  /**
4130   * Set indicator title.
4131   *
4132   * @param {string|Function|null} indicator Indicator title text, a function that returns text or
4133   *   null for no indicator title
4134   * @chainable
4135   */
4136  OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
4137      indicatorTitle = typeof indicatorTitle === 'function' ||
4138          ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
4139              OO.ui.resolveMsg( indicatorTitle ) : null;
4140  
4141      if ( this.indicatorTitle !== indicatorTitle ) {
4142          this.indicatorTitle = indicatorTitle;
4143          if ( this.$indicator ) {
4144              if ( this.indicatorTitle !== null ) {
4145                  this.$indicator.attr( 'title', indicatorTitle );
4146              } else {
4147                  this.$indicator.removeAttr( 'title' );
4148              }
4149          }
4150      }
4151  
4152      return this;
4153  };
4154  
4155  /**
4156   * Get indicator.
4157   *
4158   * @return {string} title Symbolic name of indicator
4159   */
4160  OO.ui.IndicatorElement.prototype.getIndicator = function () {
4161      return this.indicator;
4162  };
4163  
4164  /**
4165   * Get indicator title.
4166   *
4167   * @return {string} Indicator title text
4168   */
4169  OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
4170      return this.indicatorTitle;
4171  };
4172  
4173  /**
4174   * Element containing a label.
4175   *
4176   * @abstract
4177   * @class
4178   *
4179   * @constructor
4180   * @param {Object} [config] Configuration options
4181   * @cfg {jQuery} [$label] Label node, assigned to #$label, omit to use a generated `<span>`
4182   * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
4183   * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
4184   */
4185  OO.ui.LabelElement = function OoUiLabelElement( config ) {
4186      // Config intialization
4187      config = config || {};
4188  
4189      // Properties
4190      this.$label = null;
4191      this.label = null;
4192      this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
4193  
4194      // Initialization
4195      this.setLabel( config.label || this.constructor.static.label );
4196      this.setLabelElement( config.$label || this.$( '<span>' ) );
4197  };
4198  
4199  /* Setup */
4200  
4201  OO.initClass( OO.ui.LabelElement );
4202  
4203  /* Static Properties */
4204  
4205  /**
4206   * Label.
4207   *
4208   * @static
4209   * @inheritable
4210   * @property {string|Function|null} Label text; a function that returns nodes or text; or null for
4211   *  no label
4212   */
4213  OO.ui.LabelElement.static.label = null;
4214  
4215  /* Methods */
4216  
4217  /**
4218   * Set the label element.
4219   *
4220   * If an element is already set, it will be cleaned up before setting up the new element.
4221   *
4222   * @param {jQuery} $label Element to use as label
4223   */
4224  OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) {
4225      if ( this.$label ) {
4226          this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
4227      }
4228  
4229      this.$label = $label.addClass( 'oo-ui-labelElement-label' );
4230      this.setLabelContent( this.label );
4231  };
4232  
4233  /**
4234   * Set the label.
4235   *
4236   * An empty string will result in the label being hidden. A string containing only whitespace will
4237   * be converted to a single &nbsp;
4238   *
4239   * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4240   *  text; or null for no label
4241   * @chainable
4242   */
4243  OO.ui.LabelElement.prototype.setLabel = function ( label ) {
4244      label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
4245      label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null;
4246  
4247      if ( this.label !== label ) {
4248          if ( this.$label ) {
4249              this.setLabelContent( label );
4250          }
4251          this.label = label;
4252      }
4253  
4254      this.$element.toggleClass( 'oo-ui-labelElement', !!this.label );
4255  
4256      return this;
4257  };
4258  
4259  /**
4260   * Get the label.
4261   *
4262   * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4263   *  text; or null for no label
4264   */
4265  OO.ui.LabelElement.prototype.getLabel = function () {
4266      return this.label;
4267  };
4268  
4269  /**
4270   * Fit the label.
4271   *
4272   * @chainable
4273   */
4274  OO.ui.LabelElement.prototype.fitLabel = function () {
4275      if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
4276          this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
4277      }
4278  
4279      return this;
4280  };
4281  
4282  /**
4283   * Set the content of the label.
4284   *
4285   * Do not call this method until after the label element has been set by #setLabelElement.
4286   *
4287   * @private
4288   * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4289   *  text; or null for no label
4290   */
4291  OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
4292      if ( typeof label === 'string' ) {
4293          if ( label.match( /^\s*$/ ) ) {
4294              // Convert whitespace only string to a single non-breaking space
4295              this.$label.html( '&nbsp;' );
4296          } else {
4297              this.$label.text( label );
4298          }
4299      } else if ( label instanceof jQuery ) {
4300          this.$label.empty().append( label );
4301      } else {
4302          this.$label.empty();
4303      }
4304      this.$label.css( 'display', !label ? 'none' : '' );
4305  };
4306  
4307  /**
4308   * Element containing an OO.ui.PopupWidget object.
4309   *
4310   * @abstract
4311   * @class
4312   *
4313   * @constructor
4314   * @param {Object} [config] Configuration options
4315   * @cfg {Object} [popup] Configuration to pass to popup
4316   * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
4317   */
4318  OO.ui.PopupElement = function OoUiPopupElement( config ) {
4319      // Configuration initialization
4320      config = config || {};
4321  
4322      // Properties
4323      this.popup = new OO.ui.PopupWidget( $.extend(
4324          { autoClose: true },
4325          config.popup,
4326          { $: this.$, $autoCloseIgnore: this.$element }
4327      ) );
4328  };
4329  
4330  /* Methods */
4331  
4332  /**
4333   * Get popup.
4334   *
4335   * @return {OO.ui.PopupWidget} Popup widget
4336   */
4337  OO.ui.PopupElement.prototype.getPopup = function () {
4338      return this.popup;
4339  };
4340  
4341  /**
4342   * Element with named flags that can be added, removed, listed and checked.
4343   *
4344   * A flag, when set, adds a CSS class on the `$element` by combining `oo-ui-flaggedElement-` with
4345   * the flag name. Flags are primarily useful for styling.
4346   *
4347   * @abstract
4348   * @class
4349   *
4350   * @constructor
4351   * @param {Object} [config] Configuration options
4352   * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive'
4353   * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element
4354   */
4355  OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
4356      // Config initialization
4357      config = config || {};
4358  
4359      // Properties
4360      this.flags = {};
4361      this.$flagged = null;
4362  
4363      // Initialization
4364      this.setFlags( config.flags );
4365      this.setFlaggedElement( config.$flagged || this.$element );
4366  };
4367  
4368  /* Events */
4369  
4370  /**
4371   * @event flag
4372   * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
4373   *   added/removed properties
4374   */
4375  
4376  /* Methods */
4377  
4378  /**
4379   * Set the flagged element.
4380   *
4381   * If an element is already set, it will be cleaned up before setting up the new element.
4382   *
4383   * @param {jQuery} $flagged Element to add flags to
4384   */
4385  OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
4386      var classNames = Object.keys( this.flags ).map( function ( flag ) {
4387          return 'oo-ui-flaggedElement-' + flag;
4388      } ).join( ' ' );
4389  
4390      if ( this.$flagged ) {
4391          this.$flagged.removeClass( classNames );
4392      }
4393  
4394      this.$flagged = $flagged.addClass( classNames );
4395  };
4396  
4397  /**
4398   * Check if a flag is set.
4399   *
4400   * @param {string} flag Name of flag
4401   * @return {boolean} Has flag
4402   */
4403  OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) {
4404      return flag in this.flags;
4405  };
4406  
4407  /**
4408   * Get the names of all flags set.
4409   *
4410   * @return {string[]} flags Flag names
4411   */
4412  OO.ui.FlaggedElement.prototype.getFlags = function () {
4413      return Object.keys( this.flags );
4414  };
4415  
4416  /**
4417   * Clear all flags.
4418   *
4419   * @chainable
4420   * @fires flag
4421   */
4422  OO.ui.FlaggedElement.prototype.clearFlags = function () {
4423      var flag, className,
4424          changes = {},
4425          remove = [],
4426          classPrefix = 'oo-ui-flaggedElement-';
4427  
4428      for ( flag in this.flags ) {
4429          className = classPrefix + flag;
4430          changes[flag] = false;
4431          delete this.flags[flag];
4432          remove.push( className );
4433      }
4434  
4435      if ( this.$flagged ) {
4436          this.$flagged.removeClass( remove.join( ' ' ) );
4437      }
4438  
4439      this.emit( 'flag', changes );
4440  
4441      return this;
4442  };
4443  
4444  /**
4445   * Add one or more flags.
4446   *
4447   * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
4448   *  keyed by flag name containing boolean set/remove instructions.
4449   * @chainable
4450   * @fires flag
4451   */
4452  OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
4453      var i, len, flag, className,
4454          changes = {},
4455          add = [],
4456          remove = [],
4457          classPrefix = 'oo-ui-flaggedElement-';
4458  
4459      if ( typeof flags === 'string' ) {
4460          className = classPrefix + flags;
4461          // Set
4462          if ( !this.flags[flags] ) {
4463              this.flags[flags] = true;
4464              add.push( className );
4465          }
4466      } else if ( $.isArray( flags ) ) {
4467          for ( i = 0, len = flags.length; i < len; i++ ) {
4468              flag = flags[i];
4469              className = classPrefix + flag;
4470              // Set
4471              if ( !this.flags[flag] ) {
4472                  changes[flag] = true;
4473                  this.flags[flag] = true;
4474                  add.push( className );
4475              }
4476          }
4477      } else if ( OO.isPlainObject( flags ) ) {
4478          for ( flag in flags ) {
4479              className = classPrefix + flag;
4480              if ( flags[flag] ) {
4481                  // Set
4482                  if ( !this.flags[flag] ) {
4483                      changes[flag] = true;
4484                      this.flags[flag] = true;
4485                      add.push( className );
4486                  }
4487              } else {
4488                  // Remove
4489                  if ( this.flags[flag] ) {
4490                      changes[flag] = false;
4491                      delete this.flags[flag];
4492                      remove.push( className );
4493                  }
4494              }
4495          }
4496      }
4497  
4498      if ( this.$flagged ) {
4499          this.$flagged
4500              .addClass( add.join( ' ' ) )
4501              .removeClass( remove.join( ' ' ) );
4502      }
4503  
4504      this.emit( 'flag', changes );
4505  
4506      return this;
4507  };
4508  
4509  /**
4510   * Element with a title.
4511   *
4512   * Titles are rendered by the browser and are made visible when hovering the element. Titles are
4513   * not visible on touch devices.
4514   *
4515   * @abstract
4516   * @class
4517   *
4518   * @constructor
4519   * @param {Object} [config] Configuration options
4520   * @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element
4521   * @cfg {string|Function} [title] Title text or a function that returns text
4522   */
4523  OO.ui.TitledElement = function OoUiTitledElement( config ) {
4524      // Config intialization
4525      config = config || {};
4526  
4527      // Properties
4528      this.$titled = null;
4529      this.title = null;
4530  
4531      // Initialization
4532      this.setTitle( config.title || this.constructor.static.title );
4533      this.setTitledElement( config.$titled || this.$element );
4534  };
4535  
4536  /* Setup */
4537  
4538  OO.initClass( OO.ui.TitledElement );
4539  
4540  /* Static Properties */
4541  
4542  /**
4543   * Title.
4544   *
4545   * @static
4546   * @inheritable
4547   * @property {string|Function} Title text or a function that returns text
4548   */
4549  OO.ui.TitledElement.static.title = null;
4550  
4551  /* Methods */
4552  
4553  /**
4554   * Set the titled element.
4555   *
4556   * If an element is already set, it will be cleaned up before setting up the new element.
4557   *
4558   * @param {jQuery} $titled Element to set title on
4559   */
4560  OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
4561      if ( this.$titled ) {
4562          this.$titled.removeAttr( 'title' );
4563      }
4564  
4565      this.$titled = $titled;
4566      if ( this.title ) {
4567          this.$titled.attr( 'title', this.title );
4568      }
4569  };
4570  
4571  /**
4572   * Set title.
4573   *
4574   * @param {string|Function|null} title Title text, a function that returns text or null for no title
4575   * @chainable
4576   */
4577  OO.ui.TitledElement.prototype.setTitle = function ( title ) {
4578      title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
4579  
4580      if ( this.title !== title ) {
4581          if ( this.$titled ) {
4582              if ( title !== null ) {
4583                  this.$titled.attr( 'title', title );
4584              } else {
4585                  this.$titled.removeAttr( 'title' );
4586              }
4587          }
4588          this.title = title;
4589      }
4590  
4591      return this;
4592  };
4593  
4594  /**
4595   * Get title.
4596   *
4597   * @return {string} Title string
4598   */
4599  OO.ui.TitledElement.prototype.getTitle = function () {
4600      return this.title;
4601  };
4602  
4603  /**
4604   * Element that can be automatically clipped to visible boundaries.
4605   *
4606   * Whenever the element's natural height changes, you have to call
4607   * #clip to make sure it's still clipping correctly.
4608   *
4609   * @abstract
4610   * @class
4611   *
4612   * @constructor
4613   * @param {Object} [config] Configuration options
4614   * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
4615   */
4616  OO.ui.ClippableElement = function OoUiClippableElement( config ) {
4617      // Configuration initialization
4618      config = config || {};
4619  
4620      // Properties
4621      this.$clippable = null;
4622      this.clipping = false;
4623      this.clippedHorizontally = false;
4624      this.clippedVertically = false;
4625      this.$clippableContainer = null;
4626      this.$clippableScroller = null;
4627      this.$clippableWindow = null;
4628      this.idealWidth = null;
4629      this.idealHeight = null;
4630      this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this );
4631      this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this );
4632  
4633      // Initialization
4634      this.setClippableElement( config.$clippable || this.$element );
4635  };
4636  
4637  /* Methods */
4638  
4639  /**
4640   * Set clippable element.
4641   *
4642   * If an element is already set, it will be cleaned up before setting up the new element.
4643   *
4644   * @param {jQuery} $clippable Element to make clippable
4645   */
4646  OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
4647      if ( this.$clippable ) {
4648          this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
4649          this.$clippable.css( { width: '', height: '' } );
4650          this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
4651          this.$clippable.css( { overflowX: '', overflowY: '' } );
4652      }
4653  
4654      this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
4655      this.clip();
4656  };
4657  
4658  /**
4659   * Toggle clipping.
4660   *
4661   * Do not turn clipping on until after the element is attached to the DOM and visible.
4662   *
4663   * @param {boolean} [clipping] Enable clipping, omit to toggle
4664   * @chainable
4665   */
4666  OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
4667      clipping = clipping === undefined ? !this.clipping : !!clipping;
4668  
4669      if ( this.clipping !== clipping ) {
4670          this.clipping = clipping;
4671          if ( clipping ) {
4672              this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
4673              // If the clippable container is the body, we have to listen to scroll events and check
4674              // jQuery.scrollTop on the window because of browser inconsistencies
4675              this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
4676                  this.$( OO.ui.Element.getWindow( this.$clippableContainer ) ) :
4677                  this.$clippableContainer;
4678              this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
4679              this.$clippableWindow = this.$( this.getElementWindow() )
4680                  .on( 'resize', this.onClippableWindowResizeHandler );
4681              // Initial clip after visible
4682              this.clip();
4683          } else {
4684              this.$clippable.css( { width: '', height: '' } );
4685              this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
4686              this.$clippable.css( { overflowX: '', overflowY: '' } );
4687  
4688              this.$clippableContainer = null;
4689              this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
4690              this.$clippableScroller = null;
4691              this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
4692              this.$clippableWindow = null;
4693          }
4694      }
4695  
4696      return this;
4697  };
4698  
4699  /**
4700   * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
4701   *
4702   * @return {boolean} Element will be clipped to the visible area
4703   */
4704  OO.ui.ClippableElement.prototype.isClipping = function () {
4705      return this.clipping;
4706  };
4707  
4708  /**
4709   * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
4710   *
4711   * @return {boolean} Part of the element is being clipped
4712   */
4713  OO.ui.ClippableElement.prototype.isClipped = function () {
4714      return this.clippedHorizontally || this.clippedVertically;
4715  };
4716  
4717  /**
4718   * Check if the right of the element is being clipped by the nearest scrollable container.
4719   *
4720   * @return {boolean} Part of the element is being clipped
4721   */
4722  OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
4723      return this.clippedHorizontally;
4724  };
4725  
4726  /**
4727   * Check if the bottom of the element is being clipped by the nearest scrollable container.
4728   *
4729   * @return {boolean} Part of the element is being clipped
4730   */
4731  OO.ui.ClippableElement.prototype.isClippedVertically = function () {
4732      return this.clippedVertically;
4733  };
4734  
4735  /**
4736   * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
4737   *
4738   * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
4739   * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
4740   */
4741  OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
4742      this.idealWidth = width;
4743      this.idealHeight = height;
4744  
4745      if ( !this.clipping ) {
4746          // Update dimensions
4747          this.$clippable.css( { width: width, height: height } );
4748      }
4749      // While clipping, idealWidth and idealHeight are not considered
4750  };
4751  
4752  /**
4753   * Clip element to visible boundaries and allow scrolling when needed. Call this method when
4754   * the element's natural height changes.
4755   *
4756   * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
4757   * overlapped by, the visible area of the nearest scrollable container.
4758   *
4759   * @chainable
4760   */
4761  OO.ui.ClippableElement.prototype.clip = function () {
4762      if ( !this.clipping ) {
4763          // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
4764          return this;
4765      }
4766  
4767      var buffer = 10,
4768          cOffset = this.$clippable.offset(),
4769          $container = this.$clippableContainer.is( 'body' ) ?
4770              this.$clippableWindow : this.$clippableContainer,
4771          ccOffset = $container.offset() || { top: 0, left: 0 },
4772          ccHeight = $container.innerHeight() - buffer,
4773          ccWidth = $container.innerWidth() - buffer,
4774          scrollTop = this.$clippableScroller.scrollTop(),
4775          scrollLeft = this.$clippableScroller.scrollLeft(),
4776          desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
4777          desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
4778          naturalWidth = this.$clippable.prop( 'scrollWidth' ),
4779          naturalHeight = this.$clippable.prop( 'scrollHeight' ),
4780          clipWidth = desiredWidth < naturalWidth,
4781          clipHeight = desiredHeight < naturalHeight;
4782  
4783      if ( clipWidth ) {
4784          this.$clippable.css( { overflowX: 'auto', width: desiredWidth } );
4785      } else {
4786          this.$clippable.css( 'width', this.idealWidth || '' );
4787          this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
4788          this.$clippable.css( 'overflowX', '' );
4789      }
4790      if ( clipHeight ) {
4791          this.$clippable.css( { overflowY: 'auto', height: desiredHeight } );
4792      } else {
4793          this.$clippable.css( 'height', this.idealHeight || '' );
4794          this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
4795          this.$clippable.css( 'overflowY', '' );
4796      }
4797  
4798      this.clippedHorizontally = clipWidth;
4799      this.clippedVertically = clipHeight;
4800  
4801      return this;
4802  };
4803  
4804  /**
4805   * Generic toolbar tool.
4806   *
4807   * @abstract
4808   * @class
4809   * @extends OO.ui.Widget
4810   * @mixins OO.ui.IconElement
4811   *
4812   * @constructor
4813   * @param {OO.ui.ToolGroup} toolGroup
4814   * @param {Object} [config] Configuration options
4815   * @cfg {string|Function} [title] Title text or a function that returns text
4816   */
4817  OO.ui.Tool = function OoUiTool( toolGroup, config ) {
4818      // Config intialization
4819      config = config || {};
4820  
4821      // Parent constructor
4822      OO.ui.Tool.super.call( this, config );
4823  
4824      // Mixin constructors
4825      OO.ui.IconElement.call( this, config );
4826  
4827      // Properties
4828      this.toolGroup = toolGroup;
4829      this.toolbar = this.toolGroup.getToolbar();
4830      this.active = false;
4831      this.$title = this.$( '<span>' );
4832      this.$link = this.$( '<a>' );
4833      this.title = null;
4834  
4835      // Events
4836      this.toolbar.connect( this, { updateState: 'onUpdateState' } );
4837  
4838      // Initialization
4839      this.$title.addClass( 'oo-ui-tool-title' );
4840      this.$link
4841          .addClass( 'oo-ui-tool-link' )
4842          .append( this.$icon, this.$title )
4843          .prop( 'tabIndex', 0 )
4844          .attr( 'role', 'button' );
4845      this.$element
4846          .data( 'oo-ui-tool', this )
4847          .addClass(
4848              'oo-ui-tool ' + 'oo-ui-tool-name-' +
4849              this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
4850          )
4851          .append( this.$link );
4852      this.setTitle( config.title || this.constructor.static.title );
4853  };
4854  
4855  /* Setup */
4856  
4857  OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
4858  OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
4859  
4860  /* Events */
4861  
4862  /**
4863   * @event select
4864   */
4865  
4866  /* Static Properties */
4867  
4868  /**
4869   * @static
4870   * @inheritdoc
4871   */
4872  OO.ui.Tool.static.tagName = 'span';
4873  
4874  /**
4875   * Symbolic name of tool.
4876   *
4877   * @abstract
4878   * @static
4879   * @inheritable
4880   * @property {string}
4881   */
4882  OO.ui.Tool.static.name = '';
4883  
4884  /**
4885   * Tool group.
4886   *
4887   * @abstract
4888   * @static
4889   * @inheritable
4890   * @property {string}
4891   */
4892  OO.ui.Tool.static.group = '';
4893  
4894  /**
4895   * Tool title.
4896   *
4897   * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
4898   * is part of a list or menu tool group. If a trigger is associated with an action by the same name
4899   * as the tool, a description of its keyboard shortcut for the appropriate platform will be
4900   * appended to the title if the tool is part of a bar tool group.
4901   *
4902   * @abstract
4903   * @static
4904   * @inheritable
4905   * @property {string|Function} Title text or a function that returns text
4906   */
4907  OO.ui.Tool.static.title = '';
4908  
4909  /**
4910   * Tool can be automatically added to catch-all groups.
4911   *
4912   * @static
4913   * @inheritable
4914   * @property {boolean}
4915   */
4916  OO.ui.Tool.static.autoAddToCatchall = true;
4917  
4918  /**
4919   * Tool can be automatically added to named groups.
4920   *
4921   * @static
4922   * @property {boolean}
4923   * @inheritable
4924   */
4925  OO.ui.Tool.static.autoAddToGroup = true;
4926  
4927  /**
4928   * Check if this tool is compatible with given data.
4929   *
4930   * @static
4931   * @inheritable
4932   * @param {Mixed} data Data to check
4933   * @return {boolean} Tool can be used with data
4934   */
4935  OO.ui.Tool.static.isCompatibleWith = function () {
4936      return false;
4937  };
4938  
4939  /* Methods */
4940  
4941  /**
4942   * Handle the toolbar state being updated.
4943   *
4944   * This is an abstract method that must be overridden in a concrete subclass.
4945   *
4946   * @abstract
4947   */
4948  OO.ui.Tool.prototype.onUpdateState = function () {
4949      throw new Error(
4950          'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
4951      );
4952  };
4953  
4954  /**
4955   * Handle the tool being selected.
4956   *
4957   * This is an abstract method that must be overridden in a concrete subclass.
4958   *
4959   * @abstract
4960   */
4961  OO.ui.Tool.prototype.onSelect = function () {
4962      throw new Error(
4963          'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
4964      );
4965  };
4966  
4967  /**
4968   * Check if the button is active.
4969   *
4970   * @param {boolean} Button is active
4971   */
4972  OO.ui.Tool.prototype.isActive = function () {
4973      return this.active;
4974  };
4975  
4976  /**
4977   * Make the button appear active or inactive.
4978   *
4979   * @param {boolean} state Make button appear active
4980   */
4981  OO.ui.Tool.prototype.setActive = function ( state ) {
4982      this.active = !!state;
4983      if ( this.active ) {
4984          this.$element.addClass( 'oo-ui-tool-active' );
4985      } else {
4986          this.$element.removeClass( 'oo-ui-tool-active' );
4987      }
4988  };
4989  
4990  /**
4991   * Get the tool title.
4992   *
4993   * @param {string|Function} title Title text or a function that returns text
4994   * @chainable
4995   */
4996  OO.ui.Tool.prototype.setTitle = function ( title ) {
4997      this.title = OO.ui.resolveMsg( title );
4998      this.updateTitle();
4999      return this;
5000  };
5001  
5002  /**
5003   * Get the tool title.
5004   *
5005   * @return {string} Title text
5006   */
5007  OO.ui.Tool.prototype.getTitle = function () {
5008      return this.title;
5009  };
5010  
5011  /**
5012   * Get the tool's symbolic name.
5013   *
5014   * @return {string} Symbolic name of tool
5015   */
5016  OO.ui.Tool.prototype.getName = function () {
5017      return this.constructor.static.name;
5018  };
5019  
5020  /**
5021   * Update the title.
5022   */
5023  OO.ui.Tool.prototype.updateTitle = function () {
5024      var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
5025          accelTooltips = this.toolGroup.constructor.static.accelTooltips,
5026          accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
5027          tooltipParts = [];
5028  
5029      this.$title.empty()
5030          .text( this.title )
5031          .append(
5032              this.$( '<span>' )
5033                  .addClass( 'oo-ui-tool-accel' )
5034                  .text( accel )
5035          );
5036  
5037      if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
5038          tooltipParts.push( this.title );
5039      }
5040      if ( accelTooltips && typeof accel === 'string' && accel.length ) {
5041          tooltipParts.push( accel );
5042      }
5043      if ( tooltipParts.length ) {
5044          this.$link.attr( 'title', tooltipParts.join( ' ' ) );
5045      } else {
5046          this.$link.removeAttr( 'title' );
5047      }
5048  };
5049  
5050  /**
5051   * Destroy tool.
5052   */
5053  OO.ui.Tool.prototype.destroy = function () {
5054      this.toolbar.disconnect( this );
5055      this.$element.remove();
5056  };
5057  
5058  /**
5059   * Collection of tool groups.
5060   *
5061   * @class
5062   * @extends OO.ui.Element
5063   * @mixins OO.EventEmitter
5064   * @mixins OO.ui.GroupElement
5065   *
5066   * @constructor
5067   * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
5068   * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
5069   * @param {Object} [config] Configuration options
5070   * @cfg {boolean} [actions] Add an actions section opposite to the tools
5071   * @cfg {boolean} [shadow] Add a shadow below the toolbar
5072   */
5073  OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
5074      // Configuration initialization
5075      config = config || {};
5076  
5077      // Parent constructor
5078      OO.ui.Toolbar.super.call( this, config );
5079  
5080      // Mixin constructors
5081      OO.EventEmitter.call( this );
5082      OO.ui.GroupElement.call( this, config );
5083  
5084      // Properties
5085      this.toolFactory = toolFactory;
5086      this.toolGroupFactory = toolGroupFactory;
5087      this.groups = [];
5088      this.tools = {};
5089      this.$bar = this.$( '<div>' );
5090      this.$actions = this.$( '<div>' );
5091      this.initialized = false;
5092  
5093      // Events
5094      this.$element
5095          .add( this.$bar ).add( this.$group ).add( this.$actions )
5096          .on( 'mousedown touchstart', OO.ui.bind( this.onPointerDown, this ) );
5097  
5098      // Initialization
5099      this.$group.addClass( 'oo-ui-toolbar-tools' );
5100      this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group );
5101      if ( config.actions ) {
5102          this.$actions.addClass( 'oo-ui-toolbar-actions' );
5103          this.$bar.append( this.$actions );
5104      }
5105      this.$bar.append( '<div style="clear:both"></div>' );
5106      if ( config.shadow ) {
5107          this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
5108      }
5109      this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
5110  };
5111  
5112  /* Setup */
5113  
5114  OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
5115  OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
5116  OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
5117  
5118  /* Methods */
5119  
5120  /**
5121   * Get the tool factory.
5122   *
5123   * @return {OO.ui.ToolFactory} Tool factory
5124   */
5125  OO.ui.Toolbar.prototype.getToolFactory = function () {
5126      return this.toolFactory;
5127  };
5128  
5129  /**
5130   * Get the tool group factory.
5131   *
5132   * @return {OO.Factory} Tool group factory
5133   */
5134  OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
5135      return this.toolGroupFactory;
5136  };
5137  
5138  /**
5139   * Handles mouse down events.
5140   *
5141   * @param {jQuery.Event} e Mouse down event
5142   */
5143  OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
5144      var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
5145          $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
5146      if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) {
5147          return false;
5148      }
5149  };
5150  
5151  /**
5152   * Sets up handles and preloads required information for the toolbar to work.
5153   * This must be called immediately after it is attached to a visible document.
5154   */
5155  OO.ui.Toolbar.prototype.initialize = function () {
5156      this.initialized = true;
5157  };
5158  
5159  /**
5160   * Setup toolbar.
5161   *
5162   * Tools can be specified in the following ways:
5163   *
5164   * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
5165   * - All tools in a group: `{ group: 'group-name' }`
5166   * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
5167   *
5168   * @param {Object.<string,Array>} groups List of tool group configurations
5169   * @param {Array|string} [groups.include] Tools to include
5170   * @param {Array|string} [groups.exclude] Tools to exclude
5171   * @param {Array|string} [groups.promote] Tools to promote to the beginning
5172   * @param {Array|string} [groups.demote] Tools to demote to the end
5173   */
5174  OO.ui.Toolbar.prototype.setup = function ( groups ) {
5175      var i, len, type, group,
5176          items = [],
5177          defaultType = 'bar';
5178  
5179      // Cleanup previous groups
5180      this.reset();
5181  
5182      // Build out new groups
5183      for ( i = 0, len = groups.length; i < len; i++ ) {
5184          group = groups[i];
5185          if ( group.include === '*' ) {
5186              // Apply defaults to catch-all groups
5187              if ( group.type === undefined ) {
5188                  group.type = 'list';
5189              }
5190              if ( group.label === undefined ) {
5191                  group.label = 'ooui-toolbar-more';
5192              }
5193          }
5194          // Check type has been registered
5195          type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
5196          items.push(
5197              this.getToolGroupFactory().create( type, this, $.extend( { $: this.$ }, group ) )
5198          );
5199      }
5200      this.addItems( items );
5201  };
5202  
5203  /**
5204   * Remove all tools and groups from the toolbar.
5205   */
5206  OO.ui.Toolbar.prototype.reset = function () {
5207      var i, len;
5208  
5209      this.groups = [];
5210      this.tools = {};
5211      for ( i = 0, len = this.items.length; i < len; i++ ) {
5212          this.items[i].destroy();
5213      }
5214      this.clearItems();
5215  };
5216  
5217  /**
5218   * Destroys toolbar, removing event handlers and DOM elements.
5219   *
5220   * Call this whenever you are done using a toolbar.
5221   */
5222  OO.ui.Toolbar.prototype.destroy = function () {
5223      this.reset();
5224      this.$element.remove();
5225  };
5226  
5227  /**
5228   * Check if tool has not been used yet.
5229   *
5230   * @param {string} name Symbolic name of tool
5231   * @return {boolean} Tool is available
5232   */
5233  OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
5234      return !this.tools[name];
5235  };
5236  
5237  /**
5238   * Prevent tool from being used again.
5239   *
5240   * @param {OO.ui.Tool} tool Tool to reserve
5241   */
5242  OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
5243      this.tools[tool.getName()] = tool;
5244  };
5245  
5246  /**
5247   * Allow tool to be used again.
5248   *
5249   * @param {OO.ui.Tool} tool Tool to release
5250   */
5251  OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
5252      delete this.tools[tool.getName()];
5253  };
5254  
5255  /**
5256   * Get accelerator label for tool.
5257   *
5258   * This is a stub that should be overridden to provide access to accelerator information.
5259   *
5260   * @param {string} name Symbolic name of tool
5261   * @return {string|undefined} Tool accelerator label if available
5262   */
5263  OO.ui.Toolbar.prototype.getToolAccelerator = function () {
5264      return undefined;
5265  };
5266  
5267  /**
5268   * Collection of tools.
5269   *
5270   * Tools can be specified in the following ways:
5271   *
5272   * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
5273   * - All tools in a group: `{ group: 'group-name' }`
5274   * - All tools: `'*'`
5275   *
5276   * @abstract
5277   * @class
5278   * @extends OO.ui.Widget
5279   * @mixins OO.ui.GroupElement
5280   *
5281   * @constructor
5282   * @param {OO.ui.Toolbar} toolbar
5283   * @param {Object} [config] Configuration options
5284   * @cfg {Array|string} [include=[]] List of tools to include
5285   * @cfg {Array|string} [exclude=[]] List of tools to exclude
5286   * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
5287   * @cfg {Array|string} [demote=[]] List of tools to demote to the end
5288   */
5289  OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
5290      // Configuration initialization
5291      config = config || {};
5292  
5293      // Parent constructor
5294      OO.ui.ToolGroup.super.call( this, config );
5295  
5296      // Mixin constructors
5297      OO.ui.GroupElement.call( this, config );
5298  
5299      // Properties
5300      this.toolbar = toolbar;
5301      this.tools = {};
5302      this.pressed = null;
5303      this.autoDisabled = false;
5304      this.include = config.include || [];
5305      this.exclude = config.exclude || [];
5306      this.promote = config.promote || [];
5307      this.demote = config.demote || [];
5308      this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this );
5309  
5310      // Events
5311      this.$element.on( {
5312          'mousedown touchstart': OO.ui.bind( this.onPointerDown, this ),
5313          'mouseup touchend': OO.ui.bind( this.onPointerUp, this ),
5314          mouseover: OO.ui.bind( this.onMouseOver, this ),
5315          mouseout: OO.ui.bind( this.onMouseOut, this )
5316      } );
5317      this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
5318      this.aggregate( { disable: 'itemDisable' } );
5319      this.connect( this, { itemDisable: 'updateDisabled' } );
5320  
5321      // Initialization
5322      this.$group.addClass( 'oo-ui-toolGroup-tools' );
5323      this.$element
5324          .addClass( 'oo-ui-toolGroup' )
5325          .append( this.$group );
5326      this.populate();
5327  };
5328  
5329  /* Setup */
5330  
5331  OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
5332  OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
5333  
5334  /* Events */
5335  
5336  /**
5337   * @event update
5338   */
5339  
5340  /* Static Properties */
5341  
5342  /**
5343   * Show labels in tooltips.
5344   *
5345   * @static
5346   * @inheritable
5347   * @property {boolean}
5348   */
5349  OO.ui.ToolGroup.static.titleTooltips = false;
5350  
5351  /**
5352   * Show acceleration labels in tooltips.
5353   *
5354   * @static
5355   * @inheritable
5356   * @property {boolean}
5357   */
5358  OO.ui.ToolGroup.static.accelTooltips = false;
5359  
5360  /**
5361   * Automatically disable the toolgroup when all tools are disabled
5362   *
5363   * @static
5364   * @inheritable
5365   * @property {boolean}
5366   */
5367  OO.ui.ToolGroup.static.autoDisable = true;
5368  
5369  /* Methods */
5370  
5371  /**
5372   * @inheritdoc
5373   */
5374  OO.ui.ToolGroup.prototype.isDisabled = function () {
5375      return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
5376  };
5377  
5378  /**
5379   * @inheritdoc
5380   */
5381  OO.ui.ToolGroup.prototype.updateDisabled = function () {
5382      var i, item, allDisabled = true;
5383  
5384      if ( this.constructor.static.autoDisable ) {
5385          for ( i = this.items.length - 1; i >= 0; i-- ) {
5386              item = this.items[i];
5387              if ( !item.isDisabled() ) {
5388                  allDisabled = false;
5389                  break;
5390              }
5391          }
5392          this.autoDisabled = allDisabled;
5393      }
5394      OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
5395  };
5396  
5397  /**
5398   * Handle mouse down events.
5399   *
5400   * @param {jQuery.Event} e Mouse down event
5401   */
5402  OO.ui.ToolGroup.prototype.onPointerDown = function ( e ) {
5403      // e.which is 0 for touch events, 1 for left mouse button
5404      if ( !this.isDisabled() && e.which <= 1 ) {
5405          this.pressed = this.getTargetTool( e );
5406          if ( this.pressed ) {
5407              this.pressed.setActive( true );
5408              this.getElementDocument().addEventListener(
5409                  'mouseup', this.onCapturedMouseUpHandler, true
5410              );
5411          }
5412      }
5413      return false;
5414  };
5415  
5416  /**
5417   * Handle captured mouse up events.
5418   *
5419   * @param {Event} e Mouse up event
5420   */
5421  OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
5422      this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
5423      // onPointerUp may be called a second time, depending on where the mouse is when the button is
5424      // released, but since `this.pressed` will no longer be true, the second call will be ignored.
5425      this.onPointerUp( e );
5426  };
5427  
5428  /**
5429   * Handle mouse up events.
5430   *
5431   * @param {jQuery.Event} e Mouse up event
5432   */
5433  OO.ui.ToolGroup.prototype.onPointerUp = function ( e ) {
5434      var tool = this.getTargetTool( e );
5435  
5436      // e.which is 0 for touch events, 1 for left mouse button
5437      if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === tool ) {
5438          this.pressed.onSelect();
5439      }
5440  
5441      this.pressed = null;
5442      return false;
5443  };
5444  
5445  /**
5446   * Handle mouse over events.
5447   *
5448   * @param {jQuery.Event} e Mouse over event
5449   */
5450  OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
5451      var tool = this.getTargetTool( e );
5452  
5453      if ( this.pressed && this.pressed === tool ) {
5454          this.pressed.setActive( true );
5455      }
5456  };
5457  
5458  /**
5459   * Handle mouse out events.
5460   *
5461   * @param {jQuery.Event} e Mouse out event
5462   */
5463  OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
5464      var tool = this.getTargetTool( e );
5465  
5466      if ( this.pressed && this.pressed === tool ) {
5467          this.pressed.setActive( false );
5468      }
5469  };
5470  
5471  /**
5472   * Get the closest tool to a jQuery.Event.
5473   *
5474   * Only tool links are considered, which prevents other elements in the tool such as popups from
5475   * triggering tool group interactions.
5476   *
5477   * @private
5478   * @param {jQuery.Event} e
5479   * @return {OO.ui.Tool|null} Tool, `null` if none was found
5480   */
5481  OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
5482      var tool,
5483          $item = this.$( e.target ).closest( '.oo-ui-tool-link' );
5484  
5485      if ( $item.length ) {
5486          tool = $item.parent().data( 'oo-ui-tool' );
5487      }
5488  
5489      return tool && !tool.isDisabled() ? tool : null;
5490  };
5491  
5492  /**
5493   * Handle tool registry register events.
5494   *
5495   * If a tool is registered after the group is created, we must repopulate the list to account for:
5496   *
5497   * - a tool being added that may be included
5498   * - a tool already included being overridden
5499   *
5500   * @param {string} name Symbolic name of tool
5501   */
5502  OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
5503      this.populate();
5504  };
5505  
5506  /**
5507   * Get the toolbar this group is in.
5508   *
5509   * @return {OO.ui.Toolbar} Toolbar of group
5510   */
5511  OO.ui.ToolGroup.prototype.getToolbar = function () {
5512      return this.toolbar;
5513  };
5514  
5515  /**
5516   * Add and remove tools based on configuration.
5517   */
5518  OO.ui.ToolGroup.prototype.populate = function () {
5519      var i, len, name, tool,
5520          toolFactory = this.toolbar.getToolFactory(),
5521          names = {},
5522          add = [],
5523          remove = [],
5524          list = this.toolbar.getToolFactory().getTools(
5525              this.include, this.exclude, this.promote, this.demote
5526          );
5527  
5528      // Build a list of needed tools
5529      for ( i = 0, len = list.length; i < len; i++ ) {
5530          name = list[i];
5531          if (
5532              // Tool exists
5533              toolFactory.lookup( name ) &&
5534              // Tool is available or is already in this group
5535              ( this.toolbar.isToolAvailable( name ) || this.tools[name] )
5536          ) {
5537              tool = this.tools[name];
5538              if ( !tool ) {
5539                  // Auto-initialize tools on first use
5540                  this.tools[name] = tool = toolFactory.create( name, this );
5541                  tool.updateTitle();
5542              }
5543              this.toolbar.reserveTool( tool );
5544              add.push( tool );
5545              names[name] = true;
5546          }
5547      }
5548      // Remove tools that are no longer needed
5549      for ( name in this.tools ) {
5550          if ( !names[name] ) {
5551              this.tools[name].destroy();
5552              this.toolbar.releaseTool( this.tools[name] );
5553              remove.push( this.tools[name] );
5554              delete this.tools[name];
5555          }
5556      }
5557      if ( remove.length ) {
5558          this.removeItems( remove );
5559      }
5560      // Update emptiness state
5561      if ( add.length ) {
5562          this.$element.removeClass( 'oo-ui-toolGroup-empty' );
5563      } else {
5564          this.$element.addClass( 'oo-ui-toolGroup-empty' );
5565      }
5566      // Re-add tools (moving existing ones to new locations)
5567      this.addItems( add );
5568      // Disabled state may depend on items
5569      this.updateDisabled();
5570  };
5571  
5572  /**
5573   * Destroy tool group.
5574   */
5575  OO.ui.ToolGroup.prototype.destroy = function () {
5576      var name;
5577  
5578      this.clearItems();
5579      this.toolbar.getToolFactory().disconnect( this );
5580      for ( name in this.tools ) {
5581          this.toolbar.releaseTool( this.tools[name] );
5582          this.tools[name].disconnect( this ).destroy();
5583          delete this.tools[name];
5584      }
5585      this.$element.remove();
5586  };
5587  
5588  /**
5589   * Dialog for showing a message.
5590   *
5591   * User interface:
5592   * - Registers two actions by default (safe and primary).
5593   * - Renders action widgets in the footer.
5594   *
5595   * @class
5596   * @extends OO.ui.Dialog
5597   *
5598   * @constructor
5599   * @param {Object} [config] Configuration options
5600   */
5601  OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
5602      // Parent constructor
5603      OO.ui.MessageDialog.super.call( this, config );
5604  
5605      // Properties
5606      this.verticalActionLayout = null;
5607  
5608      // Initialization
5609      this.$element.addClass( 'oo-ui-messageDialog' );
5610  };
5611  
5612  /* Inheritance */
5613  
5614  OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
5615  
5616  /* Static Properties */
5617  
5618  OO.ui.MessageDialog.static.name = 'message';
5619  
5620  OO.ui.MessageDialog.static.size = 'small';
5621  
5622  OO.ui.MessageDialog.static.verbose = false;
5623  
5624  /**
5625   * Dialog title.
5626   *
5627   * A confirmation dialog's title should describe what the progressive action will do. An alert
5628   * dialog's title should describe what event occured.
5629   *
5630   * @static
5631   * inheritable
5632   * @property {jQuery|string|Function|null}
5633   */
5634  OO.ui.MessageDialog.static.title = null;
5635  
5636  /**
5637   * A confirmation dialog's message should describe the consequences of the progressive action. An
5638   * alert dialog's message should describe why the event occured.
5639   *
5640   * @static
5641   * inheritable
5642   * @property {jQuery|string|Function|null}
5643   */
5644  OO.ui.MessageDialog.static.message = null;
5645  
5646  OO.ui.MessageDialog.static.actions = [
5647      { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
5648      { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
5649  ];
5650  
5651  /* Methods */
5652  
5653  /**
5654   * @inheritdoc
5655   */
5656  OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
5657      this.fitActions();
5658      return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
5659  };
5660  
5661  /**
5662   * Toggle action layout between vertical and horizontal.
5663   *
5664   * @param {boolean} [value] Layout actions vertically, omit to toggle
5665   * @chainable
5666   */
5667  OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
5668      value = value === undefined ? !this.verticalActionLayout : !!value;
5669  
5670      if ( value !== this.verticalActionLayout ) {
5671          this.verticalActionLayout = value;
5672          this.$actions
5673              .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
5674              .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
5675      }
5676  
5677      return this;
5678  };
5679  
5680  /**
5681   * @inheritdoc
5682   */
5683  OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
5684      if ( action ) {
5685          return new OO.ui.Process( function () {
5686              this.close( { action: action } );
5687          }, this );
5688      }
5689      return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
5690  };
5691  
5692  /**
5693   * @inheritdoc
5694   *
5695   * @param {Object} [data] Dialog opening data
5696   * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
5697   * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
5698   * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
5699   * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
5700   *   action item
5701   */
5702  OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
5703      data = data || {};
5704  
5705      // Parent method
5706      return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
5707          .next( function () {
5708              this.title.setLabel(
5709                  data.title !== undefined ? data.title : this.constructor.static.title
5710              );
5711              this.message.setLabel(
5712                  data.message !== undefined ? data.message : this.constructor.static.message
5713              );
5714              this.message.$element.toggleClass(
5715                  'oo-ui-messageDialog-message-verbose',
5716                  data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
5717              );
5718          }, this );
5719  };
5720  
5721  /**
5722   * @inheritdoc
5723   */
5724  OO.ui.MessageDialog.prototype.getBodyHeight = function () {
5725      return Math.round( this.text.$element.outerHeight( true ) );
5726  };
5727  
5728  /**
5729   * @inheritdoc
5730   */
5731  OO.ui.MessageDialog.prototype.initialize = function () {
5732      // Parent method
5733      OO.ui.MessageDialog.super.prototype.initialize.call( this );
5734  
5735      // Properties
5736      this.$actions = this.$( '<div>' );
5737      this.container = new OO.ui.PanelLayout( {
5738          $: this.$, scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
5739      } );
5740      this.text = new OO.ui.PanelLayout( {
5741          $: this.$, padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
5742      } );
5743      this.message = new OO.ui.LabelWidget( {
5744          $: this.$, classes: [ 'oo-ui-messageDialog-message' ]
5745      } );
5746  
5747      // Initialization
5748      this.title.$element.addClass( 'oo-ui-messageDialog-title' );
5749      this.$content.addClass( 'oo-ui-messageDialog-content' );
5750      this.container.$element.append( this.text.$element );
5751      this.text.$element.append( this.title.$element, this.message.$element );
5752      this.$body.append( this.container.$element );
5753      this.$actions.addClass( 'oo-ui-messageDialog-actions' );
5754      this.$foot.append( this.$actions );
5755  };
5756  
5757  /**
5758   * @inheritdoc
5759   */
5760  OO.ui.MessageDialog.prototype.attachActions = function () {
5761      var i, len, other, special, others;
5762  
5763      // Parent method
5764      OO.ui.MessageDialog.super.prototype.attachActions.call( this );
5765  
5766      special = this.actions.getSpecial();
5767      others = this.actions.getOthers();
5768      if ( special.safe ) {
5769          this.$actions.append( special.safe.$element );
5770          special.safe.toggleFramed( false );
5771      }
5772      if ( others.length ) {
5773          for ( i = 0, len = others.length; i < len; i++ ) {
5774              other = others[i];
5775              this.$actions.append( other.$element );
5776              other.toggleFramed( false );
5777          }
5778      }
5779      if ( special.primary ) {
5780          this.$actions.append( special.primary.$element );
5781          special.primary.toggleFramed( false );
5782      }
5783  
5784      this.fitActions();
5785      if ( !this.isOpening() ) {
5786          this.manager.updateWindowSize( this );
5787      }
5788      this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
5789  };
5790  
5791  /**
5792   * Fit action actions into columns or rows.
5793   *
5794   * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
5795   */
5796  OO.ui.MessageDialog.prototype.fitActions = function () {
5797      var i, len, action,
5798          actions = this.actions.get();
5799  
5800      // Detect clipping
5801      this.toggleVerticalActionLayout( false );
5802      for ( i = 0, len = actions.length; i < len; i++ ) {
5803          action = actions[i];
5804          if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
5805              this.toggleVerticalActionLayout( true );
5806              break;
5807          }
5808      }
5809  };
5810  
5811  /**
5812   * Navigation dialog window.
5813   *
5814   * Logic:
5815   * - Show and hide errors.
5816   * - Retry an action.
5817   *
5818   * User interface:
5819   * - Renders header with dialog title and one action widget on either side
5820   *   (a 'safe' button on the left, and a 'primary' button on the right, both of
5821   *   which close the dialog).
5822   * - Displays any action widgets in the footer (none by default).
5823   * - Ability to dismiss errors.
5824   *
5825   * Subclass responsibilities:
5826   * - Register a 'safe' action.
5827   * - Register a 'primary' action.
5828   * - Add content to the dialog.
5829   *
5830   * @abstract
5831   * @class
5832   * @extends OO.ui.Dialog
5833   *
5834   * @constructor
5835   * @param {Object} [config] Configuration options
5836   */
5837  OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
5838      // Parent constructor
5839      OO.ui.ProcessDialog.super.call( this, config );
5840  
5841      // Initialization
5842      this.$element.addClass( 'oo-ui-processDialog' );
5843  };
5844  
5845  /* Setup */
5846  
5847  OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
5848  
5849  /* Methods */
5850  
5851  /**
5852   * Handle dismiss button click events.
5853   *
5854   * Hides errors.
5855   */
5856  OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
5857      this.hideErrors();
5858  };
5859  
5860  /**
5861   * Handle retry button click events.
5862   *
5863   * Hides errors and then tries again.
5864   */
5865  OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
5866      this.hideErrors();
5867      this.executeAction( this.currentAction.getAction() );
5868  };
5869  
5870  /**
5871   * @inheritdoc
5872   */
5873  OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
5874      if ( this.actions.isSpecial( action ) ) {
5875          this.fitLabel();
5876      }
5877      return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
5878  };
5879  
5880  /**
5881   * @inheritdoc
5882   */
5883  OO.ui.ProcessDialog.prototype.initialize = function () {
5884      // Parent method
5885      OO.ui.ProcessDialog.super.prototype.initialize.call( this );
5886  
5887      // Properties
5888      this.$navigation = this.$( '<div>' );
5889      this.$location = this.$( '<div>' );
5890      this.$safeActions = this.$( '<div>' );
5891      this.$primaryActions = this.$( '<div>' );
5892      this.$otherActions = this.$( '<div>' );
5893      this.dismissButton = new OO.ui.ButtonWidget( {
5894          $: this.$,
5895          label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
5896      } );
5897      this.retryButton = new OO.ui.ButtonWidget( {
5898          $: this.$,
5899          label: OO.ui.msg( 'ooui-dialog-process-retry' )
5900      } );
5901      this.$errors = this.$( '<div>' );
5902      this.$errorsTitle = this.$( '<div>' );
5903  
5904      // Events
5905      this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
5906      this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
5907  
5908      // Initialization
5909      this.title.$element.addClass( 'oo-ui-processDialog-title' );
5910      this.$location
5911          .append( this.title.$element )
5912          .addClass( 'oo-ui-processDialog-location' );
5913      this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
5914      this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
5915      this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
5916      this.$errorsTitle
5917          .addClass( 'oo-ui-processDialog-errors-title' )
5918          .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
5919      this.$errors
5920          .addClass( 'oo-ui-processDialog-errors' )
5921          .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
5922      this.$content
5923          .addClass( 'oo-ui-processDialog-content' )
5924          .append( this.$errors );
5925      this.$navigation
5926          .addClass( 'oo-ui-processDialog-navigation' )
5927          .append( this.$safeActions, this.$location, this.$primaryActions );
5928      this.$head.append( this.$navigation );
5929      this.$foot.append( this.$otherActions );
5930  };
5931  
5932  /**
5933   * @inheritdoc
5934   */
5935  OO.ui.ProcessDialog.prototype.attachActions = function () {
5936      var i, len, other, special, others;
5937  
5938      // Parent method
5939      OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
5940  
5941      special = this.actions.getSpecial();
5942      others = this.actions.getOthers();
5943      if ( special.primary ) {
5944          this.$primaryActions.append( special.primary.$element );
5945          special.primary.toggleFramed( true );
5946      }
5947      if ( others.length ) {
5948          for ( i = 0, len = others.length; i < len; i++ ) {
5949              other = others[i];
5950              this.$otherActions.append( other.$element );
5951              other.toggleFramed( true );
5952          }
5953      }
5954      if ( special.safe ) {
5955          this.$safeActions.append( special.safe.$element );
5956          special.safe.toggleFramed( true );
5957      }
5958  
5959      this.fitLabel();
5960      this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
5961  };
5962  
5963  /**
5964   * @inheritdoc
5965   */
5966  OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
5967      OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
5968          .fail( OO.ui.bind( this.showErrors, this ) );
5969  };
5970  
5971  /**
5972   * Fit label between actions.
5973   *
5974   * @chainable
5975   */
5976  OO.ui.ProcessDialog.prototype.fitLabel = function () {
5977      var width = Math.max(
5978          this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
5979          this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
5980      );
5981      this.$location.css( { paddingLeft: width, paddingRight: width } );
5982  
5983      return this;
5984  };
5985  
5986  /**
5987   * Handle errors that occured durring accept or reject processes.
5988   *
5989   * @param {OO.ui.Error[]} errors Errors to be handled
5990   */
5991  OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
5992      var i, len, $item,
5993          items = [],
5994          recoverable = true;
5995  
5996      for ( i = 0, len = errors.length; i < len; i++ ) {
5997          if ( !errors[i].isRecoverable() ) {
5998              recoverable = false;
5999          }
6000          $item = this.$( '<div>' )
6001              .addClass( 'oo-ui-processDialog-error' )
6002              .append( errors[i].getMessage() );
6003          items.push( $item[0] );
6004      }
6005      this.$errorItems = this.$( items );
6006      if ( recoverable ) {
6007          this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() );
6008      } else {
6009          this.currentAction.setDisabled( true );
6010      }
6011      this.retryButton.toggle( recoverable );
6012      this.$errorsTitle.after( this.$errorItems );
6013      this.$errors.show().scrollTop( 0 );
6014  };
6015  
6016  /**
6017   * Hide errors.
6018   */
6019  OO.ui.ProcessDialog.prototype.hideErrors = function () {
6020      this.$errors.hide();
6021      this.$errorItems.remove();
6022      this.$errorItems = null;
6023  };
6024  
6025  /**
6026   * Layout containing a series of pages.
6027   *
6028   * @class
6029   * @extends OO.ui.Layout
6030   *
6031   * @constructor
6032   * @param {Object} [config] Configuration options
6033   * @cfg {boolean} [continuous=false] Show all pages, one after another
6034   * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
6035   * @cfg {boolean} [outlined=false] Show an outline
6036   * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
6037   */
6038  OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
6039      // Initialize configuration
6040      config = config || {};
6041  
6042      // Parent constructor
6043      OO.ui.BookletLayout.super.call( this, config );
6044  
6045      // Properties
6046      this.currentPageName = null;
6047      this.pages = {};
6048      this.ignoreFocus = false;
6049      this.stackLayout = new OO.ui.StackLayout( { $: this.$, continuous: !!config.continuous } );
6050      this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
6051      this.outlineVisible = false;
6052      this.outlined = !!config.outlined;
6053      if ( this.outlined ) {
6054          this.editable = !!config.editable;
6055          this.outlineControlsWidget = null;
6056          this.outlineWidget = new OO.ui.OutlineWidget( { $: this.$ } );
6057          this.outlinePanel = new OO.ui.PanelLayout( { $: this.$, scrollable: true } );
6058          this.gridLayout = new OO.ui.GridLayout(
6059              [ this.outlinePanel, this.stackLayout ],
6060              { $: this.$, widths: [ 1, 2 ] }
6061          );
6062          this.outlineVisible = true;
6063          if ( this.editable ) {
6064              this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
6065                  this.outlineWidget, { $: this.$ }
6066              );
6067          }
6068      }
6069  
6070      // Events
6071      this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
6072      if ( this.outlined ) {
6073          this.outlineWidget.connect( this, { select: 'onOutlineWidgetSelect' } );
6074      }
6075      if ( this.autoFocus ) {
6076          // Event 'focus' does not bubble, but 'focusin' does
6077          this.stackLayout.onDOMEvent( 'focusin', OO.ui.bind( this.onStackLayoutFocus, this ) );
6078      }
6079  
6080      // Initialization
6081      this.$element.addClass( 'oo-ui-bookletLayout' );
6082      this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
6083      if ( this.outlined ) {
6084          this.outlinePanel.$element
6085              .addClass( 'oo-ui-bookletLayout-outlinePanel' )
6086              .append( this.outlineWidget.$element );
6087          if ( this.editable ) {
6088              this.outlinePanel.$element
6089                  .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
6090                  .append( this.outlineControlsWidget.$element );
6091          }
6092          this.$element.append( this.gridLayout.$element );
6093      } else {
6094          this.$element.append( this.stackLayout.$element );
6095      }
6096  };
6097  
6098  /* Setup */
6099  
6100  OO.inheritClass( OO.ui.BookletLayout, OO.ui.Layout );
6101  
6102  /* Events */
6103  
6104  /**
6105   * @event set
6106   * @param {OO.ui.PageLayout} page Current page
6107   */
6108  
6109  /**
6110   * @event add
6111   * @param {OO.ui.PageLayout[]} page Added pages
6112   * @param {number} index Index pages were added at
6113   */
6114  
6115  /**
6116   * @event remove
6117   * @param {OO.ui.PageLayout[]} pages Removed pages
6118   */
6119  
6120  /* Methods */
6121  
6122  /**
6123   * Handle stack layout focus.
6124   *
6125   * @param {jQuery.Event} e Focusin event
6126   */
6127  OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
6128      var name, $target;
6129  
6130      // Find the page that an element was focused within
6131      $target = $( e.target ).closest( '.oo-ui-pageLayout' );
6132      for ( name in this.pages ) {
6133          // Check for page match, exclude current page to find only page changes
6134          if ( this.pages[name].$element[0] === $target[0] && name !== this.currentPageName ) {
6135              this.setPage( name );
6136              break;
6137          }
6138      }
6139  };
6140  
6141  /**
6142   * Handle stack layout set events.
6143   *
6144   * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
6145   */
6146  OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
6147      var $input, layout = this;
6148      if ( page ) {
6149          page.scrollElementIntoView( { complete: function () {
6150              if ( layout.autoFocus ) {
6151                  // Set focus to the first input if nothing on the page is focused yet
6152                  if ( !page.$element.find( ':focus' ).length ) {
6153                      $input = page.$element.find( ':input:first' );
6154                      if ( $input.length ) {
6155                          $input[0].focus();
6156                      }
6157                  }
6158              }
6159          } } );
6160      }
6161  };
6162  
6163  /**
6164   * Handle outline widget select events.
6165   *
6166   * @param {OO.ui.OptionWidget|null} item Selected item
6167   */
6168  OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) {
6169      if ( item ) {
6170          this.setPage( item.getData() );
6171      }
6172  };
6173  
6174  /**
6175   * Check if booklet has an outline.
6176   *
6177   * @return {boolean}
6178   */
6179  OO.ui.BookletLayout.prototype.isOutlined = function () {
6180      return this.outlined;
6181  };
6182  
6183  /**
6184   * Check if booklet has editing controls.
6185   *
6186   * @return {boolean}
6187   */
6188  OO.ui.BookletLayout.prototype.isEditable = function () {
6189      return this.editable;
6190  };
6191  
6192  /**
6193   * Check if booklet has a visible outline.
6194   *
6195   * @return {boolean}
6196   */
6197  OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
6198      return this.outlined && this.outlineVisible;
6199  };
6200  
6201  /**
6202   * Hide or show the outline.
6203   *
6204   * @param {boolean} [show] Show outline, omit to invert current state
6205   * @chainable
6206   */
6207  OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
6208      if ( this.outlined ) {
6209          show = show === undefined ? !this.outlineVisible : !!show;
6210          this.outlineVisible = show;
6211          this.gridLayout.layout( show ? [ 1, 2 ] : [ 0, 1 ], [ 1 ] );
6212      }
6213  
6214      return this;
6215  };
6216  
6217  /**
6218   * Get the outline widget.
6219   *
6220   * @param {OO.ui.PageLayout} page Page to be selected
6221   * @return {OO.ui.PageLayout|null} Closest page to another
6222   */
6223  OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
6224      var next, prev, level,
6225          pages = this.stackLayout.getItems(),
6226          index = $.inArray( page, pages );
6227  
6228      if ( index !== -1 ) {
6229          next = pages[index + 1];
6230          prev = pages[index - 1];
6231          // Prefer adjacent pages at the same level
6232          if ( this.outlined ) {
6233              level = this.outlineWidget.getItemFromData( page.getName() ).getLevel();
6234              if (
6235                  prev &&
6236                  level === this.outlineWidget.getItemFromData( prev.getName() ).getLevel()
6237              ) {
6238                  return prev;
6239              }
6240              if (
6241                  next &&
6242                  level === this.outlineWidget.getItemFromData( next.getName() ).getLevel()
6243              ) {
6244                  return next;
6245              }
6246          }
6247      }
6248      return prev || next || null;
6249  };
6250  
6251  /**
6252   * Get the outline widget.
6253   *
6254   * @return {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline
6255   */
6256  OO.ui.BookletLayout.prototype.getOutline = function () {
6257      return this.outlineWidget;
6258  };
6259  
6260  /**
6261   * Get the outline controls widget. If the outline is not editable, null is returned.
6262   *
6263   * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
6264   */
6265  OO.ui.BookletLayout.prototype.getOutlineControls = function () {
6266      return this.outlineControlsWidget;
6267  };
6268  
6269  /**
6270   * Get a page by name.
6271   *
6272   * @param {string} name Symbolic name of page
6273   * @return {OO.ui.PageLayout|undefined} Page, if found
6274   */
6275  OO.ui.BookletLayout.prototype.getPage = function ( name ) {
6276      return this.pages[name];
6277  };
6278  
6279  /**
6280   * Get the current page name.
6281   *
6282   * @return {string|null} Current page name
6283   */
6284  OO.ui.BookletLayout.prototype.getPageName = function () {
6285      return this.currentPageName;
6286  };
6287  
6288  /**
6289   * Add a page to the layout.
6290   *
6291   * When pages are added with the same names as existing pages, the existing pages will be
6292   * automatically removed before the new pages are added.
6293   *
6294   * @param {OO.ui.PageLayout[]} pages Pages to add
6295   * @param {number} index Index to insert pages after
6296   * @fires add
6297   * @chainable
6298   */
6299  OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
6300      var i, len, name, page, item, currentIndex,
6301          stackLayoutPages = this.stackLayout.getItems(),
6302          remove = [],
6303          items = [];
6304  
6305      // Remove pages with same names
6306      for ( i = 0, len = pages.length; i < len; i++ ) {
6307          page = pages[i];
6308          name = page.getName();
6309  
6310          if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
6311              // Correct the insertion index
6312              currentIndex = $.inArray( this.pages[name], stackLayoutPages );
6313              if ( currentIndex !== -1 && currentIndex + 1 < index ) {
6314                  index--;
6315              }
6316              remove.push( this.pages[name] );
6317          }
6318      }
6319      if ( remove.length ) {
6320          this.removePages( remove );
6321      }
6322  
6323      // Add new pages
6324      for ( i = 0, len = pages.length; i < len; i++ ) {
6325          page = pages[i];
6326          name = page.getName();
6327          this.pages[page.getName()] = page;
6328          if ( this.outlined ) {
6329              item = new OO.ui.OutlineItemWidget( name, page, { $: this.$ } );
6330              page.setOutlineItem( item );
6331              items.push( item );
6332          }
6333      }
6334  
6335      if ( this.outlined && items.length ) {
6336          this.outlineWidget.addItems( items, index );
6337          this.updateOutlineWidget();
6338      }
6339      this.stackLayout.addItems( pages, index );
6340      this.emit( 'add', pages, index );
6341  
6342      return this;
6343  };
6344  
6345  /**
6346   * Remove a page from the layout.
6347   *
6348   * @fires remove
6349   * @chainable
6350   */
6351  OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
6352      var i, len, name, page,
6353          items = [];
6354  
6355      for ( i = 0, len = pages.length; i < len; i++ ) {
6356          page = pages[i];
6357          name = page.getName();
6358          delete this.pages[name];
6359          if ( this.outlined ) {
6360              items.push( this.outlineWidget.getItemFromData( name ) );
6361              page.setOutlineItem( null );
6362          }
6363      }
6364      if ( this.outlined && items.length ) {
6365          this.outlineWidget.removeItems( items );
6366          this.updateOutlineWidget();
6367      }
6368      this.stackLayout.removeItems( pages );
6369      this.emit( 'remove', pages );
6370  
6371      return this;
6372  };
6373  
6374  /**
6375   * Clear all pages from the layout.
6376   *
6377   * @fires remove
6378   * @chainable
6379   */
6380  OO.ui.BookletLayout.prototype.clearPages = function () {
6381      var i, len,
6382          pages = this.stackLayout.getItems();
6383  
6384      this.pages = {};
6385      this.currentPageName = null;
6386      if ( this.outlined ) {
6387          this.outlineWidget.clearItems();
6388          for ( i = 0, len = pages.length; i < len; i++ ) {
6389              pages[i].setOutlineItem( null );
6390          }
6391      }
6392      this.stackLayout.clearItems();
6393  
6394      this.emit( 'remove', pages );
6395  
6396      return this;
6397  };
6398  
6399  /**
6400   * Set the current page by name.
6401   *
6402   * @fires set
6403   * @param {string} name Symbolic name of page
6404   */
6405  OO.ui.BookletLayout.prototype.setPage = function ( name ) {
6406      var selectedItem,
6407          $focused,
6408          page = this.pages[name];
6409  
6410      if ( name !== this.currentPageName ) {
6411          if ( this.outlined ) {
6412              selectedItem = this.outlineWidget.getSelectedItem();
6413              if ( selectedItem && selectedItem.getData() !== name ) {
6414                  this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) );
6415              }
6416          }
6417          if ( page ) {
6418              if ( this.currentPageName && this.pages[this.currentPageName] ) {
6419                  this.pages[this.currentPageName].setActive( false );
6420                  // Blur anything focused if the next page doesn't have anything focusable - this
6421                  // is not needed if the next page has something focusable because once it is focused
6422                  // this blur happens automatically
6423                  if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
6424                      $focused = this.pages[this.currentPageName].$element.find( ':focus' );
6425                      if ( $focused.length ) {
6426                          $focused[0].blur();
6427                      }
6428                  }
6429              }
6430              this.currentPageName = name;
6431              this.stackLayout.setItem( page );
6432              page.setActive( true );
6433              this.emit( 'set', page );
6434          }
6435      }
6436  };
6437  
6438  /**
6439   * Call this after adding or removing items from the OutlineWidget.
6440   *
6441   * @chainable
6442   */
6443  OO.ui.BookletLayout.prototype.updateOutlineWidget = function () {
6444      // Auto-select first item when nothing is selected anymore
6445      if ( !this.outlineWidget.getSelectedItem() ) {
6446          this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() );
6447      }
6448  
6449      return this;
6450  };
6451  
6452  /**
6453   * Layout made of a field and optional label.
6454   *
6455   * @class
6456   * @extends OO.ui.Layout
6457   * @mixins OO.ui.LabelElement
6458   *
6459   * Available label alignment modes include:
6460   *  - left: Label is before the field and aligned away from it, best for when the user will be
6461   *    scanning for a specific label in a form with many fields
6462   *  - right: Label is before the field and aligned toward it, best for forms the user is very
6463   *    familiar with and will tab through field checking quickly to verify which field they are in
6464   *  - top: Label is before the field and above it, best for when the use will need to fill out all
6465   *    fields from top to bottom in a form with few fields
6466   *  - inline: Label is after the field and aligned toward it, best for small boolean fields like
6467   *    checkboxes or radio buttons
6468   *
6469   * @constructor
6470   * @param {OO.ui.Widget} field Field widget
6471   * @param {Object} [config] Configuration options
6472   * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
6473   * @cfg {string} [help] Explanatory text shown as a '?' icon.
6474   */
6475  OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
6476      // Config initialization
6477      config = $.extend( { align: 'left' }, config );
6478  
6479      // Parent constructor
6480      OO.ui.FieldLayout.super.call( this, config );
6481  
6482      // Mixin constructors
6483      OO.ui.LabelElement.call( this, config );
6484  
6485      // Properties
6486      this.$field = this.$( '<div>' );
6487      this.field = field;
6488      this.align = null;
6489      if ( config.help ) {
6490          this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
6491              $: this.$,
6492              classes: [ 'oo-ui-fieldLayout-help' ],
6493              framed: false,
6494              icon: 'info'
6495          } );
6496  
6497          this.popupButtonWidget.getPopup().$body.append(
6498              this.$( '<div>' )
6499                  .text( config.help )
6500                  .addClass( 'oo-ui-fieldLayout-help-content' )
6501          );
6502          this.$help = this.popupButtonWidget.$element;
6503      } else {
6504          this.$help = this.$( [] );
6505      }
6506  
6507      // Events
6508      if ( this.field instanceof OO.ui.InputWidget ) {
6509          this.$label.on( 'click', OO.ui.bind( this.onLabelClick, this ) );
6510      }
6511      this.field.connect( this, { disable: 'onFieldDisable' } );
6512  
6513      // Initialization
6514      this.$element.addClass( 'oo-ui-fieldLayout' );
6515      this.$field
6516          .addClass( 'oo-ui-fieldLayout-field' )
6517          .toggleClass( 'oo-ui-fieldLayout-disable', this.field.isDisabled() )
6518          .append( this.field.$element );
6519      this.setAlignment( config.align );
6520  };
6521  
6522  /* Setup */
6523  
6524  OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
6525  OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
6526  
6527  /* Methods */
6528  
6529  /**
6530   * Handle field disable events.
6531   *
6532   * @param {boolean} value Field is disabled
6533   */
6534  OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
6535      this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
6536  };
6537  
6538  /**
6539   * Handle label mouse click events.
6540   *
6541   * @param {jQuery.Event} e Mouse click event
6542   */
6543  OO.ui.FieldLayout.prototype.onLabelClick = function () {
6544      this.field.simulateLabelClick();
6545      return false;
6546  };
6547  
6548  /**
6549   * Get the field.
6550   *
6551   * @return {OO.ui.Widget} Field widget
6552   */
6553  OO.ui.FieldLayout.prototype.getField = function () {
6554      return this.field;
6555  };
6556  
6557  /**
6558   * Set the field alignment mode.
6559   *
6560   * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
6561   * @chainable
6562   */
6563  OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
6564      if ( value !== this.align ) {
6565          // Default to 'left'
6566          if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
6567              value = 'left';
6568          }
6569          // Reorder elements
6570          if ( value === 'inline' ) {
6571              this.$element.append( this.$field, this.$label, this.$help );
6572          } else {
6573              this.$element.append( this.$help, this.$label, this.$field );
6574          }
6575          // Set classes
6576          if ( this.align ) {
6577              this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
6578          }
6579          this.align = value;
6580          // The following classes can be used here:
6581          // oo-ui-fieldLayout-align-left
6582          // oo-ui-fieldLayout-align-right
6583          // oo-ui-fieldLayout-align-top
6584          // oo-ui-fieldLayout-align-inline
6585          this.$element.addClass( 'oo-ui-fieldLayout-align-' + this.align );
6586      }
6587  
6588      return this;
6589  };
6590  
6591  /**
6592   * Layout made of a fieldset and optional legend.
6593   *
6594   * Just add OO.ui.FieldLayout items.
6595   *
6596   * @class
6597   * @extends OO.ui.Layout
6598   * @mixins OO.ui.LabelElement
6599   * @mixins OO.ui.IconElement
6600   * @mixins OO.ui.GroupElement
6601   *
6602   * @constructor
6603   * @param {Object} [config] Configuration options
6604   * @cfg {string} [icon] Symbolic icon name
6605   * @cfg {OO.ui.FieldLayout[]} [items] Items to add
6606   */
6607  OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
6608      // Config initialization
6609      config = config || {};
6610  
6611      // Parent constructor
6612      OO.ui.FieldsetLayout.super.call( this, config );
6613  
6614      // Mixin constructors
6615      OO.ui.IconElement.call( this, config );
6616      OO.ui.LabelElement.call( this, config );
6617      OO.ui.GroupElement.call( this, config );
6618  
6619      // Initialization
6620      this.$element
6621          .addClass( 'oo-ui-fieldsetLayout' )
6622          .prepend( this.$icon, this.$label, this.$group );
6623      if ( $.isArray( config.items ) ) {
6624          this.addItems( config.items );
6625      }
6626  };
6627  
6628  /* Setup */
6629  
6630  OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
6631  OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
6632  OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
6633  OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
6634  
6635  /* Static Properties */
6636  
6637  OO.ui.FieldsetLayout.static.tagName = 'div';
6638  
6639  /**
6640   * Layout with an HTML form.
6641   *
6642   * @class
6643   * @extends OO.ui.Layout
6644   *
6645   * @constructor
6646   * @param {Object} [config] Configuration options
6647   */
6648  OO.ui.FormLayout = function OoUiFormLayout( config ) {
6649      // Configuration initialization
6650      config = config || {};
6651  
6652      // Parent constructor
6653      OO.ui.FormLayout.super.call( this, config );
6654  
6655      // Events
6656      this.$element.on( 'submit', OO.ui.bind( this.onFormSubmit, this ) );
6657  
6658      // Initialization
6659      this.$element.addClass( 'oo-ui-formLayout' );
6660  };
6661  
6662  /* Setup */
6663  
6664  OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
6665  
6666  /* Events */
6667  
6668  /**
6669   * @event submit
6670   */
6671  
6672  /* Static Properties */
6673  
6674  OO.ui.FormLayout.static.tagName = 'form';
6675  
6676  /* Methods */
6677  
6678  /**
6679   * Handle form submit events.
6680   *
6681   * @param {jQuery.Event} e Submit event
6682   * @fires submit
6683   */
6684  OO.ui.FormLayout.prototype.onFormSubmit = function () {
6685      this.emit( 'submit' );
6686      return false;
6687  };
6688  
6689  /**
6690   * Layout made of proportionally sized columns and rows.
6691   *
6692   * @class
6693   * @extends OO.ui.Layout
6694   *
6695   * @constructor
6696   * @param {OO.ui.PanelLayout[]} panels Panels in the grid
6697   * @param {Object} [config] Configuration options
6698   * @cfg {number[]} [widths] Widths of columns as ratios
6699   * @cfg {number[]} [heights] Heights of columns as ratios
6700   */
6701  OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
6702      var i, len, widths;
6703  
6704      // Config initialization
6705      config = config || {};
6706  
6707      // Parent constructor
6708      OO.ui.GridLayout.super.call( this, config );
6709  
6710      // Properties
6711      this.panels = [];
6712      this.widths = [];
6713      this.heights = [];
6714  
6715      // Initialization
6716      this.$element.addClass( 'oo-ui-gridLayout' );
6717      for ( i = 0, len = panels.length; i < len; i++ ) {
6718          this.panels.push( panels[i] );
6719          this.$element.append( panels[i].$element );
6720      }
6721      if ( config.widths || config.heights ) {
6722          this.layout( config.widths || [ 1 ], config.heights || [ 1 ] );
6723      } else {
6724          // Arrange in columns by default
6725          widths = [];
6726          for ( i = 0, len = this.panels.length; i < len; i++ ) {
6727              widths[i] = 1;
6728          }
6729          this.layout( widths, [ 1 ] );
6730      }
6731  };
6732  
6733  /* Setup */
6734  
6735  OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
6736  
6737  /* Events */
6738  
6739  /**
6740   * @event layout
6741   */
6742  
6743  /**
6744   * @event update
6745   */
6746  
6747  /* Static Properties */
6748  
6749  OO.ui.GridLayout.static.tagName = 'div';
6750  
6751  /* Methods */
6752  
6753  /**
6754   * Set grid dimensions.
6755   *
6756   * @param {number[]} widths Widths of columns as ratios
6757   * @param {number[]} heights Heights of rows as ratios
6758   * @fires layout
6759   * @throws {Error} If grid is not large enough to fit all panels
6760   */
6761  OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
6762      var x, y,
6763          xd = 0,
6764          yd = 0,
6765          cols = widths.length,
6766          rows = heights.length;
6767  
6768      // Verify grid is big enough to fit panels
6769      if ( cols * rows < this.panels.length ) {
6770          throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
6771      }
6772  
6773      // Sum up denominators
6774      for ( x = 0; x < cols; x++ ) {
6775          xd += widths[x];
6776      }
6777      for ( y = 0; y < rows; y++ ) {
6778          yd += heights[y];
6779      }
6780      // Store factors
6781      this.widths = [];
6782      this.heights = [];
6783      for ( x = 0; x < cols; x++ ) {
6784          this.widths[x] = widths[x] / xd;
6785      }
6786      for ( y = 0; y < rows; y++ ) {
6787          this.heights[y] = heights[y] / yd;
6788      }
6789      // Synchronize view
6790      this.update();
6791      this.emit( 'layout' );
6792  };
6793  
6794  /**
6795   * Update panel positions and sizes.
6796   *
6797   * @fires update
6798   */
6799  OO.ui.GridLayout.prototype.update = function () {
6800      var x, y, panel,
6801          i = 0,
6802          left = 0,
6803          top = 0,
6804          dimensions,
6805          width = 0,
6806          height = 0,
6807          cols = this.widths.length,
6808          rows = this.heights.length;
6809  
6810      for ( y = 0; y < rows; y++ ) {
6811          height = this.heights[y];
6812          for ( x = 0; x < cols; x++ ) {
6813              panel = this.panels[i];
6814              width = this.widths[x];
6815              dimensions = {
6816                  width: Math.round( width * 100 ) + '%',
6817                  height: Math.round( height * 100 ) + '%',
6818                  top: Math.round( top * 100 ) + '%',
6819                  // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
6820                  visibility: width === 0 || height === 0 ? 'hidden' : ''
6821              };
6822              // If RTL, reverse:
6823              if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) {
6824                  dimensions.right = Math.round( left * 100 ) + '%';
6825              } else {
6826                  dimensions.left = Math.round( left * 100 ) + '%';
6827              }
6828              panel.$element.css( dimensions );
6829              i++;
6830              left += width;
6831          }
6832          top += height;
6833          left = 0;
6834      }
6835  
6836      this.emit( 'update' );
6837  };
6838  
6839  /**
6840   * Get a panel at a given position.
6841   *
6842   * The x and y position is affected by the current grid layout.
6843   *
6844   * @param {number} x Horizontal position
6845   * @param {number} y Vertical position
6846   * @return {OO.ui.PanelLayout} The panel at the given postion
6847   */
6848  OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
6849      return this.panels[( x * this.widths.length ) + y];
6850  };
6851  
6852  /**
6853   * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
6854   *
6855   * @class
6856   * @extends OO.ui.Layout
6857   *
6858   * @constructor
6859   * @param {Object} [config] Configuration options
6860   * @cfg {boolean} [scrollable=false] Allow vertical scrolling
6861   * @cfg {boolean} [padded=false] Pad the content from the edges
6862   * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
6863   */
6864  OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
6865      // Config initialization
6866      config = config || {};
6867  
6868      // Parent constructor
6869      OO.ui.PanelLayout.super.call( this, config );
6870  
6871      // Initialization
6872      this.$element.addClass( 'oo-ui-panelLayout' );
6873      if ( config.scrollable ) {
6874          this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
6875      }
6876  
6877      if ( config.padded ) {
6878          this.$element.addClass( 'oo-ui-panelLayout-padded' );
6879      }
6880  
6881      if ( config.expanded === undefined || config.expanded ) {
6882          this.$element.addClass( 'oo-ui-panelLayout-expanded' );
6883      }
6884  };
6885  
6886  /* Setup */
6887  
6888  OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
6889  
6890  /**
6891   * Page within an booklet layout.
6892   *
6893   * @class
6894   * @extends OO.ui.PanelLayout
6895   *
6896   * @constructor
6897   * @param {string} name Unique symbolic name of page
6898   * @param {Object} [config] Configuration options
6899   * @param {string} [outlineItem] Outline item widget
6900   */
6901  OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
6902      // Configuration initialization
6903      config = $.extend( { scrollable: true }, config );
6904  
6905      // Parent constructor
6906      OO.ui.PageLayout.super.call( this, config );
6907  
6908      // Properties
6909      this.name = name;
6910      this.outlineItem = config.outlineItem || null;
6911      this.active = false;
6912  
6913      // Initialization
6914      this.$element.addClass( 'oo-ui-pageLayout' );
6915  };
6916  
6917  /* Setup */
6918  
6919  OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
6920  
6921  /* Events */
6922  
6923  /**
6924   * @event active
6925   * @param {boolean} active Page is active
6926   */
6927  
6928  /* Methods */
6929  
6930  /**
6931   * Get page name.
6932   *
6933   * @return {string} Symbolic name of page
6934   */
6935  OO.ui.PageLayout.prototype.getName = function () {
6936      return this.name;
6937  };
6938  
6939  /**
6940   * Check if page is active.
6941   *
6942   * @return {boolean} Page is active
6943   */
6944  OO.ui.PageLayout.prototype.isActive = function () {
6945      return this.active;
6946  };
6947  
6948  /**
6949   * Get outline item.
6950   *
6951   * @return {OO.ui.OutlineItemWidget|null} Outline item widget
6952   */
6953  OO.ui.PageLayout.prototype.getOutlineItem = function () {
6954      return this.outlineItem;
6955  };
6956  
6957  /**
6958   * Set outline item.
6959   *
6960   * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
6961   *   outline item as desired; this method is called for setting (with an object) and unsetting
6962   *   (with null) and overriding methods would have to check the value of `outlineItem` to avoid
6963   *   operating on null instead of an OO.ui.OutlineItemWidget object.
6964   *
6965   * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear
6966   * @chainable
6967   */
6968  OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
6969      this.outlineItem = outlineItem || null;
6970      if ( outlineItem ) {
6971          this.setupOutlineItem();
6972      }
6973      return this;
6974  };
6975  
6976  /**
6977   * Setup outline item.
6978   *
6979   * @localdoc Subclasses should override this method to adjust the outline item as desired.
6980   *
6981   * @param {OO.ui.OutlineItemWidget} outlineItem Outline item widget to setup
6982   * @chainable
6983   */
6984  OO.ui.PageLayout.prototype.setupOutlineItem = function () {
6985      return this;
6986  };
6987  
6988  /**
6989   * Set page active state.
6990   *
6991   * @param {boolean} Page is active
6992   * @fires active
6993   */
6994  OO.ui.PageLayout.prototype.setActive = function ( active ) {
6995      active = !!active;
6996  
6997      if ( active !== this.active ) {
6998          this.active = active;
6999          this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
7000          this.emit( 'active', this.active );
7001      }
7002  };
7003  
7004  /**
7005   * Layout containing a series of mutually exclusive pages.
7006   *
7007   * @class
7008   * @extends OO.ui.PanelLayout
7009   * @mixins OO.ui.GroupElement
7010   *
7011   * @constructor
7012   * @param {Object} [config] Configuration options
7013   * @cfg {boolean} [continuous=false] Show all pages, one after another
7014   * @cfg {string} [icon=''] Symbolic icon name
7015   * @cfg {OO.ui.Layout[]} [items] Layouts to add
7016   */
7017  OO.ui.StackLayout = function OoUiStackLayout( config ) {
7018      // Config initialization
7019      config = $.extend( { scrollable: true }, config );
7020  
7021      // Parent constructor
7022      OO.ui.StackLayout.super.call( this, config );
7023  
7024      // Mixin constructors
7025      OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
7026  
7027      // Properties
7028      this.currentItem = null;
7029      this.continuous = !!config.continuous;
7030  
7031      // Initialization
7032      this.$element.addClass( 'oo-ui-stackLayout' );
7033      if ( this.continuous ) {
7034          this.$element.addClass( 'oo-ui-stackLayout-continuous' );
7035      }
7036      if ( $.isArray( config.items ) ) {
7037          this.addItems( config.items );
7038      }
7039  };
7040  
7041  /* Setup */
7042  
7043  OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
7044  OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
7045  
7046  /* Events */
7047  
7048  /**
7049   * @event set
7050   * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
7051   */
7052  
7053  /* Methods */
7054  
7055  /**
7056   * Get the current item.
7057   *
7058   * @return {OO.ui.Layout|null}
7059   */
7060  OO.ui.StackLayout.prototype.getCurrentItem = function () {
7061      return this.currentItem;
7062  };
7063  
7064  /**
7065   * Unset the current item.
7066   *
7067   * @private
7068   * @param {OO.ui.StackLayout} layout
7069   * @fires set
7070   */
7071  OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
7072      var prevItem = this.currentItem;
7073      if ( prevItem === null ) {
7074          return;
7075      }
7076  
7077      this.currentItem = null;
7078      this.emit( 'set', null );
7079  };
7080  
7081  /**
7082   * Add items.
7083   *
7084   * Adding an existing item (by value) will move it.
7085   *
7086   * @param {OO.ui.Layout[]} items Items to add
7087   * @param {number} [index] Index to insert items after
7088   * @chainable
7089   */
7090  OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
7091      // Mixin method
7092      OO.ui.GroupElement.prototype.addItems.call( this, items, index );
7093  
7094      if ( !this.currentItem && items.length ) {
7095          this.setItem( items[0] );
7096      }
7097  
7098      return this;
7099  };
7100  
7101  /**
7102   * Remove items.
7103   *
7104   * Items will be detached, not removed, so they can be used later.
7105   *
7106   * @param {OO.ui.Layout[]} items Items to remove
7107   * @chainable
7108   * @fires set
7109   */
7110  OO.ui.StackLayout.prototype.removeItems = function ( items ) {
7111      // Mixin method
7112      OO.ui.GroupElement.prototype.removeItems.call( this, items );
7113  
7114      if ( $.inArray( this.currentItem, items ) !== -1 ) {
7115          if ( this.items.length ) {
7116              this.setItem( this.items[0] );
7117          } else {
7118              this.unsetCurrentItem();
7119          }
7120      }
7121  
7122      return this;
7123  };
7124  
7125  /**
7126   * Clear all items.
7127   *
7128   * Items will be detached, not removed, so they can be used later.
7129   *
7130   * @chainable
7131   * @fires set
7132   */
7133  OO.ui.StackLayout.prototype.clearItems = function () {
7134      this.unsetCurrentItem();
7135      OO.ui.GroupElement.prototype.clearItems.call( this );
7136  
7137      return this;
7138  };
7139  
7140  /**
7141   * Show item.
7142   *
7143   * Any currently shown item will be hidden.
7144   *
7145   * FIXME: If the passed item to show has not been added in the items list, then
7146   * this method drops it and unsets the current item.
7147   *
7148   * @param {OO.ui.Layout} item Item to show
7149   * @chainable
7150   * @fires set
7151   */
7152  OO.ui.StackLayout.prototype.setItem = function ( item ) {
7153      var i, len;
7154  
7155      if ( item !== this.currentItem ) {
7156          if ( !this.continuous ) {
7157              for ( i = 0, len = this.items.length; i < len; i++ ) {
7158                  this.items[i].$element.css( 'display', '' );
7159              }
7160          }
7161          if ( $.inArray( item, this.items ) !== -1 ) {
7162              if ( !this.continuous ) {
7163                  item.$element.css( 'display', 'block' );
7164              }
7165              this.currentItem = item;
7166              this.emit( 'set', item );
7167          } else {
7168              this.unsetCurrentItem();
7169          }
7170      }
7171  
7172      return this;
7173  };
7174  
7175  /**
7176   * Horizontal bar layout of tools as icon buttons.
7177   *
7178   * @class
7179   * @extends OO.ui.ToolGroup
7180   *
7181   * @constructor
7182   * @param {OO.ui.Toolbar} toolbar
7183   * @param {Object} [config] Configuration options
7184   */
7185  OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
7186      // Parent constructor
7187      OO.ui.BarToolGroup.super.call( this, toolbar, config );
7188  
7189      // Initialization
7190      this.$element.addClass( 'oo-ui-barToolGroup' );
7191  };
7192  
7193  /* Setup */
7194  
7195  OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
7196  
7197  /* Static Properties */
7198  
7199  OO.ui.BarToolGroup.static.titleTooltips = true;
7200  
7201  OO.ui.BarToolGroup.static.accelTooltips = true;
7202  
7203  OO.ui.BarToolGroup.static.name = 'bar';
7204  
7205  /**
7206   * Popup list of tools with an icon and optional label.
7207   *
7208   * @abstract
7209   * @class
7210   * @extends OO.ui.ToolGroup
7211   * @mixins OO.ui.IconElement
7212   * @mixins OO.ui.IndicatorElement
7213   * @mixins OO.ui.LabelElement
7214   * @mixins OO.ui.TitledElement
7215   * @mixins OO.ui.ClippableElement
7216   *
7217   * @constructor
7218   * @param {OO.ui.Toolbar} toolbar
7219   * @param {Object} [config] Configuration options
7220   * @cfg {string} [header] Text to display at the top of the pop-up
7221   */
7222  OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
7223      // Configuration initialization
7224      config = config || {};
7225  
7226      // Parent constructor
7227      OO.ui.PopupToolGroup.super.call( this, toolbar, config );
7228  
7229      // Mixin constructors
7230      OO.ui.IconElement.call( this, config );
7231      OO.ui.IndicatorElement.call( this, config );
7232      OO.ui.LabelElement.call( this, config );
7233      OO.ui.TitledElement.call( this, config );
7234      OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
7235  
7236      // Properties
7237      this.active = false;
7238      this.dragging = false;
7239      this.onBlurHandler = OO.ui.bind( this.onBlur, this );
7240      this.$handle = this.$( '<span>' );
7241  
7242      // Events
7243      this.$handle.on( {
7244          'mousedown touchstart': OO.ui.bind( this.onHandlePointerDown, this ),
7245          'mouseup touchend': OO.ui.bind( this.onHandlePointerUp, this )
7246      } );
7247  
7248      // Initialization
7249      this.$handle
7250          .addClass( 'oo-ui-popupToolGroup-handle' )
7251          .append( this.$icon, this.$label, this.$indicator );
7252      // If the pop-up should have a header, add it to the top of the toolGroup.
7253      // Note: If this feature is useful for other widgets, we could abstract it into an
7254      // OO.ui.HeaderedElement mixin constructor.
7255      if ( config.header !== undefined ) {
7256          this.$group
7257              .prepend( this.$( '<span>' )
7258                  .addClass( 'oo-ui-popupToolGroup-header' )
7259                  .text( config.header )
7260              );
7261      }
7262      this.$element
7263          .addClass( 'oo-ui-popupToolGroup' )
7264          .prepend( this.$handle );
7265  };
7266  
7267  /* Setup */
7268  
7269  OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
7270  OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement );
7271  OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement );
7272  OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement );
7273  OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
7274  OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
7275  
7276  /* Static Properties */
7277  
7278  /* Methods */
7279  
7280  /**
7281   * @inheritdoc
7282   */
7283  OO.ui.PopupToolGroup.prototype.setDisabled = function () {
7284      // Parent method
7285      OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
7286  
7287      if ( this.isDisabled() && this.isElementAttached() ) {
7288          this.setActive( false );
7289      }
7290  };
7291  
7292  /**
7293   * Handle focus being lost.
7294   *
7295   * The event is actually generated from a mouseup, so it is not a normal blur event object.
7296   *
7297   * @param {jQuery.Event} e Mouse up event
7298   */
7299  OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
7300      // Only deactivate when clicking outside the dropdown element
7301      if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) {
7302          this.setActive( false );
7303      }
7304  };
7305  
7306  /**
7307   * @inheritdoc
7308   */
7309  OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) {
7310      // e.which is 0 for touch events, 1 for left mouse button
7311      if ( !this.isDisabled() && e.which <= 1 ) {
7312          this.setActive( false );
7313      }
7314      return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e );
7315  };
7316  
7317  /**
7318   * Handle mouse up events.
7319   *
7320   * @param {jQuery.Event} e Mouse up event
7321   */
7322  OO.ui.PopupToolGroup.prototype.onHandlePointerUp = function () {
7323      return false;
7324  };
7325  
7326  /**
7327   * Handle mouse down events.
7328   *
7329   * @param {jQuery.Event} e Mouse down event
7330   */
7331  OO.ui.PopupToolGroup.prototype.onHandlePointerDown = function ( e ) {
7332      // e.which is 0 for touch events, 1 for left mouse button
7333      if ( !this.isDisabled() && e.which <= 1 ) {
7334          this.setActive( !this.active );
7335      }
7336      return false;
7337  };
7338  
7339  /**
7340   * Switch into active mode.
7341   *
7342   * When active, mouseup events anywhere in the document will trigger deactivation.
7343   */
7344  OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
7345      value = !!value;
7346      if ( this.active !== value ) {
7347          this.active = value;
7348          if ( value ) {
7349              this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
7350  
7351              // Try anchoring the popup to the left first
7352              this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
7353              this.toggleClipping( true );
7354              if ( this.isClippedHorizontally() ) {
7355                  // Anchoring to the left caused the popup to clip, so anchor it to the right instead
7356                  this.toggleClipping( false );
7357                  this.$element
7358                      .removeClass( 'oo-ui-popupToolGroup-left' )
7359                      .addClass( 'oo-ui-popupToolGroup-right' );
7360                  this.toggleClipping( true );
7361              }
7362          } else {
7363              this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
7364              this.$element.removeClass(
7365                  'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left  oo-ui-popupToolGroup-right'
7366              );
7367              this.toggleClipping( false );
7368          }
7369      }
7370  };
7371  
7372  /**
7373   * Drop down list layout of tools as labeled icon buttons.
7374   *
7375   * @class
7376   * @extends OO.ui.PopupToolGroup
7377   *
7378   * @constructor
7379   * @param {OO.ui.Toolbar} toolbar
7380   * @param {Object} [config] Configuration options
7381   */
7382  OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
7383      // Parent constructor
7384      OO.ui.ListToolGroup.super.call( this, toolbar, config );
7385  
7386      // Initialization
7387      this.$element.addClass( 'oo-ui-listToolGroup' );
7388  };
7389  
7390  /* Setup */
7391  
7392  OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
7393  
7394  /* Static Properties */
7395  
7396  OO.ui.ListToolGroup.static.accelTooltips = true;
7397  
7398  OO.ui.ListToolGroup.static.name = 'list';
7399  
7400  /**
7401   * Drop down menu layout of tools as selectable menu items.
7402   *
7403   * @class
7404   * @extends OO.ui.PopupToolGroup
7405   *
7406   * @constructor
7407   * @param {OO.ui.Toolbar} toolbar
7408   * @param {Object} [config] Configuration options
7409   */
7410  OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
7411      // Configuration initialization
7412      config = config || {};
7413  
7414      // Parent constructor
7415      OO.ui.MenuToolGroup.super.call( this, toolbar, config );
7416  
7417      // Events
7418      this.toolbar.connect( this, { updateState: 'onUpdateState' } );
7419  
7420      // Initialization
7421      this.$element.addClass( 'oo-ui-menuToolGroup' );
7422  };
7423  
7424  /* Setup */
7425  
7426  OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
7427  
7428  /* Static Properties */
7429  
7430  OO.ui.MenuToolGroup.static.accelTooltips = true;
7431  
7432  OO.ui.MenuToolGroup.static.name = 'menu';
7433  
7434  /* Methods */
7435  
7436  /**
7437   * Handle the toolbar state being updated.
7438   *
7439   * When the state changes, the title of each active item in the menu will be joined together and
7440   * used as a label for the group. The label will be empty if none of the items are active.
7441   */
7442  OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
7443      var name,
7444          labelTexts = [];
7445  
7446      for ( name in this.tools ) {
7447          if ( this.tools[name].isActive() ) {
7448              labelTexts.push( this.tools[name].getTitle() );
7449          }
7450      }
7451  
7452      this.setLabel( labelTexts.join( ', ' ) || ' ' );
7453  };
7454  
7455  /**
7456   * Tool that shows a popup when selected.
7457   *
7458   * @abstract
7459   * @class
7460   * @extends OO.ui.Tool
7461   * @mixins OO.ui.PopupElement
7462   *
7463   * @constructor
7464   * @param {OO.ui.Toolbar} toolbar
7465   * @param {Object} [config] Configuration options
7466   */
7467  OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
7468      // Parent constructor
7469      OO.ui.PopupTool.super.call( this, toolbar, config );
7470  
7471      // Mixin constructors
7472      OO.ui.PopupElement.call( this, config );
7473  
7474      // Initialization
7475      this.$element
7476          .addClass( 'oo-ui-popupTool' )
7477          .append( this.popup.$element );
7478  };
7479  
7480  /* Setup */
7481  
7482  OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
7483  OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement );
7484  
7485  /* Methods */
7486  
7487  /**
7488   * Handle the tool being selected.
7489   *
7490   * @inheritdoc
7491   */
7492  OO.ui.PopupTool.prototype.onSelect = function () {
7493      if ( !this.isDisabled() ) {
7494          this.popup.toggle();
7495      }
7496      this.setActive( false );
7497      return false;
7498  };
7499  
7500  /**
7501   * Handle the toolbar state being updated.
7502   *
7503   * @inheritdoc
7504   */
7505  OO.ui.PopupTool.prototype.onUpdateState = function () {
7506      this.setActive( false );
7507  };
7508  
7509  /**
7510   * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
7511   *
7512   * Use together with OO.ui.ItemWidget to make disabled state inheritable.
7513   *
7514   * @abstract
7515   * @class
7516   * @extends OO.ui.GroupElement
7517   *
7518   * @constructor
7519   * @param {Object} [config] Configuration options
7520   */
7521  OO.ui.GroupWidget = function OoUiGroupWidget( config ) {
7522      // Parent constructor
7523      OO.ui.GroupWidget.super.call( this, config );
7524  };
7525  
7526  /* Setup */
7527  
7528  OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
7529  
7530  /* Methods */
7531  
7532  /**
7533   * Set the disabled state of the widget.
7534   *
7535   * This will also update the disabled state of child widgets.
7536   *
7537   * @param {boolean} disabled Disable widget
7538   * @chainable
7539   */
7540  OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
7541      var i, len;
7542  
7543      // Parent method
7544      // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
7545      OO.ui.Widget.prototype.setDisabled.call( this, disabled );
7546  
7547      // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
7548      if ( this.items ) {
7549          for ( i = 0, len = this.items.length; i < len; i++ ) {
7550              this.items[i].updateDisabled();
7551          }
7552      }
7553  
7554      return this;
7555  };
7556  
7557  /**
7558   * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
7559   *
7560   * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
7561   * allows bidrectional communication.
7562   *
7563   * Use together with OO.ui.GroupWidget to make disabled state inheritable.
7564   *
7565   * @abstract
7566   * @class
7567   *
7568   * @constructor
7569   */
7570  OO.ui.ItemWidget = function OoUiItemWidget() {
7571      //
7572  };
7573  
7574  /* Methods */
7575  
7576  /**
7577   * Check if widget is disabled.
7578   *
7579   * Checks parent if present, making disabled state inheritable.
7580   *
7581   * @return {boolean} Widget is disabled
7582   */
7583  OO.ui.ItemWidget.prototype.isDisabled = function () {
7584      return this.disabled ||
7585          ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
7586  };
7587  
7588  /**
7589   * Set group element is in.
7590   *
7591   * @param {OO.ui.GroupElement|null} group Group element, null if none
7592   * @chainable
7593   */
7594  OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
7595      // Parent method
7596      // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
7597      OO.ui.Element.prototype.setElementGroup.call( this, group );
7598  
7599      // Initialize item disabled states
7600      this.updateDisabled();
7601  
7602      return this;
7603  };
7604  
7605  /**
7606   * Mixin that adds a menu showing suggested values for a text input.
7607   *
7608   * Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections.
7609   *
7610   * @class
7611   * @abstract
7612   *
7613   * @constructor
7614   * @param {OO.ui.TextInputWidget} input Input widget
7615   * @param {Object} [config] Configuration options
7616   * @cfg {jQuery} [$overlay=this.$( 'body' )] Overlay layer
7617   */
7618  OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
7619      // Config intialization
7620      config = config || {};
7621  
7622      // Properties
7623      this.lookupInput = input;
7624      this.$overlay = config.$overlay || this.$( 'body,.oo-ui-window-overlay' ).last();
7625      this.lookupMenu = new OO.ui.TextInputMenuWidget( this, {
7626          $: OO.ui.Element.getJQuery( this.$overlay ),
7627          input: this.lookupInput,
7628          $container: config.$container
7629      } );
7630      this.lookupCache = {};
7631      this.lookupQuery = null;
7632      this.lookupRequest = null;
7633      this.populating = false;
7634  
7635      // Events
7636      this.$overlay.append( this.lookupMenu.$element );
7637  
7638      this.lookupInput.$input.on( {
7639          focus: OO.ui.bind( this.onLookupInputFocus, this ),
7640          blur: OO.ui.bind( this.onLookupInputBlur, this ),
7641          mousedown: OO.ui.bind( this.onLookupInputMouseDown, this )
7642      } );
7643      this.lookupInput.connect( this, { change: 'onLookupInputChange' } );
7644  
7645      // Initialization
7646      this.$element.addClass( 'oo-ui-lookupWidget' );
7647      this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
7648  };
7649  
7650  /* Methods */
7651  
7652  /**
7653   * Handle input focus event.
7654   *
7655   * @param {jQuery.Event} e Input focus event
7656   */
7657  OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
7658      this.openLookupMenu();
7659  };
7660  
7661  /**
7662   * Handle input blur event.
7663   *
7664   * @param {jQuery.Event} e Input blur event
7665   */
7666  OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
7667      this.lookupMenu.toggle( false );
7668  };
7669  
7670  /**
7671   * Handle input mouse down event.
7672   *
7673   * @param {jQuery.Event} e Input mouse down event
7674   */
7675  OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
7676      this.openLookupMenu();
7677  };
7678  
7679  /**
7680   * Handle input change event.
7681   *
7682   * @param {string} value New input value
7683   */
7684  OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
7685      this.openLookupMenu();
7686  };
7687  
7688  /**
7689   * Get lookup menu.
7690   *
7691   * @return {OO.ui.TextInputMenuWidget}
7692   */
7693  OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
7694      return this.lookupMenu;
7695  };
7696  
7697  /**
7698   * Open the menu.
7699   *
7700   * @chainable
7701   */
7702  OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
7703      var value = this.lookupInput.getValue();
7704  
7705      if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) {
7706          this.populateLookupMenu();
7707          this.lookupMenu.toggle( true );
7708      } else {
7709          this.lookupMenu
7710              .clearItems()
7711              .toggle( false );
7712      }
7713  
7714      return this;
7715  };
7716  
7717  /**
7718   * Populate lookup menu with current information.
7719   *
7720   * @chainable
7721   */
7722  OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
7723      var widget = this;
7724  
7725      if ( !this.populating ) {
7726          this.populating = true;
7727          this.getLookupMenuItems()
7728              .done( function ( items ) {
7729                  widget.lookupMenu.clearItems();
7730                  if ( items.length ) {
7731                      widget.lookupMenu
7732                          .addItems( items )
7733                          .toggle( true );
7734                      widget.initializeLookupMenuSelection();
7735                      widget.openLookupMenu();
7736                  } else {
7737                      widget.lookupMenu.toggle( true );
7738                  }
7739                  widget.populating = false;
7740              } )
7741              .fail( function () {
7742                  widget.lookupMenu.clearItems();
7743                  widget.populating = false;
7744              } );
7745      }
7746  
7747      return this;
7748  };
7749  
7750  /**
7751   * Set selection in the lookup menu with current information.
7752   *
7753   * @chainable
7754   */
7755  OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
7756      if ( !this.lookupMenu.getSelectedItem() ) {
7757          this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
7758      }
7759      this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
7760  };
7761  
7762  /**
7763   * Get lookup menu items for the current query.
7764   *
7765   * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
7766   * of the done event
7767   */
7768  OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
7769      var widget = this,
7770          value = this.lookupInput.getValue(),
7771          deferred = $.Deferred();
7772  
7773      if ( value && value !== this.lookupQuery ) {
7774          // Abort current request if query has changed
7775          if ( this.lookupRequest ) {
7776              this.lookupRequest.abort();
7777              this.lookupQuery = null;
7778              this.lookupRequest = null;
7779          }
7780          if ( value in this.lookupCache ) {
7781              deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
7782          } else {
7783              this.lookupQuery = value;
7784              this.lookupRequest = this.getLookupRequest()
7785                  .always( function () {
7786                      widget.lookupQuery = null;
7787                      widget.lookupRequest = null;
7788                  } )
7789                  .done( function ( data ) {
7790                      widget.lookupCache[value] = widget.getLookupCacheItemFromData( data );
7791                      deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) );
7792                  } )
7793                  .fail( function () {
7794                      deferred.reject();
7795                  } );
7796              this.pushPending();
7797              this.lookupRequest.always( function () {
7798                  widget.popPending();
7799              } );
7800          }
7801      }
7802      return deferred.promise();
7803  };
7804  
7805  /**
7806   * Get a new request object of the current lookup query value.
7807   *
7808   * @abstract
7809   * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method
7810   */
7811  OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
7812      // Stub, implemented in subclass
7813      return null;
7814  };
7815  
7816  /**
7817   * Handle successful lookup request.
7818   *
7819   * Overriding methods should call #populateLookupMenu when results are available and cache results
7820   * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects.
7821   *
7822   * @abstract
7823   * @param {Mixed} data Response from server
7824   */
7825  OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () {
7826      // Stub, implemented in subclass
7827  };
7828  
7829  /**
7830   * Get a list of menu item widgets from the data stored by the lookup request's done handler.
7831   *
7832   * @abstract
7833   * @param {Mixed} data Cached result data, usually an array
7834   * @return {OO.ui.MenuItemWidget[]} Menu items
7835   */
7836  OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
7837      // Stub, implemented in subclass
7838      return [];
7839  };
7840  
7841  /**
7842   * Set of controls for an OO.ui.OutlineWidget.
7843   *
7844   * Controls include moving items up and down, removing items, and adding different kinds of items.
7845   *
7846   * @class
7847   * @extends OO.ui.Widget
7848   * @mixins OO.ui.GroupElement
7849   * @mixins OO.ui.IconElement
7850   *
7851   * @constructor
7852   * @param {OO.ui.OutlineWidget} outline Outline to control
7853   * @param {Object} [config] Configuration options
7854   */
7855  OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
7856      // Configuration initialization
7857      config = $.extend( { icon: 'add-item' }, config );
7858  
7859      // Parent constructor
7860      OO.ui.OutlineControlsWidget.super.call( this, config );
7861  
7862      // Mixin constructors
7863      OO.ui.GroupElement.call( this, config );
7864      OO.ui.IconElement.call( this, config );
7865  
7866      // Properties
7867      this.outline = outline;
7868      this.$movers = this.$( '<div>' );
7869      this.upButton = new OO.ui.ButtonWidget( {
7870          $: this.$,
7871          framed: false,
7872          icon: 'collapse',
7873          title: OO.ui.msg( 'ooui-outline-control-move-up' )
7874      } );
7875      this.downButton = new OO.ui.ButtonWidget( {
7876          $: this.$,
7877          framed: false,
7878          icon: 'expand',
7879          title: OO.ui.msg( 'ooui-outline-control-move-down' )
7880      } );
7881      this.removeButton = new OO.ui.ButtonWidget( {
7882          $: this.$,
7883          framed: false,
7884          icon: 'remove',
7885          title: OO.ui.msg( 'ooui-outline-control-remove' )
7886      } );
7887  
7888      // Events
7889      outline.connect( this, {
7890          select: 'onOutlineChange',
7891          add: 'onOutlineChange',
7892          remove: 'onOutlineChange'
7893      } );
7894      this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
7895      this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
7896      this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
7897  
7898      // Initialization
7899      this.$element.addClass( 'oo-ui-outlineControlsWidget' );
7900      this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
7901      this.$movers
7902          .addClass( 'oo-ui-outlineControlsWidget-movers' )
7903          .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
7904      this.$element.append( this.$icon, this.$group, this.$movers );
7905  };
7906  
7907  /* Setup */
7908  
7909  OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
7910  OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
7911  OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement );
7912  
7913  /* Events */
7914  
7915  /**
7916   * @event move
7917   * @param {number} places Number of places to move
7918   */
7919  
7920  /**
7921   * @event remove
7922   */
7923  
7924  /* Methods */
7925  
7926  /**
7927   * Handle outline change events.
7928   */
7929  OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
7930      var i, len, firstMovable, lastMovable,
7931          items = this.outline.getItems(),
7932          selectedItem = this.outline.getSelectedItem(),
7933          movable = selectedItem && selectedItem.isMovable(),
7934          removable = selectedItem && selectedItem.isRemovable();
7935  
7936      if ( movable ) {
7937          i = -1;
7938          len = items.length;
7939          while ( ++i < len ) {
7940              if ( items[i].isMovable() ) {
7941                  firstMovable = items[i];
7942                  break;
7943              }
7944          }
7945          i = len;
7946          while ( i-- ) {
7947              if ( items[i].isMovable() ) {
7948                  lastMovable = items[i];
7949                  break;
7950              }
7951          }
7952      }
7953      this.upButton.setDisabled( !movable || selectedItem === firstMovable );
7954      this.downButton.setDisabled( !movable || selectedItem === lastMovable );
7955      this.removeButton.setDisabled( !removable );
7956  };
7957  
7958  /**
7959   * Mixin for widgets with a boolean on/off state.
7960   *
7961   * @abstract
7962   * @class
7963   *
7964   * @constructor
7965   * @param {Object} [config] Configuration options
7966   * @cfg {boolean} [value=false] Initial value
7967   */
7968  OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
7969      // Configuration initialization
7970      config = config || {};
7971  
7972      // Properties
7973      this.value = null;
7974  
7975      // Initialization
7976      this.$element.addClass( 'oo-ui-toggleWidget' );
7977      this.setValue( !!config.value );
7978  };
7979  
7980  /* Events */
7981  
7982  /**
7983   * @event change
7984   * @param {boolean} value Changed value
7985   */
7986  
7987  /* Methods */
7988  
7989  /**
7990   * Get the value of the toggle.
7991   *
7992   * @return {boolean}
7993   */
7994  OO.ui.ToggleWidget.prototype.getValue = function () {
7995      return this.value;
7996  };
7997  
7998  /**
7999   * Set the value of the toggle.
8000   *
8001   * @param {boolean} value New value
8002   * @fires change
8003   * @chainable
8004   */
8005  OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
8006      value = !!value;
8007      if ( this.value !== value ) {
8008          this.value = value;
8009          this.emit( 'change', value );
8010          this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
8011          this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
8012      }
8013      return this;
8014  };
8015  
8016  /**
8017   * Group widget for multiple related buttons.
8018   *
8019   * Use together with OO.ui.ButtonWidget.
8020   *
8021   * @class
8022   * @extends OO.ui.Widget
8023   * @mixins OO.ui.GroupElement
8024   *
8025   * @constructor
8026   * @param {Object} [config] Configuration options
8027   * @cfg {OO.ui.ButtonWidget} [items] Buttons to add
8028   */
8029  OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
8030      // Parent constructor
8031      OO.ui.ButtonGroupWidget.super.call( this, config );
8032  
8033      // Mixin constructors
8034      OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8035  
8036      // Initialization
8037      this.$element.addClass( 'oo-ui-buttonGroupWidget' );
8038      if ( $.isArray( config.items ) ) {
8039          this.addItems( config.items );
8040      }
8041  };
8042  
8043  /* Setup */
8044  
8045  OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
8046  OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
8047  
8048  /**
8049   * Generic widget for buttons.
8050   *
8051   * @class
8052   * @extends OO.ui.Widget
8053   * @mixins OO.ui.ButtonElement
8054   * @mixins OO.ui.IconElement
8055   * @mixins OO.ui.IndicatorElement
8056   * @mixins OO.ui.LabelElement
8057   * @mixins OO.ui.TitledElement
8058   * @mixins OO.ui.FlaggedElement
8059   *
8060   * @constructor
8061   * @param {Object} [config] Configuration options
8062   * @cfg {string} [href] Hyperlink to visit when clicked
8063   * @cfg {string} [target] Target to open hyperlink in
8064   */
8065  OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
8066      // Configuration initialization
8067      config = $.extend( { target: '_blank' }, config );
8068  
8069      // Parent constructor
8070      OO.ui.ButtonWidget.super.call( this, config );
8071  
8072      // Mixin constructors
8073      OO.ui.ButtonElement.call( this, config );
8074      OO.ui.IconElement.call( this, config );
8075      OO.ui.IndicatorElement.call( this, config );
8076      OO.ui.LabelElement.call( this, config );
8077      OO.ui.TitledElement.call( this, config, $.extend( {}, config, { $titled: this.$button } ) );
8078      OO.ui.FlaggedElement.call( this, config );
8079  
8080      // Properties
8081      this.href = null;
8082      this.target = null;
8083      this.isHyperlink = false;
8084  
8085      // Events
8086      this.$button.on( {
8087          click: OO.ui.bind( this.onClick, this ),
8088          keypress: OO.ui.bind( this.onKeyPress, this )
8089      } );
8090  
8091      // Initialization
8092      this.$button.append( this.$icon, this.$label, this.$indicator );
8093      this.$element
8094          .addClass( 'oo-ui-buttonWidget' )
8095          .append( this.$button );
8096      this.setHref( config.href );
8097      this.setTarget( config.target );
8098  };
8099  
8100  /* Setup */
8101  
8102  OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
8103  OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement );
8104  OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement );
8105  OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement );
8106  OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
8107  OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
8108  OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
8109  
8110  /* Events */
8111  
8112  /**
8113   * @event click
8114   */
8115  
8116  /* Methods */
8117  
8118  /**
8119   * Handles mouse click events.
8120   *
8121   * @param {jQuery.Event} e Mouse click event
8122   * @fires click
8123   */
8124  OO.ui.ButtonWidget.prototype.onClick = function () {
8125      if ( !this.isDisabled() ) {
8126          this.emit( 'click' );
8127          if ( this.isHyperlink ) {
8128              return true;
8129          }
8130      }
8131      return false;
8132  };
8133  
8134  /**
8135   * Handles keypress events.
8136   *
8137   * @param {jQuery.Event} e Keypress event
8138   * @fires click
8139   */
8140  OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
8141      if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
8142          this.onClick();
8143          if ( this.isHyperlink ) {
8144              return true;
8145          }
8146      }
8147      return false;
8148  };
8149  
8150  /**
8151   * Get hyperlink location.
8152   *
8153   * @return {string} Hyperlink location
8154   */
8155  OO.ui.ButtonWidget.prototype.getHref = function () {
8156      return this.href;
8157  };
8158  
8159  /**
8160   * Get hyperlink target.
8161   *
8162   * @return {string} Hyperlink target
8163   */
8164  OO.ui.ButtonWidget.prototype.getTarget = function () {
8165      return this.target;
8166  };
8167  
8168  /**
8169   * Set hyperlink location.
8170   *
8171   * @param {string|null} href Hyperlink location, null to remove
8172   */
8173  OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
8174      href = typeof href === 'string' ? href : null;
8175  
8176      if ( href !== this.href ) {
8177          this.href = href;
8178          if ( href !== null ) {
8179              this.$button.attr( 'href', href );
8180              this.isHyperlink = true;
8181          } else {
8182              this.$button.removeAttr( 'href' );
8183              this.isHyperlink = false;
8184          }
8185      }
8186  
8187      return this;
8188  };
8189  
8190  /**
8191   * Set hyperlink target.
8192   *
8193   * @param {string|null} target Hyperlink target, null to remove
8194   */
8195  OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
8196      target = typeof target === 'string' ? target : null;
8197  
8198      if ( target !== this.target ) {
8199          this.target = target;
8200          if ( target !== null ) {
8201              this.$button.attr( 'target', target );
8202          } else {
8203              this.$button.removeAttr( 'target' );
8204          }
8205      }
8206  
8207      return this;
8208  };
8209  
8210  /**
8211   * Button widget that executes an action and is managed by an OO.ui.ActionSet.
8212   *
8213   * @class
8214   * @extends OO.ui.ButtonWidget
8215   * @mixins OO.ui.PendingElement
8216   *
8217   * @constructor
8218   * @param {Object} [config] Configuration options
8219   * @cfg {string} [action] Symbolic action name
8220   * @cfg {string[]} [modes] Symbolic mode names
8221   */
8222  OO.ui.ActionWidget = function OoUiActionWidget( config ) {
8223      // Config intialization
8224      config = $.extend( { framed: false }, config );
8225  
8226      // Parent constructor
8227      OO.ui.ActionWidget.super.call( this, config );
8228  
8229      // Mixin constructors
8230      OO.ui.PendingElement.call( this, config );
8231  
8232      // Properties
8233      this.action = config.action || '';
8234      this.modes = config.modes || [];
8235      this.width = 0;
8236      this.height = 0;
8237  
8238      // Initialization
8239      this.$element.addClass( 'oo-ui-actionWidget' );
8240  };
8241  
8242  /* Setup */
8243  
8244  OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
8245  OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement );
8246  
8247  /* Events */
8248  
8249  /**
8250   * @event resize
8251   */
8252  
8253  /* Methods */
8254  
8255  /**
8256   * Check if action is available in a certain mode.
8257   *
8258   * @param {string} mode Name of mode
8259   * @return {boolean} Has mode
8260   */
8261  OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
8262      return this.modes.indexOf( mode ) !== -1;
8263  };
8264  
8265  /**
8266   * Get symbolic action name.
8267   *
8268   * @return {string}
8269   */
8270  OO.ui.ActionWidget.prototype.getAction = function () {
8271      return this.action;
8272  };
8273  
8274  /**
8275   * Get symbolic action name.
8276   *
8277   * @return {string}
8278   */
8279  OO.ui.ActionWidget.prototype.getModes = function () {
8280      return this.modes.slice();
8281  };
8282  
8283  /**
8284   * Emit a resize event if the size has changed.
8285   *
8286   * @chainable
8287   */
8288  OO.ui.ActionWidget.prototype.propagateResize = function () {
8289      var width, height;
8290  
8291      if ( this.isElementAttached() ) {
8292          width = this.$element.width();
8293          height = this.$element.height();
8294  
8295          if ( width !== this.width || height !== this.height ) {
8296              this.width = width;
8297              this.height = height;
8298              this.emit( 'resize' );
8299          }
8300      }
8301  
8302      return this;
8303  };
8304  
8305  /**
8306   * @inheritdoc
8307   */
8308  OO.ui.ActionWidget.prototype.setIcon = function () {
8309      // Mixin method
8310      OO.ui.IconElement.prototype.setIcon.apply( this, arguments );
8311      this.propagateResize();
8312  
8313      return this;
8314  };
8315  
8316  /**
8317   * @inheritdoc
8318   */
8319  OO.ui.ActionWidget.prototype.setLabel = function () {
8320      // Mixin method
8321      OO.ui.LabelElement.prototype.setLabel.apply( this, arguments );
8322      this.propagateResize();
8323  
8324      return this;
8325  };
8326  
8327  /**
8328   * @inheritdoc
8329   */
8330  OO.ui.ActionWidget.prototype.setFlags = function () {
8331      // Mixin method
8332      OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments );
8333      this.propagateResize();
8334  
8335      return this;
8336  };
8337  
8338  /**
8339   * @inheritdoc
8340   */
8341  OO.ui.ActionWidget.prototype.clearFlags = function () {
8342      // Mixin method
8343      OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments );
8344      this.propagateResize();
8345  
8346      return this;
8347  };
8348  
8349  /**
8350   * Toggle visibility of button.
8351   *
8352   * @param {boolean} [show] Show button, omit to toggle visibility
8353   * @chainable
8354   */
8355  OO.ui.ActionWidget.prototype.toggle = function () {
8356      // Parent method
8357      OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
8358      this.propagateResize();
8359  
8360      return this;
8361  };
8362  
8363  /**
8364   * Button that shows and hides a popup.
8365   *
8366   * @class
8367   * @extends OO.ui.ButtonWidget
8368   * @mixins OO.ui.PopupElement
8369   *
8370   * @constructor
8371   * @param {Object} [config] Configuration options
8372   */
8373  OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
8374      // Parent constructor
8375      OO.ui.PopupButtonWidget.super.call( this, config );
8376  
8377      // Mixin constructors
8378      OO.ui.PopupElement.call( this, config );
8379  
8380      // Initialization
8381      this.$element
8382          .addClass( 'oo-ui-popupButtonWidget' )
8383          .append( this.popup.$element );
8384  };
8385  
8386  /* Setup */
8387  
8388  OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
8389  OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
8390  
8391  /* Methods */
8392  
8393  /**
8394   * Handles mouse click events.
8395   *
8396   * @param {jQuery.Event} e Mouse click event
8397   */
8398  OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
8399      // Skip clicks within the popup
8400      if ( $.contains( this.popup.$element[0], e.target ) ) {
8401          return;
8402      }
8403  
8404      if ( !this.isDisabled() ) {
8405          this.popup.toggle();
8406          // Parent method
8407          OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
8408      }
8409      return false;
8410  };
8411  
8412  /**
8413   * Button that toggles on and off.
8414   *
8415   * @class
8416   * @extends OO.ui.ButtonWidget
8417   * @mixins OO.ui.ToggleWidget
8418   *
8419   * @constructor
8420   * @param {Object} [config] Configuration options
8421   * @cfg {boolean} [value=false] Initial value
8422   */
8423  OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
8424      // Configuration initialization
8425      config = config || {};
8426  
8427      // Parent constructor
8428      OO.ui.ToggleButtonWidget.super.call( this, config );
8429  
8430      // Mixin constructors
8431      OO.ui.ToggleWidget.call( this, config );
8432  
8433      // Initialization
8434      this.$element.addClass( 'oo-ui-toggleButtonWidget' );
8435  };
8436  
8437  /* Setup */
8438  
8439  OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
8440  OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
8441  
8442  /* Methods */
8443  
8444  /**
8445   * @inheritdoc
8446   */
8447  OO.ui.ToggleButtonWidget.prototype.onClick = function () {
8448      if ( !this.isDisabled() ) {
8449          this.setValue( !this.value );
8450      }
8451  
8452      // Parent method
8453      return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
8454  };
8455  
8456  /**
8457   * @inheritdoc
8458   */
8459  OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
8460      value = !!value;
8461      if ( value !== this.value ) {
8462          this.setActive( value );
8463      }
8464  
8465      // Parent method (from mixin)
8466      OO.ui.ToggleWidget.prototype.setValue.call( this, value );
8467  
8468      return this;
8469  };
8470  
8471  /**
8472   * Icon widget.
8473   *
8474   * See OO.ui.IconElement for more information.
8475   *
8476   * @class
8477   * @extends OO.ui.Widget
8478   * @mixins OO.ui.IconElement
8479   * @mixins OO.ui.TitledElement
8480   *
8481   * @constructor
8482   * @param {Object} [config] Configuration options
8483   */
8484  OO.ui.IconWidget = function OoUiIconWidget( config ) {
8485      // Config intialization
8486      config = config || {};
8487  
8488      // Parent constructor
8489      OO.ui.IconWidget.super.call( this, config );
8490  
8491      // Mixin constructors
8492      OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
8493      OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
8494  
8495      // Initialization
8496      this.$element.addClass( 'oo-ui-iconWidget' );
8497  };
8498  
8499  /* Setup */
8500  
8501  OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
8502  OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
8503  OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
8504  
8505  /* Static Properties */
8506  
8507  OO.ui.IconWidget.static.tagName = 'span';
8508  
8509  /**
8510   * Indicator widget.
8511   *
8512   * See OO.ui.IndicatorElement for more information.
8513   *
8514   * @class
8515   * @extends OO.ui.Widget
8516   * @mixins OO.ui.IndicatorElement
8517   * @mixins OO.ui.TitledElement
8518   *
8519   * @constructor
8520   * @param {Object} [config] Configuration options
8521   */
8522  OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
8523      // Config intialization
8524      config = config || {};
8525  
8526      // Parent constructor
8527      OO.ui.IndicatorWidget.super.call( this, config );
8528  
8529      // Mixin constructors
8530      OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
8531      OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
8532  
8533      // Initialization
8534      this.$element.addClass( 'oo-ui-indicatorWidget' );
8535  };
8536  
8537  /* Setup */
8538  
8539  OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
8540  OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
8541  OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
8542  
8543  /* Static Properties */
8544  
8545  OO.ui.IndicatorWidget.static.tagName = 'span';
8546  
8547  /**
8548   * Inline menu of options.
8549   *
8550   * Inline menus provide a control for accessing a menu and compose a menu within the widget, which
8551   * can be accessed using the #getMenu method.
8552   *
8553   * Use with OO.ui.MenuOptionWidget.
8554   *
8555   * @class
8556   * @extends OO.ui.Widget
8557   * @mixins OO.ui.IconElement
8558   * @mixins OO.ui.IndicatorElement
8559   * @mixins OO.ui.LabelElement
8560   * @mixins OO.ui.TitledElement
8561   *
8562   * @constructor
8563   * @param {Object} [config] Configuration options
8564   * @cfg {Object} [menu] Configuration options to pass to menu widget
8565   */
8566  OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) {
8567      // Configuration initialization
8568      config = $.extend( { indicator: 'down' }, config );
8569  
8570      // Parent constructor
8571      OO.ui.InlineMenuWidget.super.call( this, config );
8572  
8573      // Mixin constructors
8574      OO.ui.IconElement.call( this, config );
8575      OO.ui.IndicatorElement.call( this, config );
8576      OO.ui.LabelElement.call( this, config );
8577      OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
8578  
8579      // Properties
8580      this.menu = new OO.ui.MenuWidget( $.extend( { $: this.$, widget: this }, config.menu ) );
8581      this.$handle = this.$( '<span>' );
8582  
8583      // Events
8584      this.$element.on( { click: OO.ui.bind( this.onClick, this ) } );
8585      this.menu.connect( this, { select: 'onMenuSelect' } );
8586  
8587      // Initialization
8588      this.$handle
8589          .addClass( 'oo-ui-inlineMenuWidget-handle' )
8590          .append( this.$icon, this.$label, this.$indicator );
8591      this.$element
8592          .addClass( 'oo-ui-inlineMenuWidget' )
8593          .append( this.$handle, this.menu.$element );
8594  };
8595  
8596  /* Setup */
8597  
8598  OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget );
8599  OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconElement );
8600  OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatorElement );
8601  OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabelElement );
8602  OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement );
8603  
8604  /* Methods */
8605  
8606  /**
8607   * Get the menu.
8608   *
8609   * @return {OO.ui.MenuWidget} Menu of widget
8610   */
8611  OO.ui.InlineMenuWidget.prototype.getMenu = function () {
8612      return this.menu;
8613  };
8614  
8615  /**
8616   * Handles menu select events.
8617   *
8618   * @param {OO.ui.MenuItemWidget} item Selected menu item
8619   */
8620  OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) {
8621      var selectedLabel;
8622  
8623      if ( !item ) {
8624          return;
8625      }
8626  
8627      selectedLabel = item.getLabel();
8628  
8629      // If the label is a DOM element, clone it, because setLabel will append() it
8630      if ( selectedLabel instanceof jQuery ) {
8631          selectedLabel = selectedLabel.clone();
8632      }
8633  
8634      this.setLabel( selectedLabel );
8635  };
8636  
8637  /**
8638   * Handles mouse click events.
8639   *
8640   * @param {jQuery.Event} e Mouse click event
8641   */
8642  OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
8643      // Skip clicks within the menu
8644      if ( $.contains( this.menu.$element[0], e.target ) ) {
8645          return;
8646      }
8647  
8648      if ( !this.isDisabled() ) {
8649          if ( this.menu.isVisible() ) {
8650              this.menu.toggle( false );
8651          } else {
8652              this.menu.toggle( true );
8653          }
8654      }
8655      return false;
8656  };
8657  
8658  /**
8659   * Base class for input widgets.
8660   *
8661   * @abstract
8662   * @class
8663   * @extends OO.ui.Widget
8664   *
8665   * @constructor
8666   * @param {Object} [config] Configuration options
8667   * @cfg {string} [name=''] HTML input name
8668   * @cfg {string} [value=''] Input value
8669   * @cfg {boolean} [readOnly=false] Prevent changes
8670   * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
8671   */
8672  OO.ui.InputWidget = function OoUiInputWidget( config ) {
8673      // Config intialization
8674      config = $.extend( { readOnly: false }, config );
8675  
8676      // Parent constructor
8677      OO.ui.InputWidget.super.call( this, config );
8678  
8679      // Properties
8680      this.$input = this.getInputElement( config );
8681      this.value = '';
8682      this.readOnly = false;
8683      this.inputFilter = config.inputFilter;
8684  
8685      // Events
8686      this.$input.on( 'keydown mouseup cut paste change input select', OO.ui.bind( this.onEdit, this ) );
8687  
8688      // Initialization
8689      this.$input
8690          .attr( 'name', config.name )
8691          .prop( 'disabled', this.isDisabled() );
8692      this.setReadOnly( config.readOnly );
8693      this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input );
8694      this.setValue( config.value );
8695  };
8696  
8697  /* Setup */
8698  
8699  OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
8700  
8701  /* Events */
8702  
8703  /**
8704   * @event change
8705   * @param value
8706   */
8707  
8708  /* Methods */
8709  
8710  /**
8711   * Get input element.
8712   *
8713   * @param {Object} [config] Configuration options
8714   * @return {jQuery} Input element
8715   */
8716  OO.ui.InputWidget.prototype.getInputElement = function () {
8717      return this.$( '<input>' );
8718  };
8719  
8720  /**
8721   * Handle potentially value-changing events.
8722   *
8723   * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
8724   */
8725  OO.ui.InputWidget.prototype.onEdit = function () {
8726      var widget = this;
8727      if ( !this.isDisabled() ) {
8728          // Allow the stack to clear so the value will be updated
8729          setTimeout( function () {
8730              widget.setValue( widget.$input.val() );
8731          } );
8732      }
8733  };
8734  
8735  /**
8736   * Get the value of the input.
8737   *
8738   * @return {string} Input value
8739   */
8740  OO.ui.InputWidget.prototype.getValue = function () {
8741      return this.value;
8742  };
8743  
8744  /**
8745   * Sets the direction of the current input, either RTL or LTR
8746   *
8747   * @param {boolean} isRTL
8748   */
8749  OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
8750      if ( isRTL ) {
8751          this.$input.removeClass( 'oo-ui-ltr' );
8752          this.$input.addClass( 'oo-ui-rtl' );
8753      } else {
8754          this.$input.removeClass( 'oo-ui-rtl' );
8755          this.$input.addClass( 'oo-ui-ltr' );
8756      }
8757  };
8758  
8759  /**
8760   * Set the value of the input.
8761   *
8762   * @param {string} value New value
8763   * @fires change
8764   * @chainable
8765   */
8766  OO.ui.InputWidget.prototype.setValue = function ( value ) {
8767      value = this.sanitizeValue( value );
8768      if ( this.value !== value ) {
8769          this.value = value;
8770          this.emit( 'change', this.value );
8771      }
8772      // Update the DOM if it has changed. Note that with sanitizeValue, it
8773      // is possible for the DOM value to change without this.value changing.
8774      if ( this.$input.val() !== this.value ) {
8775          this.$input.val( this.value );
8776      }
8777      return this;
8778  };
8779  
8780  /**
8781   * Sanitize incoming value.
8782   *
8783   * Ensures value is a string, and converts undefined and null to empty strings.
8784   *
8785   * @param {string} value Original value
8786   * @return {string} Sanitized value
8787   */
8788  OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) {
8789      if ( value === undefined || value === null ) {
8790          return '';
8791      } else if ( this.inputFilter ) {
8792          return this.inputFilter( String( value ) );
8793      } else {
8794          return String( value );
8795      }
8796  };
8797  
8798  /**
8799   * Simulate the behavior of clicking on a label bound to this input.
8800   */
8801  OO.ui.InputWidget.prototype.simulateLabelClick = function () {
8802      if ( !this.isDisabled() ) {
8803          if ( this.$input.is( ':checkbox,:radio' ) ) {
8804              this.$input.click();
8805          } else if ( this.$input.is( ':input' ) ) {
8806              this.$input[0].focus();
8807          }
8808      }
8809  };
8810  
8811  /**
8812   * Check if the widget is read-only.
8813   *
8814   * @return {boolean}
8815   */
8816  OO.ui.InputWidget.prototype.isReadOnly = function () {
8817      return this.readOnly;
8818  };
8819  
8820  /**
8821   * Set the read-only state of the widget.
8822   *
8823   * This should probably change the widgets's appearance and prevent it from being used.
8824   *
8825   * @param {boolean} state Make input read-only
8826   * @chainable
8827   */
8828  OO.ui.InputWidget.prototype.setReadOnly = function ( state ) {
8829      this.readOnly = !!state;
8830      this.$input.prop( 'readOnly', this.readOnly );
8831      return this;
8832  };
8833  
8834  /**
8835   * @inheritdoc
8836   */
8837  OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
8838      OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
8839      if ( this.$input ) {
8840          this.$input.prop( 'disabled', this.isDisabled() );
8841      }
8842      return this;
8843  };
8844  
8845  /**
8846   * Focus the input.
8847   *
8848   * @chainable
8849   */
8850  OO.ui.InputWidget.prototype.focus = function () {
8851      this.$input[0].focus();
8852      return this;
8853  };
8854  
8855  /**
8856   * Blur the input.
8857   *
8858   * @chainable
8859   */
8860  OO.ui.InputWidget.prototype.blur = function () {
8861      this.$input[0].blur();
8862      return this;
8863  };
8864  
8865  /**
8866   * Checkbox input widget.
8867   *
8868   * @class
8869   * @extends OO.ui.InputWidget
8870   *
8871   * @constructor
8872   * @param {Object} [config] Configuration options
8873   */
8874  OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
8875      // Parent constructor
8876      OO.ui.CheckboxInputWidget.super.call( this, config );
8877  
8878      // Initialization
8879      this.$element.addClass( 'oo-ui-checkboxInputWidget' );
8880  };
8881  
8882  /* Setup */
8883  
8884  OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
8885  
8886  /* Events */
8887  
8888  /* Methods */
8889  
8890  /**
8891   * Get input element.
8892   *
8893   * @return {jQuery} Input element
8894   */
8895  OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
8896      return this.$( '<input type="checkbox" />' );
8897  };
8898  
8899  /**
8900   * Get checked state of the checkbox
8901   *
8902   * @return {boolean} If the checkbox is checked
8903   */
8904  OO.ui.CheckboxInputWidget.prototype.getValue = function () {
8905      return this.value;
8906  };
8907  
8908  /**
8909   * Set value
8910   */
8911  OO.ui.CheckboxInputWidget.prototype.setValue = function ( value ) {
8912      value = !!value;
8913      if ( this.value !== value ) {
8914          this.value = value;
8915          this.$input.prop( 'checked', this.value );
8916          this.emit( 'change', this.value );
8917      }
8918  };
8919  
8920  /**
8921   * @inheritdoc
8922   */
8923  OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
8924      var widget = this;
8925      if ( !this.isDisabled() ) {
8926          // Allow the stack to clear so the value will be updated
8927          setTimeout( function () {
8928              widget.setValue( widget.$input.prop( 'checked' ) );
8929          } );
8930      }
8931  };
8932  
8933  /**
8934   * Input widget with a text field.
8935   *
8936   * @class
8937   * @extends OO.ui.InputWidget
8938   * @mixins OO.ui.IconElement
8939   * @mixins OO.ui.IndicatorElement
8940   * @mixins OO.ui.PendingElement
8941   *
8942   * @constructor
8943   * @param {Object} [config] Configuration options
8944   * @cfg {string} [placeholder] Placeholder text
8945   * @cfg {boolean} [multiline=false] Allow multiple lines of text
8946   * @cfg {boolean} [autosize=false] Automatically resize to fit content
8947   * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
8948   */
8949  OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
8950      // Configuration initialization
8951      config = config || {};
8952  
8953      // Parent constructor
8954      OO.ui.TextInputWidget.super.call( this, config );
8955  
8956      // Mixin constructors
8957      OO.ui.IconElement.call( this, config );
8958      OO.ui.IndicatorElement.call( this, config );
8959      OO.ui.PendingElement.call( this, config );
8960  
8961      // Properties
8962      this.multiline = !!config.multiline;
8963      this.autosize = !!config.autosize;
8964      this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
8965  
8966      // Events
8967      this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
8968      this.$element.on( 'DOMNodeInsertedIntoDocument', OO.ui.bind( this.onElementAttach, this ) );
8969      this.$icon.on( 'mousedown', OO.ui.bind( this.onIconMouseDown, this ) );
8970      this.$indicator.on( 'mousedown', OO.ui.bind( this.onIndicatorMouseDown, this ) );
8971  
8972      // Initialization
8973      this.$element
8974          .addClass( 'oo-ui-textInputWidget' )
8975          .append( this.$icon, this.$indicator );
8976      if ( config.placeholder ) {
8977          this.$input.attr( 'placeholder', config.placeholder );
8978      }
8979      this.$element.attr( 'role', 'textbox' );
8980  };
8981  
8982  /* Setup */
8983  
8984  OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
8985  OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
8986  OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
8987  OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
8988  
8989  /* Events */
8990  
8991  /**
8992   * User presses enter inside the text box.
8993   *
8994   * Not called if input is multiline.
8995   *
8996   * @event enter
8997   */
8998  
8999  /**
9000   * User clicks the icon.
9001   *
9002   * @event icon
9003   */
9004  
9005  /**
9006   * User clicks the indicator.
9007   *
9008   * @event indicator
9009   */
9010  
9011  /* Methods */
9012  
9013  /**
9014   * Handle icon mouse down events.
9015   *
9016   * @param {jQuery.Event} e Mouse down event
9017   * @fires icon
9018   */
9019  OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
9020      if ( e.which === 1 ) {
9021          this.$input[0].focus();
9022          this.emit( 'icon' );
9023          return false;
9024      }
9025  };
9026  
9027  /**
9028   * Handle indicator mouse down events.
9029   *
9030   * @param {jQuery.Event} e Mouse down event
9031   * @fires indicator
9032   */
9033  OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
9034      if ( e.which === 1 ) {
9035          this.$input[0].focus();
9036          this.emit( 'indicator' );
9037          return false;
9038      }
9039  };
9040  
9041  /**
9042   * Handle key press events.
9043   *
9044   * @param {jQuery.Event} e Key press event
9045   * @fires enter If enter key is pressed and input is not multiline
9046   */
9047  OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
9048      if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
9049          this.emit( 'enter' );
9050      }
9051  };
9052  
9053  /**
9054   * Handle element attach events.
9055   *
9056   * @param {jQuery.Event} e Element attach event
9057   */
9058  OO.ui.TextInputWidget.prototype.onElementAttach = function () {
9059      this.adjustSize();
9060  };
9061  
9062  /**
9063   * @inheritdoc
9064   */
9065  OO.ui.TextInputWidget.prototype.onEdit = function () {
9066      this.adjustSize();
9067  
9068      // Parent method
9069      return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
9070  };
9071  
9072  /**
9073   * @inheritdoc
9074   */
9075  OO.ui.TextInputWidget.prototype.setValue = function ( value ) {
9076      // Parent method
9077      OO.ui.TextInputWidget.super.prototype.setValue.call( this, value );
9078  
9079      this.adjustSize();
9080      return this;
9081  };
9082  
9083  /**
9084   * Automatically adjust the size of the text input.
9085   *
9086   * This only affects multi-line inputs that are auto-sized.
9087   *
9088   * @chainable
9089   */
9090  OO.ui.TextInputWidget.prototype.adjustSize = function () {
9091      var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, idealHeight;
9092  
9093      if ( this.multiline && this.autosize ) {
9094          $clone = this.$input.clone()
9095              .val( this.$input.val() )
9096              .css( { height: 0 } )
9097              .insertAfter( this.$input );
9098          // Set inline height property to 0 to measure scroll height
9099          scrollHeight = $clone[0].scrollHeight;
9100          // Remove inline height property to measure natural heights
9101          $clone.css( 'height', '' );
9102          innerHeight = $clone.innerHeight();
9103          outerHeight = $clone.outerHeight();
9104          // Measure max rows height
9105          $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' );
9106          maxInnerHeight = $clone.innerHeight();
9107          $clone.removeAttr( 'rows' ).css( 'height', '' );
9108          $clone.remove();
9109          idealHeight = Math.min( maxInnerHeight, scrollHeight );
9110          // Only apply inline height when expansion beyond natural height is needed
9111          this.$input.css(
9112              'height',
9113              // Use the difference between the inner and outer height as a buffer
9114              idealHeight > outerHeight ? idealHeight + ( outerHeight - innerHeight ) : ''
9115          );
9116      }
9117      return this;
9118  };
9119  
9120  /**
9121   * Get input element.
9122   *
9123   * @param {Object} [config] Configuration options
9124   * @return {jQuery} Input element
9125   */
9126  OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
9127      return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="text" />' );
9128  };
9129  
9130  /* Methods */
9131  
9132  /**
9133   * Check if input supports multiple lines.
9134   *
9135   * @return {boolean}
9136   */
9137  OO.ui.TextInputWidget.prototype.isMultiline = function () {
9138      return !!this.multiline;
9139  };
9140  
9141  /**
9142   * Check if input automatically adjusts its size.
9143   *
9144   * @return {boolean}
9145   */
9146  OO.ui.TextInputWidget.prototype.isAutosizing = function () {
9147      return !!this.autosize;
9148  };
9149  
9150  /**
9151   * Select the contents of the input.
9152   *
9153   * @chainable
9154   */
9155  OO.ui.TextInputWidget.prototype.select = function () {
9156      this.$input.select();
9157      return this;
9158  };
9159  
9160  /**
9161   * Text input with a menu of optional values.
9162   *
9163   * @class
9164   * @extends OO.ui.Widget
9165   *
9166   * @constructor
9167   * @param {Object} [config] Configuration options
9168   * @cfg {Object} [menu] Configuration options to pass to menu widget
9169   * @cfg {Object} [input] Configuration options to pass to input widget
9170   */
9171  OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
9172      // Configuration initialization
9173      config = config || {};
9174  
9175      // Parent constructor
9176      OO.ui.ComboBoxWidget.super.call( this, config );
9177  
9178      // Properties
9179      this.input = new OO.ui.TextInputWidget( $.extend(
9180          { $: this.$, indicator: 'down', disabled: this.isDisabled() },
9181          config.input
9182      ) );
9183      this.menu = new OO.ui.MenuWidget( $.extend(
9184          { $: this.$, widget: this, input: this.input, disabled: this.isDisabled() },
9185          config.menu
9186      ) );
9187  
9188      // Events
9189      this.input.connect( this, {
9190          change: 'onInputChange',
9191          indicator: 'onInputIndicator',
9192          enter: 'onInputEnter'
9193      } );
9194      this.menu.connect( this, {
9195          choose: 'onMenuChoose',
9196          add: 'onMenuItemsChange',
9197          remove: 'onMenuItemsChange'
9198      } );
9199  
9200      // Initialization
9201      this.$element.addClass( 'oo-ui-comboBoxWidget' ).append(
9202          this.input.$element,
9203          this.menu.$element
9204      );
9205      this.onMenuItemsChange();
9206  };
9207  
9208  /* Setup */
9209  
9210  OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
9211  
9212  /* Methods */
9213  
9214  /**
9215   * Handle input change events.
9216   *
9217   * @param {string} value New value
9218   */
9219  OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
9220      var match = this.menu.getItemFromData( value );
9221  
9222      this.menu.selectItem( match );
9223  
9224      if ( !this.isDisabled() ) {
9225          this.menu.toggle( true );
9226      }
9227  };
9228  
9229  /**
9230   * Handle input indicator events.
9231   */
9232  OO.ui.ComboBoxWidget.prototype.onInputIndicator = function () {
9233      if ( !this.isDisabled() ) {
9234          this.menu.toggle();
9235      }
9236  };
9237  
9238  /**
9239   * Handle input enter events.
9240   */
9241  OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
9242      if ( !this.isDisabled() ) {
9243          this.menu.toggle( false );
9244      }
9245  };
9246  
9247  /**
9248   * Handle menu choose events.
9249   *
9250   * @param {OO.ui.OptionWidget} item Chosen item
9251   */
9252  OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
9253      if ( item ) {
9254          this.input.setValue( item.getData() );
9255      }
9256  };
9257  
9258  /**
9259   * Handle menu item change events.
9260   */
9261  OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
9262      this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
9263  };
9264  
9265  /**
9266   * @inheritdoc
9267   */
9268  OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
9269      // Parent method
9270      OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
9271  
9272      if ( this.input ) {
9273          this.input.setDisabled( this.isDisabled() );
9274      }
9275      if ( this.menu ) {
9276          this.menu.setDisabled( this.isDisabled() );
9277      }
9278  
9279      return this;
9280  };
9281  
9282  /**
9283   * Label widget.
9284   *
9285   * @class
9286   * @extends OO.ui.Widget
9287   * @mixins OO.ui.LabelElement
9288   *
9289   * @constructor
9290   * @param {Object} [config] Configuration options
9291   */
9292  OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
9293      // Config intialization
9294      config = config || {};
9295  
9296      // Parent constructor
9297      OO.ui.LabelWidget.super.call( this, config );
9298  
9299      // Mixin constructors
9300      OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
9301  
9302      // Properties
9303      this.input = config.input;
9304  
9305      // Events
9306      if ( this.input instanceof OO.ui.InputWidget ) {
9307          this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
9308      }
9309  
9310      // Initialization
9311      this.$element.addClass( 'oo-ui-labelWidget' );
9312  };
9313  
9314  /* Setup */
9315  
9316  OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
9317  OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
9318  
9319  /* Static Properties */
9320  
9321  OO.ui.LabelWidget.static.tagName = 'span';
9322  
9323  /* Methods */
9324  
9325  /**
9326   * Handles label mouse click events.
9327   *
9328   * @param {jQuery.Event} e Mouse click event
9329   */
9330  OO.ui.LabelWidget.prototype.onClick = function () {
9331      this.input.simulateLabelClick();
9332      return false;
9333  };
9334  
9335  /**
9336   * Generic option widget for use with OO.ui.SelectWidget.
9337   *
9338   * @class
9339   * @extends OO.ui.Widget
9340   * @mixins OO.ui.LabelElement
9341   * @mixins OO.ui.FlaggedElement
9342   *
9343   * @constructor
9344   * @param {Mixed} data Option data
9345   * @param {Object} [config] Configuration options
9346   * @cfg {string} [rel] Value for `rel` attribute in DOM, allowing per-option styling
9347   */
9348  OO.ui.OptionWidget = function OoUiOptionWidget( data, config ) {
9349      // Config intialization
9350      config = config || {};
9351  
9352      // Parent constructor
9353      OO.ui.OptionWidget.super.call( this, config );
9354  
9355      // Mixin constructors
9356      OO.ui.ItemWidget.call( this );
9357      OO.ui.LabelElement.call( this, config );
9358      OO.ui.FlaggedElement.call( this, config );
9359  
9360      // Properties
9361      this.data = data;
9362      this.selected = false;
9363      this.highlighted = false;
9364      this.pressed = false;
9365  
9366      // Initialization
9367      this.$element
9368          .data( 'oo-ui-optionWidget', this )
9369          .attr( 'rel', config.rel )
9370          .attr( 'role', 'option' )
9371          .addClass( 'oo-ui-optionWidget' )
9372          .append( this.$label );
9373      this.$element
9374          .prepend( this.$icon )
9375          .append( this.$indicator );
9376  };
9377  
9378  /* Setup */
9379  
9380  OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
9381  OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
9382  OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement );
9383  OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement );
9384  
9385  /* Static Properties */
9386  
9387  OO.ui.OptionWidget.static.selectable = true;
9388  
9389  OO.ui.OptionWidget.static.highlightable = true;
9390  
9391  OO.ui.OptionWidget.static.pressable = true;
9392  
9393  OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
9394  
9395  /* Methods */
9396  
9397  /**
9398   * Check if option can be selected.
9399   *
9400   * @return {boolean} Item is selectable
9401   */
9402  OO.ui.OptionWidget.prototype.isSelectable = function () {
9403      return this.constructor.static.selectable && !this.isDisabled();
9404  };
9405  
9406  /**
9407   * Check if option can be highlighted.
9408   *
9409   * @return {boolean} Item is highlightable
9410   */
9411  OO.ui.OptionWidget.prototype.isHighlightable = function () {
9412      return this.constructor.static.highlightable && !this.isDisabled();
9413  };
9414  
9415  /**
9416   * Check if option can be pressed.
9417   *
9418   * @return {boolean} Item is pressable
9419   */
9420  OO.ui.OptionWidget.prototype.isPressable = function () {
9421      return this.constructor.static.pressable && !this.isDisabled();
9422  };
9423  
9424  /**
9425   * Check if option is selected.
9426   *
9427   * @return {boolean} Item is selected
9428   */
9429  OO.ui.OptionWidget.prototype.isSelected = function () {
9430      return this.selected;
9431  };
9432  
9433  /**
9434   * Check if option is highlighted.
9435   *
9436   * @return {boolean} Item is highlighted
9437   */
9438  OO.ui.OptionWidget.prototype.isHighlighted = function () {
9439      return this.highlighted;
9440  };
9441  
9442  /**
9443   * Check if option is pressed.
9444   *
9445   * @return {boolean} Item is pressed
9446   */
9447  OO.ui.OptionWidget.prototype.isPressed = function () {
9448      return this.pressed;
9449  };
9450  
9451  /**
9452   * Set selected state.
9453   *
9454   * @param {boolean} [state=false] Select option
9455   * @chainable
9456   */
9457  OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
9458      if ( this.constructor.static.selectable ) {
9459          this.selected = !!state;
9460          this.$element.toggleClass( 'oo-ui-optionWidget-selected', state );
9461          if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
9462              this.scrollElementIntoView();
9463          }
9464      }
9465      return this;
9466  };
9467  
9468  /**
9469   * Set highlighted state.
9470   *
9471   * @param {boolean} [state=false] Highlight option
9472   * @chainable
9473   */
9474  OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
9475      if ( this.constructor.static.highlightable ) {
9476          this.highlighted = !!state;
9477          this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
9478      }
9479      return this;
9480  };
9481  
9482  /**
9483   * Set pressed state.
9484   *
9485   * @param {boolean} [state=false] Press option
9486   * @chainable
9487   */
9488  OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
9489      if ( this.constructor.static.pressable ) {
9490          this.pressed = !!state;
9491          this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
9492      }
9493      return this;
9494  };
9495  
9496  /**
9497   * Make the option's highlight flash.
9498   *
9499   * While flashing, the visual style of the pressed state is removed if present.
9500   *
9501   * @return {jQuery.Promise} Promise resolved when flashing is done
9502   */
9503  OO.ui.OptionWidget.prototype.flash = function () {
9504      var widget = this,
9505          $element = this.$element,
9506          deferred = $.Deferred();
9507  
9508      if ( !this.isDisabled() && this.constructor.static.pressable ) {
9509          $element.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
9510          setTimeout( function () {
9511              // Restore original classes
9512              $element
9513                  .toggleClass( 'oo-ui-optionWidget-highlighted', widget.highlighted )
9514                  .toggleClass( 'oo-ui-optionWidget-pressed', widget.pressed );
9515  
9516              setTimeout( function () {
9517                  deferred.resolve();
9518              }, 100 );
9519  
9520          }, 100 );
9521      }
9522  
9523      return deferred.promise();
9524  };
9525  
9526  /**
9527   * Get option data.
9528   *
9529   * @return {Mixed} Option data
9530   */
9531  OO.ui.OptionWidget.prototype.getData = function () {
9532      return this.data;
9533  };
9534  
9535  /**
9536   * Option widget with an option icon and indicator.
9537   *
9538   * Use together with OO.ui.SelectWidget.
9539   *
9540   * @class
9541   * @extends OO.ui.OptionWidget
9542   * @mixins OO.ui.IconElement
9543   * @mixins OO.ui.IndicatorElement
9544   *
9545   * @constructor
9546   * @param {Mixed} data Option data
9547   * @param {Object} [config] Configuration options
9548   */
9549  OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( data, config ) {
9550      // Parent constructor
9551      OO.ui.DecoratedOptionWidget.super.call( this, data, config );
9552  
9553      // Mixin constructors
9554      OO.ui.IconElement.call( this, config );
9555      OO.ui.IndicatorElement.call( this, config );
9556  
9557      // Initialization
9558      this.$element
9559          .addClass( 'oo-ui-decoratedOptionWidget' )
9560          .prepend( this.$icon )
9561          .append( this.$indicator );
9562  };
9563  
9564  /* Setup */
9565  
9566  OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
9567  OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement );
9568  OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
9569  
9570  /**
9571   * Option widget that looks like a button.
9572   *
9573   * Use together with OO.ui.ButtonSelectWidget.
9574   *
9575   * @class
9576   * @extends OO.ui.DecoratedOptionWidget
9577   * @mixins OO.ui.ButtonElement
9578   *
9579   * @constructor
9580   * @param {Mixed} data Option data
9581   * @param {Object} [config] Configuration options
9582   */
9583  OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( data, config ) {
9584      // Parent constructor
9585      OO.ui.ButtonOptionWidget.super.call( this, data, config );
9586  
9587      // Mixin constructors
9588      OO.ui.ButtonElement.call( this, config );
9589  
9590      // Initialization
9591      this.$element.addClass( 'oo-ui-buttonOptionWidget' );
9592      this.$button.append( this.$element.contents() );
9593      this.$element.append( this.$button );
9594  };
9595  
9596  /* Setup */
9597  
9598  OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
9599  OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
9600  
9601  /* Static Properties */
9602  
9603  // Allow button mouse down events to pass through so they can be handled by the parent select widget
9604  OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
9605  
9606  /* Methods */
9607  
9608  /**
9609   * @inheritdoc
9610   */
9611  OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
9612      OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
9613  
9614      if ( this.constructor.static.selectable ) {
9615          this.setActive( state );
9616      }
9617  
9618      return this;
9619  };
9620  
9621  /**
9622   * Item of an OO.ui.MenuWidget.
9623   *
9624   * @class
9625   * @extends OO.ui.DecoratedOptionWidget
9626   *
9627   * @constructor
9628   * @param {Mixed} data Item data
9629   * @param {Object} [config] Configuration options
9630   */
9631  OO.ui.MenuItemWidget = function OoUiMenuItemWidget( data, config ) {
9632      // Configuration initialization
9633      config = $.extend( { icon: 'check' }, config );
9634  
9635      // Parent constructor
9636      OO.ui.MenuItemWidget.super.call( this, data, config );
9637  
9638      // Initialization
9639      this.$element
9640          .attr( 'role', 'menuitem' )
9641          .addClass( 'oo-ui-menuItemWidget' );
9642  };
9643  
9644  /* Setup */
9645  
9646  OO.inheritClass( OO.ui.MenuItemWidget, OO.ui.DecoratedOptionWidget );
9647  
9648  /**
9649   * Section to group one or more items in a OO.ui.MenuWidget.
9650   *
9651   * @class
9652   * @extends OO.ui.DecoratedOptionWidget
9653   *
9654   * @constructor
9655   * @param {Mixed} data Item data
9656   * @param {Object} [config] Configuration options
9657   */
9658  OO.ui.MenuSectionItemWidget = function OoUiMenuSectionItemWidget( data, config ) {
9659      // Parent constructor
9660      OO.ui.MenuSectionItemWidget.super.call( this, data, config );
9661  
9662      // Initialization
9663      this.$element.addClass( 'oo-ui-menuSectionItemWidget' );
9664  };
9665  
9666  /* Setup */
9667  
9668  OO.inheritClass( OO.ui.MenuSectionItemWidget, OO.ui.DecoratedOptionWidget );
9669  
9670  /* Static Properties */
9671  
9672  OO.ui.MenuSectionItemWidget.static.selectable = false;
9673  
9674  OO.ui.MenuSectionItemWidget.static.highlightable = false;
9675  
9676  /**
9677   * Items for an OO.ui.OutlineWidget.
9678   *
9679   * @class
9680   * @extends OO.ui.DecoratedOptionWidget
9681   *
9682   * @constructor
9683   * @param {Mixed} data Item data
9684   * @param {Object} [config] Configuration options
9685   * @cfg {number} [level] Indentation level
9686   * @cfg {boolean} [movable] Allow modification from outline controls
9687   */
9688  OO.ui.OutlineItemWidget = function OoUiOutlineItemWidget( data, config ) {
9689      // Config intialization
9690      config = config || {};
9691  
9692      // Parent constructor
9693      OO.ui.OutlineItemWidget.super.call( this, data, config );
9694  
9695      // Properties
9696      this.level = 0;
9697      this.movable = !!config.movable;
9698      this.removable = !!config.removable;
9699  
9700      // Initialization
9701      this.$element.addClass( 'oo-ui-outlineItemWidget' );
9702      this.setLevel( config.level );
9703  };
9704  
9705  /* Setup */
9706  
9707  OO.inheritClass( OO.ui.OutlineItemWidget, OO.ui.DecoratedOptionWidget );
9708  
9709  /* Static Properties */
9710  
9711  OO.ui.OutlineItemWidget.static.highlightable = false;
9712  
9713  OO.ui.OutlineItemWidget.static.scrollIntoViewOnSelect = true;
9714  
9715  OO.ui.OutlineItemWidget.static.levelClass = 'oo-ui-outlineItemWidget-level-';
9716  
9717  OO.ui.OutlineItemWidget.static.levels = 3;
9718  
9719  /* Methods */
9720  
9721  /**
9722   * Check if item is movable.
9723   *
9724   * Movablilty is used by outline controls.
9725   *
9726   * @return {boolean} Item is movable
9727   */
9728  OO.ui.OutlineItemWidget.prototype.isMovable = function () {
9729      return this.movable;
9730  };
9731  
9732  /**
9733   * Check if item is removable.
9734   *
9735   * Removablilty is used by outline controls.
9736   *
9737   * @return {boolean} Item is removable
9738   */
9739  OO.ui.OutlineItemWidget.prototype.isRemovable = function () {
9740      return this.removable;
9741  };
9742  
9743  /**
9744   * Get indentation level.
9745   *
9746   * @return {number} Indentation level
9747   */
9748  OO.ui.OutlineItemWidget.prototype.getLevel = function () {
9749      return this.level;
9750  };
9751  
9752  /**
9753   * Set movability.
9754   *
9755   * Movablilty is used by outline controls.
9756   *
9757   * @param {boolean} movable Item is movable
9758   * @chainable
9759   */
9760  OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
9761      this.movable = !!movable;
9762      return this;
9763  };
9764  
9765  /**
9766   * Set removability.
9767   *
9768   * Removablilty is used by outline controls.
9769   *
9770   * @param {boolean} movable Item is removable
9771   * @chainable
9772   */
9773  OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
9774      this.removable = !!removable;
9775      return this;
9776  };
9777  
9778  /**
9779   * Set indentation level.
9780   *
9781   * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
9782   * @chainable
9783   */
9784  OO.ui.OutlineItemWidget.prototype.setLevel = function ( level ) {
9785      var levels = this.constructor.static.levels,
9786          levelClass = this.constructor.static.levelClass,
9787          i = levels;
9788  
9789      this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
9790      while ( i-- ) {
9791          if ( this.level === i ) {
9792              this.$element.addClass( levelClass + i );
9793          } else {
9794              this.$element.removeClass( levelClass + i );
9795          }
9796      }
9797  
9798      return this;
9799  };
9800  
9801  /**
9802   * Container for content that is overlaid and positioned absolutely.
9803   *
9804   * @class
9805   * @extends OO.ui.Widget
9806   * @mixins OO.ui.LabelElement
9807   *
9808   * @constructor
9809   * @param {Object} [config] Configuration options
9810   * @cfg {number} [width=320] Width of popup in pixels
9811   * @cfg {number} [height] Height of popup, omit to use automatic height
9812   * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
9813   * @cfg {string} [align='center'] Alignment of popup to origin
9814   * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
9815   * @cfg {jQuery} [$content] Content to append to the popup's body
9816   * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
9817   * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
9818   * @cfg {boolean} [head] Show label and close button at the top
9819   * @cfg {boolean} [padded] Add padding to the body
9820   */
9821  OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
9822      // Config intialization
9823      config = config || {};
9824  
9825      // Parent constructor
9826      OO.ui.PopupWidget.super.call( this, config );
9827  
9828      // Mixin constructors
9829      OO.ui.LabelElement.call( this, config );
9830      OO.ui.ClippableElement.call( this, config );
9831  
9832      // Properties
9833      this.visible = false;
9834      this.$popup = this.$( '<div>' );
9835      this.$head = this.$( '<div>' );
9836      this.$body = this.$( '<div>' );
9837      this.$anchor = this.$( '<div>' );
9838      this.$container = config.$container; // If undefined, will be computed lazily in updateDimensions()
9839      this.autoClose = !!config.autoClose;
9840      this.$autoCloseIgnore = config.$autoCloseIgnore;
9841      this.transitionTimeout = null;
9842      this.anchor = null;
9843      this.width = config.width !== undefined ? config.width : 320;
9844      this.height = config.height !== undefined ? config.height : null;
9845      this.align = config.align || 'center';
9846      this.closeButton = new OO.ui.ButtonWidget( { $: this.$, framed: false, icon: 'close' } );
9847      this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this );
9848  
9849      // Events
9850      this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
9851  
9852      // Initialization
9853      this.toggleAnchor( config.anchor === undefined || config.anchor );
9854      this.$body.addClass( 'oo-ui-popupWidget-body' );
9855      this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
9856      this.$head
9857          .addClass( 'oo-ui-popupWidget-head' )
9858          .append( this.$label, this.closeButton.$element );
9859      if ( !config.head ) {
9860          this.$head.hide();
9861      }
9862      this.$popup
9863          .addClass( 'oo-ui-popupWidget-popup' )
9864          .append( this.$head, this.$body );
9865      this.$element
9866          .hide()
9867          .addClass( 'oo-ui-popupWidget' )
9868          .append( this.$popup, this.$anchor );
9869      // Move content, which was added to #$element by OO.ui.Widget, to the body
9870      if ( config.$content instanceof jQuery ) {
9871          this.$body.append( config.$content );
9872      }
9873      if ( config.padded ) {
9874          this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
9875      }
9876      this.setClippableElement( this.$body );
9877  };
9878  
9879  /* Setup */
9880  
9881  OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
9882  OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
9883  OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
9884  
9885  /* Events */
9886  
9887  /**
9888   * @event hide
9889   */
9890  
9891  /**
9892   * @event show
9893   */
9894  
9895  /* Methods */
9896  
9897  /**
9898   * Handles mouse down events.
9899   *
9900   * @param {jQuery.Event} e Mouse down event
9901   */
9902  OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
9903      if (
9904          this.isVisible() &&
9905          !$.contains( this.$element[0], e.target ) &&
9906          ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
9907      ) {
9908          this.toggle( false );
9909      }
9910  };
9911  
9912  /**
9913   * Bind mouse down listener.
9914   */
9915  OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
9916      // Capture clicks outside popup
9917      this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
9918  };
9919  
9920  /**
9921   * Handles close button click events.
9922   */
9923  OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
9924      if ( this.isVisible() ) {
9925          this.toggle( false );
9926      }
9927  };
9928  
9929  /**
9930   * Unbind mouse down listener.
9931   */
9932  OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
9933      this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
9934  };
9935  
9936  /**
9937   * Set whether to show a anchor.
9938   *
9939   * @param {boolean} [show] Show anchor, omit to toggle
9940   */
9941  OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
9942      show = show === undefined ? !this.anchored : !!show;
9943  
9944      if ( this.anchored !== show ) {
9945          if ( show ) {
9946              this.$element.addClass( 'oo-ui-popupWidget-anchored' );
9947          } else {
9948              this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
9949          }
9950          this.anchored = show;
9951      }
9952  };
9953  
9954  /**
9955   * Check if showing a anchor.
9956   *
9957   * @return {boolean} anchor is visible
9958   */
9959  OO.ui.PopupWidget.prototype.hasAnchor = function () {
9960      return this.anchor;
9961  };
9962  
9963  /**
9964   * @inheritdoc
9965   */
9966  OO.ui.PopupWidget.prototype.toggle = function ( show ) {
9967      show = show === undefined ? !this.isVisible() : !!show;
9968  
9969      var change = show !== this.isVisible();
9970  
9971      // Parent method
9972      OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
9973  
9974      if ( change ) {
9975          if ( show ) {
9976              if ( this.autoClose ) {
9977                  this.bindMouseDownListener();
9978              }
9979              this.updateDimensions();
9980              this.toggleClipping( true );
9981          } else {
9982              this.toggleClipping( false );
9983              if ( this.autoClose ) {
9984                  this.unbindMouseDownListener();
9985              }
9986          }
9987      }
9988  
9989      return this;
9990  };
9991  
9992  /**
9993   * Set the size of the popup.
9994   *
9995   * Changing the size may also change the popup's position depending on the alignment.
9996   *
9997   * @param {number} width Width
9998   * @param {number} height Height
9999   * @param {boolean} [transition=false] Use a smooth transition
10000   * @chainable
10001   */
10002  OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
10003      this.width = width;
10004      this.height = height !== undefined ? height : null;
10005      if ( this.isVisible() ) {
10006          this.updateDimensions( transition );
10007      }
10008  };
10009  
10010  /**
10011   * Update the size and position.
10012   *
10013   * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
10014   * be called automatically.
10015   *
10016   * @param {boolean} [transition=false] Use a smooth transition
10017   * @chainable
10018   */
10019  OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
10020      var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
10021          popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
10022          widget = this,
10023          padding = 10;
10024  
10025      if ( !this.$container ) {
10026          // Lazy-initialize $container if not specified in constructor
10027          this.$container = this.$( this.getClosestScrollableElementContainer() );
10028      }
10029  
10030      // Set height and width before measuring things, since it might cause our measurements
10031      // to change (e.g. due to scrollbars appearing or disappearing)
10032      this.$popup.css( {
10033          width: this.width,
10034          height: this.height !== null ? this.height : 'auto'
10035      } );
10036  
10037      // Compute initial popupOffset based on alignment
10038      popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[this.align];
10039  
10040      // Figure out if this will cause the popup to go beyond the edge of the container
10041      originOffset = Math.round( this.$element.offset().left );
10042      containerLeft = Math.round( this.$container.offset().left );
10043      containerWidth = this.$container.innerWidth();
10044      containerRight = containerLeft + containerWidth;
10045      popupLeft = popupOffset - padding;
10046      popupRight = popupOffset + padding + this.width + padding;
10047      overlapLeft = ( originOffset + popupLeft ) - containerLeft;
10048      overlapRight = containerRight - ( originOffset + popupRight );
10049  
10050      // Adjust offset to make the popup not go beyond the edge, if needed
10051      if ( overlapRight < 0 ) {
10052          popupOffset += overlapRight;
10053      } else if ( overlapLeft < 0 ) {
10054          popupOffset -= overlapLeft;
10055      }
10056  
10057      // Adjust offset to avoid anchor being rendered too close to the edge
10058      anchorWidth = this.$anchor.width();
10059      if ( this.align === 'right' ) {
10060          popupOffset += anchorWidth;
10061      } else if ( this.align === 'left' ) {
10062          popupOffset -= anchorWidth;
10063      }
10064  
10065      // Prevent transition from being interrupted
10066      clearTimeout( this.transitionTimeout );
10067      if ( transition ) {
10068          // Enable transition
10069          this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
10070      }
10071  
10072      // Position body relative to anchor
10073      this.$popup.css( 'left', popupOffset );
10074  
10075      if ( transition ) {
10076          // Prevent transitioning after transition is complete
10077          this.transitionTimeout = setTimeout( function () {
10078              widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
10079          }, 200 );
10080      } else {
10081          // Prevent transitioning immediately
10082          this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
10083      }
10084  
10085      return this;
10086  };
10087  
10088  /**
10089   * Search widget.
10090   *
10091   * Search widgets combine a query input, placed above, and a results selection widget, placed below.
10092   * Results are cleared and populated each time the query is changed.
10093   *
10094   * @class
10095   * @extends OO.ui.Widget
10096   *
10097   * @constructor
10098   * @param {Object} [config] Configuration options
10099   * @cfg {string|jQuery} [placeholder] Placeholder text for query input
10100   * @cfg {string} [value] Initial query value
10101   */
10102  OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
10103      // Configuration intialization
10104      config = config || {};
10105  
10106      // Parent constructor
10107      OO.ui.SearchWidget.super.call( this, config );
10108  
10109      // Properties
10110      this.query = new OO.ui.TextInputWidget( {
10111          $: this.$,
10112          icon: 'search',
10113          placeholder: config.placeholder,
10114          value: config.value
10115      } );
10116      this.results = new OO.ui.SelectWidget( { $: this.$ } );
10117      this.$query = this.$( '<div>' );
10118      this.$results = this.$( '<div>' );
10119  
10120      // Events
10121      this.query.connect( this, {
10122          change: 'onQueryChange',
10123          enter: 'onQueryEnter'
10124      } );
10125      this.results.connect( this, {
10126          highlight: 'onResultsHighlight',
10127          select: 'onResultsSelect'
10128      } );
10129      this.query.$input.on( 'keydown', OO.ui.bind( this.onQueryKeydown, this ) );
10130  
10131      // Initialization
10132      this.$query
10133          .addClass( 'oo-ui-searchWidget-query' )
10134          .append( this.query.$element );
10135      this.$results
10136          .addClass( 'oo-ui-searchWidget-results' )
10137          .append( this.results.$element );
10138      this.$element
10139          .addClass( 'oo-ui-searchWidget' )
10140          .append( this.$results, this.$query );
10141  };
10142  
10143  /* Setup */
10144  
10145  OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
10146  
10147  /* Events */
10148  
10149  /**
10150   * @event highlight
10151   * @param {Object|null} item Item data or null if no item is highlighted
10152   */
10153  
10154  /**
10155   * @event select
10156   * @param {Object|null} item Item data or null if no item is selected
10157   */
10158  
10159  /* Methods */
10160  
10161  /**
10162   * Handle query key down events.
10163   *
10164   * @param {jQuery.Event} e Key down event
10165   */
10166  OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
10167      var highlightedItem, nextItem,
10168          dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
10169  
10170      if ( dir ) {
10171          highlightedItem = this.results.getHighlightedItem();
10172          if ( !highlightedItem ) {
10173              highlightedItem = this.results.getSelectedItem();
10174          }
10175          nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
10176          this.results.highlightItem( nextItem );
10177          nextItem.scrollElementIntoView();
10178      }
10179  };
10180  
10181  /**
10182   * Handle select widget select events.
10183   *
10184   * Clears existing results. Subclasses should repopulate items according to new query.
10185   *
10186   * @param {string} value New value
10187   */
10188  OO.ui.SearchWidget.prototype.onQueryChange = function () {
10189      // Reset
10190      this.results.clearItems();
10191  };
10192  
10193  /**
10194   * Handle select widget enter key events.
10195   *
10196   * Selects highlighted item.
10197   *
10198   * @param {string} value New value
10199   */
10200  OO.ui.SearchWidget.prototype.onQueryEnter = function () {
10201      // Reset
10202      this.results.selectItem( this.results.getHighlightedItem() );
10203  };
10204  
10205  /**
10206   * Handle select widget highlight events.
10207   *
10208   * @param {OO.ui.OptionWidget} item Highlighted item
10209   * @fires highlight
10210   */
10211  OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
10212      this.emit( 'highlight', item ? item.getData() : null );
10213  };
10214  
10215  /**
10216   * Handle select widget select events.
10217   *
10218   * @param {OO.ui.OptionWidget} item Selected item
10219   * @fires select
10220   */
10221  OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
10222      this.emit( 'select', item ? item.getData() : null );
10223  };
10224  
10225  /**
10226   * Get the query input.
10227   *
10228   * @return {OO.ui.TextInputWidget} Query input
10229   */
10230  OO.ui.SearchWidget.prototype.getQuery = function () {
10231      return this.query;
10232  };
10233  
10234  /**
10235   * Get the results list.
10236   *
10237   * @return {OO.ui.SelectWidget} Select list
10238   */
10239  OO.ui.SearchWidget.prototype.getResults = function () {
10240      return this.results;
10241  };
10242  
10243  /**
10244   * Generic selection of options.
10245   *
10246   * Items can contain any rendering, and are uniquely identified by a has of thier data. Any widget
10247   * that provides options, from which the user must choose one, should be built on this class.
10248   *
10249   * Use together with OO.ui.OptionWidget.
10250   *
10251   * @class
10252   * @extends OO.ui.Widget
10253   * @mixins OO.ui.GroupElement
10254   *
10255   * @constructor
10256   * @param {Object} [config] Configuration options
10257   * @cfg {OO.ui.OptionWidget[]} [items] Options to add
10258   */
10259  OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
10260      // Config intialization
10261      config = config || {};
10262  
10263      // Parent constructor
10264      OO.ui.SelectWidget.super.call( this, config );
10265  
10266      // Mixin constructors
10267      OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
10268  
10269      // Properties
10270      this.pressed = false;
10271      this.selecting = null;
10272      this.hashes = {};
10273      this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this );
10274      this.onMouseMoveHandler = OO.ui.bind( this.onMouseMove, this );
10275  
10276      // Events
10277      this.$element.on( {
10278          mousedown: OO.ui.bind( this.onMouseDown, this ),
10279          mouseover: OO.ui.bind( this.onMouseOver, this ),
10280          mouseleave: OO.ui.bind( this.onMouseLeave, this )
10281      } );
10282  
10283      // Initialization
10284      this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
10285      if ( $.isArray( config.items ) ) {
10286          this.addItems( config.items );
10287      }
10288  };
10289  
10290  /* Setup */
10291  
10292  OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
10293  
10294  // Need to mixin base class as well
10295  OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
10296  OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
10297  
10298  /* Events */
10299  
10300  /**
10301   * @event highlight
10302   * @param {OO.ui.OptionWidget|null} item Highlighted item
10303   */
10304  
10305  /**
10306   * @event press
10307   * @param {OO.ui.OptionWidget|null} item Pressed item
10308   */
10309  
10310  /**
10311   * @event select
10312   * @param {OO.ui.OptionWidget|null} item Selected item
10313   */
10314  
10315  /**
10316   * @event choose
10317   * @param {OO.ui.OptionWidget|null} item Chosen item
10318   */
10319  
10320  /**
10321   * @event add
10322   * @param {OO.ui.OptionWidget[]} items Added items
10323   * @param {number} index Index items were added at
10324   */
10325  
10326  /**
10327   * @event remove
10328   * @param {OO.ui.OptionWidget[]} items Removed items
10329   */
10330  
10331  /* Methods */
10332  
10333  /**
10334   * Handle mouse down events.
10335   *
10336   * @private
10337   * @param {jQuery.Event} e Mouse down event
10338   */
10339  OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
10340      var item;
10341  
10342      if ( !this.isDisabled() && e.which === 1 ) {
10343          this.togglePressed( true );
10344          item = this.getTargetItem( e );
10345          if ( item && item.isSelectable() ) {
10346              this.pressItem( item );
10347              this.selecting = item;
10348              this.getElementDocument().addEventListener(
10349                  'mouseup',
10350                  this.onMouseUpHandler,
10351                  true
10352              );
10353              this.getElementDocument().addEventListener(
10354                  'mousemove',
10355                  this.onMouseMoveHandler,
10356                  true
10357              );
10358          }
10359      }
10360      return false;
10361  };
10362  
10363  /**
10364   * Handle mouse up events.
10365   *
10366   * @private
10367   * @param {jQuery.Event} e Mouse up event
10368   */
10369  OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
10370      var item;
10371  
10372      this.togglePressed( false );
10373      if ( !this.selecting ) {
10374          item = this.getTargetItem( e );
10375          if ( item && item.isSelectable() ) {
10376              this.selecting = item;
10377          }
10378      }
10379      if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
10380          this.pressItem( null );
10381          this.chooseItem( this.selecting );
10382          this.selecting = null;
10383      }
10384  
10385      this.getElementDocument().removeEventListener(
10386          'mouseup',
10387          this.onMouseUpHandler,
10388          true
10389      );
10390      this.getElementDocument().removeEventListener(
10391          'mousemove',
10392          this.onMouseMoveHandler,
10393          true
10394      );
10395  
10396      return false;
10397  };
10398  
10399  /**
10400   * Handle mouse move events.
10401   *
10402   * @private
10403   * @param {jQuery.Event} e Mouse move event
10404   */
10405  OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
10406      var item;
10407  
10408      if ( !this.isDisabled() && this.pressed ) {
10409          item = this.getTargetItem( e );
10410          if ( item && item !== this.selecting && item.isSelectable() ) {
10411              this.pressItem( item );
10412              this.selecting = item;
10413          }
10414      }
10415      return false;
10416  };
10417  
10418  /**
10419   * Handle mouse over events.
10420   *
10421   * @private
10422   * @param {jQuery.Event} e Mouse over event
10423   */
10424  OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
10425      var item;
10426  
10427      if ( !this.isDisabled() ) {
10428          item = this.getTargetItem( e );
10429          this.highlightItem( item && item.isHighlightable() ? item : null );
10430      }
10431      return false;
10432  };
10433  
10434  /**
10435   * Handle mouse leave events.
10436   *
10437   * @private
10438   * @param {jQuery.Event} e Mouse over event
10439   */
10440  OO.ui.SelectWidget.prototype.onMouseLeave = function () {
10441      if ( !this.isDisabled() ) {
10442          this.highlightItem( null );
10443      }
10444      return false;
10445  };
10446  
10447  /**
10448   * Get the closest item to a jQuery.Event.
10449   *
10450   * @private
10451   * @param {jQuery.Event} e
10452   * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
10453   */
10454  OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
10455      var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' );
10456      if ( $item.length ) {
10457          return $item.data( 'oo-ui-optionWidget' );
10458      }
10459      return null;
10460  };
10461  
10462  /**
10463   * Get selected item.
10464   *
10465   * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
10466   */
10467  OO.ui.SelectWidget.prototype.getSelectedItem = function () {
10468      var i, len;
10469  
10470      for ( i = 0, len = this.items.length; i < len; i++ ) {
10471          if ( this.items[i].isSelected() ) {
10472              return this.items[i];
10473          }
10474      }
10475      return null;
10476  };
10477  
10478  /**
10479   * Get highlighted item.
10480   *
10481   * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
10482   */
10483  OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
10484      var i, len;
10485  
10486      for ( i = 0, len = this.items.length; i < len; i++ ) {
10487          if ( this.items[i].isHighlighted() ) {
10488              return this.items[i];
10489          }
10490      }
10491      return null;
10492  };
10493  
10494  /**
10495   * Get an existing item with equivilant data.
10496   *
10497   * @param {Object} data Item data to search for
10498   * @return {OO.ui.OptionWidget|null} Item with equivilent value, `null` if none exists
10499   */
10500  OO.ui.SelectWidget.prototype.getItemFromData = function ( data ) {
10501      var hash = OO.getHash( data );
10502  
10503      if ( hash in this.hashes ) {
10504          return this.hashes[hash];
10505      }
10506  
10507      return null;
10508  };
10509  
10510  /**
10511   * Toggle pressed state.
10512   *
10513   * @param {boolean} pressed An option is being pressed
10514   */
10515  OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
10516      if ( pressed === undefined ) {
10517          pressed = !this.pressed;
10518      }
10519      if ( pressed !== this.pressed ) {
10520          this.$element
10521              .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
10522              .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
10523          this.pressed = pressed;
10524      }
10525  };
10526  
10527  /**
10528   * Highlight an item.
10529   *
10530   * Highlighting is mutually exclusive.
10531   *
10532   * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
10533   * @fires highlight
10534   * @chainable
10535   */
10536  OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
10537      var i, len, highlighted,
10538          changed = false;
10539  
10540      for ( i = 0, len = this.items.length; i < len; i++ ) {
10541          highlighted = this.items[i] === item;
10542          if ( this.items[i].isHighlighted() !== highlighted ) {
10543              this.items[i].setHighlighted( highlighted );
10544              changed = true;
10545          }
10546      }
10547      if ( changed ) {
10548          this.emit( 'highlight', item );
10549      }
10550  
10551      return this;
10552  };
10553  
10554  /**
10555   * Select an item.
10556   *
10557   * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
10558   * @fires select
10559   * @chainable
10560   */
10561  OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
10562      var i, len, selected,
10563          changed = false;
10564  
10565      for ( i = 0, len = this.items.length; i < len; i++ ) {
10566          selected = this.items[i] === item;
10567          if ( this.items[i].isSelected() !== selected ) {
10568              this.items[i].setSelected( selected );
10569              changed = true;
10570          }
10571      }
10572      if ( changed ) {
10573          this.emit( 'select', item );
10574      }
10575  
10576      return this;
10577  };
10578  
10579  /**
10580   * Press an item.
10581   *
10582   * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
10583   * @fires press
10584   * @chainable
10585   */
10586  OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
10587      var i, len, pressed,
10588          changed = false;
10589  
10590      for ( i = 0, len = this.items.length; i < len; i++ ) {
10591          pressed = this.items[i] === item;
10592          if ( this.items[i].isPressed() !== pressed ) {
10593              this.items[i].setPressed( pressed );
10594              changed = true;
10595          }
10596      }
10597      if ( changed ) {
10598          this.emit( 'press', item );
10599      }
10600  
10601      return this;
10602  };
10603  
10604  /**
10605   * Choose an item.
10606   *
10607   * Identical to #selectItem, but may vary in subclasses that want to take additional action when
10608   * an item is selected using the keyboard or mouse.
10609   *
10610   * @param {OO.ui.OptionWidget} item Item to choose
10611   * @fires choose
10612   * @chainable
10613   */
10614  OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
10615      this.selectItem( item );
10616      this.emit( 'choose', item );
10617  
10618      return this;
10619  };
10620  
10621  /**
10622   * Get an item relative to another one.
10623   *
10624   * @param {OO.ui.OptionWidget} item Item to start at
10625   * @param {number} direction Direction to move in
10626   * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
10627   */
10628  OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
10629      var inc = direction > 0 ? 1 : -1,
10630          len = this.items.length,
10631          index = item instanceof OO.ui.OptionWidget ?
10632              $.inArray( item, this.items ) : ( inc > 0 ? -1 : 0 ),
10633          stopAt = Math.max( Math.min( index, len - 1 ), 0 ),
10634          i = inc > 0 ?
10635              // Default to 0 instead of -1, if nothing is selected let's start at the beginning
10636              Math.max( index, -1 ) :
10637              // Default to n-1 instead of -1, if nothing is selected let's start at the end
10638              Math.min( index, len );
10639  
10640      while ( true ) {
10641          i = ( i + inc + len ) % len;
10642          item = this.items[i];
10643          if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
10644              return item;
10645          }
10646          // Stop iterating when we've looped all the way around
10647          if ( i === stopAt ) {
10648              break;
10649          }
10650      }
10651      return null;
10652  };
10653  
10654  /**
10655   * Get the next selectable item.
10656   *
10657   * @return {OO.ui.OptionWidget|null} Item, `null` if ther aren't any selectable items
10658   */
10659  OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
10660      var i, len, item;
10661  
10662      for ( i = 0, len = this.items.length; i < len; i++ ) {
10663          item = this.items[i];
10664          if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
10665              return item;
10666          }
10667      }
10668  
10669      return null;
10670  };
10671  
10672  /**
10673   * Add items.
10674   *
10675   * When items are added with the same values as existing items, the existing items will be
10676   * automatically removed before the new items are added.
10677   *
10678   * @param {OO.ui.OptionWidget[]} items Items to add
10679   * @param {number} [index] Index to insert items after
10680   * @fires add
10681   * @chainable
10682   */
10683  OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
10684      var i, len, item, hash,
10685          remove = [];
10686  
10687      for ( i = 0, len = items.length; i < len; i++ ) {
10688          item = items[i];
10689          hash = OO.getHash( item.getData() );
10690          if ( hash in this.hashes ) {
10691              // Remove item with same value
10692              remove.push( this.hashes[hash] );
10693          }
10694          this.hashes[hash] = item;
10695      }
10696      if ( remove.length ) {
10697          this.removeItems( remove );
10698      }
10699  
10700      // Mixin method
10701      OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
10702  
10703      // Always provide an index, even if it was omitted
10704      this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
10705  
10706      return this;
10707  };
10708  
10709  /**
10710   * Remove items.
10711   *
10712   * Items will be detached, not removed, so they can be used later.
10713   *
10714   * @param {OO.ui.OptionWidget[]} items Items to remove
10715   * @fires remove
10716   * @chainable
10717   */
10718  OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
10719      var i, len, item, hash;
10720  
10721      for ( i = 0, len = items.length; i < len; i++ ) {
10722          item = items[i];
10723          hash = OO.getHash( item.getData() );
10724          if ( hash in this.hashes ) {
10725              // Remove existing item
10726              delete this.hashes[hash];
10727          }
10728          if ( item.isSelected() ) {
10729              this.selectItem( null );
10730          }
10731      }
10732  
10733      // Mixin method
10734      OO.ui.GroupWidget.prototype.removeItems.call( this, items );
10735  
10736      this.emit( 'remove', items );
10737  
10738      return this;
10739  };
10740  
10741  /**
10742   * Clear all items.
10743   *
10744   * Items will be detached, not removed, so they can be used later.
10745   *
10746   * @fires remove
10747   * @chainable
10748   */
10749  OO.ui.SelectWidget.prototype.clearItems = function () {
10750      var items = this.items.slice();
10751  
10752      // Clear all items
10753      this.hashes = {};
10754      // Mixin method
10755      OO.ui.GroupWidget.prototype.clearItems.call( this );
10756      this.selectItem( null );
10757  
10758      this.emit( 'remove', items );
10759  
10760      return this;
10761  };
10762  
10763  /**
10764   * Select widget containing button options.
10765   *
10766   * Use together with OO.ui.ButtonOptionWidget.
10767   *
10768   * @class
10769   * @extends OO.ui.SelectWidget
10770   *
10771   * @constructor
10772   * @param {Object} [config] Configuration options
10773   */
10774  OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
10775      // Parent constructor
10776      OO.ui.ButtonSelectWidget.super.call( this, config );
10777  
10778      // Initialization
10779      this.$element.addClass( 'oo-ui-buttonSelectWidget' );
10780  };
10781  
10782  /* Setup */
10783  
10784  OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
10785  
10786  /**
10787   * Overlaid menu of options.
10788   *
10789   * Menus are clipped to the visible viewport. They do not provide a control for opening or closing
10790   * the menu.
10791   *
10792   * Use together with OO.ui.MenuItemWidget.
10793   *
10794   * @class
10795   * @extends OO.ui.SelectWidget
10796   * @mixins OO.ui.ClippableElement
10797   *
10798   * @constructor
10799   * @param {Object} [config] Configuration options
10800   * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
10801   * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
10802   * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
10803   */
10804  OO.ui.MenuWidget = function OoUiMenuWidget( config ) {
10805      // Config intialization
10806      config = config || {};
10807  
10808      // Parent constructor
10809      OO.ui.MenuWidget.super.call( this, config );
10810  
10811      // Mixin constructors
10812      OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
10813  
10814      // Properties
10815      this.flashing = false;
10816      this.visible = false;
10817      this.newItems = null;
10818      this.autoHide = config.autoHide === undefined || !!config.autoHide;
10819      this.$input = config.input ? config.input.$input : null;
10820      this.$widget = config.widget ? config.widget.$element : null;
10821      this.$previousFocus = null;
10822      this.isolated = !config.input;
10823      this.onKeyDownHandler = OO.ui.bind( this.onKeyDown, this );
10824      this.onDocumentMouseDownHandler = OO.ui.bind( this.onDocumentMouseDown, this );
10825  
10826      // Initialization
10827      this.$element
10828          .hide()
10829          .attr( 'role', 'menu' )
10830          .addClass( 'oo-ui-menuWidget' );
10831  };
10832  
10833  /* Setup */
10834  
10835  OO.inheritClass( OO.ui.MenuWidget, OO.ui.SelectWidget );
10836  OO.mixinClass( OO.ui.MenuWidget, OO.ui.ClippableElement );
10837  
10838  /* Methods */
10839  
10840  /**
10841   * Handles document mouse down events.
10842   *
10843   * @param {jQuery.Event} e Key down event
10844   */
10845  OO.ui.MenuWidget.prototype.onDocumentMouseDown = function ( e ) {
10846      if ( !$.contains( this.$element[0], e.target ) && ( !this.$widget || !$.contains( this.$widget[0], e.target ) ) ) {
10847          this.toggle( false );
10848      }
10849  };
10850  
10851  /**
10852   * Handles key down events.
10853   *
10854   * @param {jQuery.Event} e Key down event
10855   */
10856  OO.ui.MenuWidget.prototype.onKeyDown = function ( e ) {
10857      var nextItem,
10858          handled = false,
10859          highlightItem = this.getHighlightedItem();
10860  
10861      if ( !this.isDisabled() && this.isVisible() ) {
10862          if ( !highlightItem ) {
10863              highlightItem = this.getSelectedItem();
10864          }
10865          switch ( e.keyCode ) {
10866              case OO.ui.Keys.ENTER:
10867                  this.chooseItem( highlightItem );
10868                  handled = true;
10869                  break;
10870              case OO.ui.Keys.UP:
10871                  nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
10872                  handled = true;
10873                  break;
10874              case OO.ui.Keys.DOWN:
10875                  nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
10876                  handled = true;
10877                  break;
10878              case OO.ui.Keys.ESCAPE:
10879                  if ( highlightItem ) {
10880                      highlightItem.setHighlighted( false );
10881                  }
10882                  this.toggle( false );
10883                  handled = true;
10884                  break;
10885          }
10886  
10887          if ( nextItem ) {
10888              this.highlightItem( nextItem );
10889              nextItem.scrollElementIntoView();
10890          }
10891  
10892          if ( handled ) {
10893              e.preventDefault();
10894              e.stopPropagation();
10895              return false;
10896          }
10897      }
10898  };
10899  
10900  /**
10901   * Bind key down listener.
10902   */
10903  OO.ui.MenuWidget.prototype.bindKeyDownListener = function () {
10904      if ( this.$input ) {
10905          this.$input.on( 'keydown', this.onKeyDownHandler );
10906      } else {
10907          // Capture menu navigation keys
10908          this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
10909      }
10910  };
10911  
10912  /**
10913   * Unbind key down listener.
10914   */
10915  OO.ui.MenuWidget.prototype.unbindKeyDownListener = function () {
10916      if ( this.$input ) {
10917          this.$input.off( 'keydown' );
10918      } else {
10919          this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
10920      }
10921  };
10922  
10923  /**
10924   * Choose an item.
10925   *
10926   * This will close the menu when done, unlike selectItem which only changes selection.
10927   *
10928   * @param {OO.ui.OptionWidget} item Item to choose
10929   * @chainable
10930   */
10931  OO.ui.MenuWidget.prototype.chooseItem = function ( item ) {
10932      var widget = this;
10933  
10934      // Parent method
10935      OO.ui.MenuWidget.super.prototype.chooseItem.call( this, item );
10936  
10937      if ( item && !this.flashing ) {
10938          this.flashing = true;
10939          item.flash().done( function () {
10940              widget.toggle( false );
10941              widget.flashing = false;
10942          } );
10943      } else {
10944          this.toggle( false );
10945      }
10946  
10947      return this;
10948  };
10949  
10950  /**
10951   * @inheritdoc
10952   */
10953  OO.ui.MenuWidget.prototype.addItems = function ( items, index ) {
10954      var i, len, item;
10955  
10956      // Parent method
10957      OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index );
10958  
10959      // Auto-initialize
10960      if ( !this.newItems ) {
10961          this.newItems = [];
10962      }
10963  
10964      for ( i = 0, len = items.length; i < len; i++ ) {
10965          item = items[i];
10966          if ( this.isVisible() ) {
10967              // Defer fitting label until item has been attached
10968              item.fitLabel();
10969          } else {
10970              this.newItems.push( item );
10971          }
10972      }
10973  
10974      // Reevaluate clipping
10975      this.clip();
10976  
10977      return this;
10978  };
10979  
10980  /**
10981   * @inheritdoc
10982   */
10983  OO.ui.MenuWidget.prototype.removeItems = function ( items ) {
10984      // Parent method
10985      OO.ui.MenuWidget.super.prototype.removeItems.call( this, items );
10986  
10987      // Reevaluate clipping
10988      this.clip();
10989  
10990      return this;
10991  };
10992  
10993  /**
10994   * @inheritdoc
10995   */
10996  OO.ui.MenuWidget.prototype.clearItems = function () {
10997      // Parent method
10998      OO.ui.MenuWidget.super.prototype.clearItems.call( this );
10999  
11000      // Reevaluate clipping
11001      this.clip();
11002  
11003      return this;
11004  };
11005  
11006  /**
11007   * @inheritdoc
11008   */
11009  OO.ui.MenuWidget.prototype.toggle = function ( visible ) {
11010      visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
11011  
11012      var i, len,
11013          change = visible !== this.isVisible();
11014  
11015      // Parent method
11016      OO.ui.MenuWidget.super.prototype.toggle.call( this, visible );
11017  
11018      if ( change ) {
11019          if ( visible ) {
11020              this.bindKeyDownListener();
11021  
11022              // Change focus to enable keyboard navigation
11023              if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
11024                  this.$previousFocus = this.$( ':focus' );
11025                  this.$input[0].focus();
11026              }
11027              if ( this.newItems && this.newItems.length ) {
11028                  for ( i = 0, len = this.newItems.length; i < len; i++ ) {
11029                      this.newItems[i].fitLabel();
11030                  }
11031                  this.newItems = null;
11032              }
11033              this.toggleClipping( true );
11034  
11035              // Auto-hide
11036              if ( this.autoHide ) {
11037                  this.getElementDocument().addEventListener(
11038                      'mousedown', this.onDocumentMouseDownHandler, true
11039                  );
11040              }
11041          } else {
11042              this.unbindKeyDownListener();
11043              if ( this.isolated && this.$previousFocus ) {
11044                  this.$previousFocus[0].focus();
11045                  this.$previousFocus = null;
11046              }
11047              this.getElementDocument().removeEventListener(
11048                  'mousedown', this.onDocumentMouseDownHandler, true
11049              );
11050              this.toggleClipping( false );
11051          }
11052      }
11053  
11054      return this;
11055  };
11056  
11057  /**
11058   * Menu for a text input widget.
11059   *
11060   * This menu is specially designed to be positioned beneath the text input widget. Even if the input
11061   * is in a different frame, the menu's position is automatically calulated and maintained when the
11062   * menu is toggled or the window is resized.
11063   *
11064   * @class
11065   * @extends OO.ui.MenuWidget
11066   *
11067   * @constructor
11068   * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
11069   * @param {Object} [config] Configuration options
11070   * @cfg {jQuery} [$container=input.$element] Element to render menu under
11071   */
11072  OO.ui.TextInputMenuWidget = function OoUiTextInputMenuWidget( input, config ) {
11073      // Parent constructor
11074      OO.ui.TextInputMenuWidget.super.call( this, config );
11075  
11076      // Properties
11077      this.input = input;
11078      this.$container = config.$container || this.input.$element;
11079      this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this );
11080  
11081      // Initialization
11082      this.$element.addClass( 'oo-ui-textInputMenuWidget' );
11083  };
11084  
11085  /* Setup */
11086  
11087  OO.inheritClass( OO.ui.TextInputMenuWidget, OO.ui.MenuWidget );
11088  
11089  /* Methods */
11090  
11091  /**
11092   * Handle window resize event.
11093   *
11094   * @param {jQuery.Event} e Window resize event
11095   */
11096  OO.ui.TextInputMenuWidget.prototype.onWindowResize = function () {
11097      this.position();
11098  };
11099  
11100  /**
11101   * @inheritdoc
11102   */
11103  OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) {
11104      visible = !!visible;
11105  
11106      var change = visible !== this.isVisible();
11107  
11108      if ( change && visible ) {
11109          // Make sure the width is set before the parent method runs.
11110          // After this we have to call this.position(); again to actually
11111          // position ourselves correctly.
11112          this.position();
11113      }
11114  
11115      // Parent method
11116      OO.ui.TextInputMenuWidget.super.prototype.toggle.call( this, visible );
11117  
11118      if ( change ) {
11119          if ( this.isVisible() ) {
11120              this.position();
11121              this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
11122          } else {
11123              this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
11124          }
11125      }
11126  
11127      return this;
11128  };
11129  
11130  /**
11131   * Position the menu.
11132   *
11133   * @chainable
11134   */
11135  OO.ui.TextInputMenuWidget.prototype.position = function () {
11136      var frameOffset,
11137          $container = this.$container,
11138          dimensions = $container.offset();
11139  
11140      // Position under input
11141      dimensions.top += $container.height();
11142  
11143      // Compensate for frame position if in a differnt frame
11144      if ( this.input.$.$iframe && this.input.$.context !== this.$element[0].ownerDocument ) {
11145          frameOffset = OO.ui.Element.getRelativePosition(
11146              this.input.$.$iframe, this.$element.offsetParent()
11147          );
11148          dimensions.left += frameOffset.left;
11149          dimensions.top += frameOffset.top;
11150      } else {
11151          // Fix for RTL (for some reason, no need to fix if the frameoffset is set)
11152          if ( this.$element.css( 'direction' ) === 'rtl' ) {
11153              dimensions.right = this.$element.parent().position().left -
11154                  $container.width() - dimensions.left;
11155              // Erase the value for 'left':
11156              delete dimensions.left;
11157          }
11158      }
11159      this.$element.css( dimensions );
11160      this.setIdealSize( $container.width() );
11161      // We updated the position, so re-evaluate the clipping state
11162      this.clip();
11163  
11164      return this;
11165  };
11166  
11167  /**
11168   * Structured list of items.
11169   *
11170   * Use with OO.ui.OutlineItemWidget.
11171   *
11172   * @class
11173   * @extends OO.ui.SelectWidget
11174   *
11175   * @constructor
11176   * @param {Object} [config] Configuration options
11177   */
11178  OO.ui.OutlineWidget = function OoUiOutlineWidget( config ) {
11179      // Config intialization
11180      config = config || {};
11181  
11182      // Parent constructor
11183      OO.ui.OutlineWidget.super.call( this, config );
11184  
11185      // Initialization
11186      this.$element.addClass( 'oo-ui-outlineWidget' );
11187  };
11188  
11189  /* Setup */
11190  
11191  OO.inheritClass( OO.ui.OutlineWidget, OO.ui.SelectWidget );
11192  
11193  /**
11194   * Switch that slides on and off.
11195   *
11196   * @class
11197   * @extends OO.ui.Widget
11198   * @mixins OO.ui.ToggleWidget
11199   *
11200   * @constructor
11201   * @param {Object} [config] Configuration options
11202   * @cfg {boolean} [value=false] Initial value
11203   */
11204  OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
11205      // Parent constructor
11206      OO.ui.ToggleSwitchWidget.super.call( this, config );
11207  
11208      // Mixin constructors
11209      OO.ui.ToggleWidget.call( this, config );
11210  
11211      // Properties
11212      this.dragging = false;
11213      this.dragStart = null;
11214      this.sliding = false;
11215      this.$glow = this.$( '<span>' );
11216      this.$grip = this.$( '<span>' );
11217  
11218      // Events
11219      this.$element.on( 'click', OO.ui.bind( this.onClick, this ) );
11220  
11221      // Initialization
11222      this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
11223      this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
11224      this.$element
11225          .addClass( 'oo-ui-toggleSwitchWidget' )
11226          .append( this.$glow, this.$grip );
11227  };
11228  
11229  /* Setup */
11230  
11231  OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
11232  OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
11233  
11234  /* Methods */
11235  
11236  /**
11237   * Handle mouse down events.
11238   *
11239   * @param {jQuery.Event} e Mouse down event
11240   */
11241  OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
11242      if ( !this.isDisabled() && e.which === 1 ) {
11243          this.setValue( !this.value );
11244      }
11245  };
11246  
11247  }( OO ) );


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1