Update OOUI to v0.31.4
[lhc/web/wiklou.git] / resources / lib / ooui / oojs-ui-core.js
index c32844c..4eea3bd 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOUI v0.31.0
+ * OOUI v0.31.4
  * https://www.mediawiki.org/wiki/OOUI
  *
  * Copyright 2011–2019 OOUI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2019-03-14T00:52:20Z
+ * Date: 2019-04-16T23:14:51Z
  */
 ( function ( OO ) {
 
@@ -295,7 +295,7 @@ OO.ui.throttle = function ( func, wait ) {
                previous = 0,
                run = function () {
                        timeout = null;
-                       previous = OO.ui.now();
+                       previous = Date.now();
                        func.apply( context, args );
                };
        return function () {
@@ -304,7 +304,7 @@ OO.ui.throttle = function ( func, wait ) {
                // period. If it's less, run the function immediately. If it's more,
                // set a timeout for the remaining time -- but don't replace an
                // existing timeout, since that'd indefinitely prolong the wait.
-               var remaining = wait - ( OO.ui.now() - previous );
+               var remaining = wait - ( Date.now() - previous );
                context = this;
                args = arguments;
                if ( remaining <= 0 ) {
@@ -323,10 +323,12 @@ OO.ui.throttle = function ( func, wait ) {
 /**
  * A (possibly faster) way to get the current timestamp as an integer.
  *
+ * @deprecated Since 0.31.1; use `Date.now()` instead.
  * @return {number} Current timestamp, in milliseconds since the Unix epoch
  */
-OO.ui.now = Date.now || function () {
-       return new Date().getTime();
+OO.ui.now = function () {
+       OO.ui.warnDeprecation( 'OO.ui.now() is deprecated, use Date.now() instead' );
+       return Date.now();
 };
 
 /**
@@ -345,124 +347,74 @@ OO.ui.infuse = function ( idOrNode, config ) {
        return OO.ui.Element.static.infuse( idOrNode, config );
 };
 
-( function () {
-       /**
-        * Message store for the default implementation of OO.ui.msg.
-        *
-        * Environments that provide a localization system should not use this, but should override
-        * OO.ui.msg altogether.
-        *
-        * @private
-        */
-       var messages = {
-               // Tool tip for a button that moves items in a list down one place
-               'ooui-outline-control-move-down': 'Move item down',
-               // Tool tip for a button that moves items in a list up one place
-               'ooui-outline-control-move-up': 'Move item up',
-               // Tool tip for a button that removes items from a list
-               'ooui-outline-control-remove': 'Remove item',
-               // Label for the toolbar group that contains a list of all other available tools
-               'ooui-toolbar-more': 'More',
-               // Label for the fake tool that expands the full list of tools in a toolbar group
-               'ooui-toolgroup-expand': 'More',
-               // Label for the fake tool that collapses the full list of tools in a toolbar group
-               'ooui-toolgroup-collapse': 'Fewer',
-               // Default label for the tooltip for the button that removes a tag item
-               'ooui-item-remove': 'Remove',
-               // Default label for the accept button of a confirmation dialog
-               'ooui-dialog-message-accept': 'OK',
-               // Default label for the reject button of a confirmation dialog
-               'ooui-dialog-message-reject': 'Cancel',
-               // Title for process dialog error description
-               'ooui-dialog-process-error': 'Something went wrong',
-               // Label for process dialog dismiss error button, visible when describing errors
-               'ooui-dialog-process-dismiss': 'Dismiss',
-               // Label for process dialog retry action button, visible when describing only recoverable
-               // errors
-               'ooui-dialog-process-retry': 'Try again',
-               // Label for process dialog retry action button, visible when describing only warnings
-               'ooui-dialog-process-continue': 'Continue',
-               // Label for button in combobox input that triggers its dropdown
-               'ooui-combobox-button-label': 'Dropdown for combobox',
-               // Label for the file selection widget's select file button
-               'ooui-selectfile-button-select': 'Select a file',
-               // Label for the file selection widget if file selection is not supported
-               'ooui-selectfile-not-supported': 'File selection is not supported',
-               // Label for the file selection widget when no file is currently selected
-               'ooui-selectfile-placeholder': 'No file is selected',
-               // Label for the file selection widget's drop target
-               'ooui-selectfile-dragdrop-placeholder': 'Drop file here',
-               // Label for the help icon attached to a form field
-               'ooui-field-help': 'Help'
-       };
-
-       /**
-        * Get a localized message.
-        *
-        * After the message key, message parameters may optionally be passed. In the default
-        * implementation, any occurrences of $1 are replaced with the first parameter, $2 with the
-        * second parameter, etc.
-        * Alternative implementations of OO.ui.msg may use any substitution system they like, as long
-        * as they support unnamed, ordered message parameters.
-        *
-        * In environments that provide a localization system, this function should be overridden to
-        * return the message translated in the user's language. The default implementation always
-        * returns English messages. An example of doing this with
-        * [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) follows.
-        *
-        *     @example
-        *     var i, iLen, button,
-        *         messagePath = 'oojs-ui/dist/i18n/',
-        *         languages = [ $.i18n().locale, 'ur', 'en' ],
-        *         languageMap = {};
-        *
-        *     for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
-        *         languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
-        *     }
-        *
-        *     $.i18n().load( languageMap ).done( function() {
-        *         // Replace the built-in `msg` only once we've loaded the internationalization.
-        *         // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
-        *         // you put off creating any widgets until this promise is complete, no English
-        *         // will be displayed.
-        *         OO.ui.msg = $.i18n;
-        *
-        *         // A button displaying "OK" in the default locale
-        *         button = new OO.ui.ButtonWidget( {
-        *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
-        *             icon: 'check'
-        *         } );
-        *         $( document.body ).append( button.$element );
-        *
-        *         // A button displaying "OK" in Urdu
-        *         $.i18n().locale = 'ur';
-        *         button = new OO.ui.ButtonWidget( {
-        *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
-        *             icon: 'check'
-        *         } );
-        *         $( document.body ).append( button.$element );
-        *     } );
-        *
-        * @param {string} key Message key
-        * @param {...Mixed} [params] Message parameters
-        * @return {string} Translated message with parameters substituted
-        */
-       OO.ui.msg = function ( key ) {
-               var message = messages[ key ],
-                       params = Array.prototype.slice.call( arguments, 1 );
-               if ( typeof message === 'string' ) {
-                       // Perform $1 substitution
-                       message = message.replace( /\$(\d+)/g, function ( unused, n ) {
-                               var i = parseInt( n, 10 );
-                               return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
-                       } );
-               } else {
-                       // Return placeholder if message not found
-                       message = '[' + key + ']';
-               }
-               return message;
-       };
-}() );
+/**
+ * Get a localized message.
+ *
+ * After the message key, message parameters may optionally be passed. In the default
+ * implementation, any occurrences of $1 are replaced with the first parameter, $2 with the
+ * second parameter, etc.
+ * Alternative implementations of OO.ui.msg may use any substitution system they like, as long
+ * as they support unnamed, ordered message parameters.
+ *
+ * In environments that provide a localization system, this function should be overridden to
+ * return the message translated in the user's language. The default implementation always
+ * returns English messages. An example of doing this with
+ * [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) follows.
+ *
+ *     @example
+ *     var i, iLen, button,
+ *         messagePath = 'oojs-ui/dist/i18n/',
+ *         languages = [ $.i18n().locale, 'ur', 'en' ],
+ *         languageMap = {};
+ *
+ *     for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
+ *         languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
+ *     }
+ *
+ *     $.i18n().load( languageMap ).done( function() {
+ *         // Replace the built-in `msg` only once we've loaded the internationalization.
+ *         // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
+ *         // you put off creating any widgets until this promise is complete, no English
+ *         // will be displayed.
+ *         OO.ui.msg = $.i18n;
+ *
+ *         // A button displaying "OK" in the default locale
+ *         button = new OO.ui.ButtonWidget( {
+ *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
+ *             icon: 'check'
+ *         } );
+ *         $( document.body ).append( button.$element );
+ *
+ *         // A button displaying "OK" in Urdu
+ *         $.i18n().locale = 'ur';
+ *         button = new OO.ui.ButtonWidget( {
+ *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
+ *             icon: 'check'
+ *         } );
+ *         $( document.body ).append( button.$element );
+ *     } );
+ *
+ * @param {string} key Message key
+ * @param {...Mixed} [params] Message parameters
+ * @return {string} Translated message with parameters substituted
+ */
+OO.ui.msg = function ( key ) {
+       // `OO.ui.msg.messages` is defined in code generated during the build process
+       var messages = OO.ui.msg.messages,
+               message = messages[ key ],
+               params = Array.prototype.slice.call( arguments, 1 );
+       if ( typeof message === 'string' ) {
+               // Perform $1 substitution
+               message = message.replace( /\$(\d+)/g, function ( unused, n ) {
+                       var i = parseInt( n, 10 );
+                       return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
+               } );
+       } else {
+               // Return placeholder if message not found
+               message = '[' + key + ']';
+       }
+       return message;
+};
 
 /**
  * Package a message and arguments for deferred resolution.
@@ -581,6 +533,36 @@ OO.ui.getDefaultOverlay = function () {
        return OO.ui.$defaultOverlay;
 };
 
+/**
+ * Message store for the default implementation of OO.ui.msg.
+ *
+ * Environments that provide a localization system should not use this, but should override
+ * OO.ui.msg altogether.
+ *
+ * @private
+ */
+OO.ui.msg.messages = {
+       "ooui-outline-control-move-down": "Move item down",
+       "ooui-outline-control-move-up": "Move item up",
+       "ooui-outline-control-remove": "Remove item",
+       "ooui-toolbar-more": "More",
+       "ooui-toolgroup-expand": "More",
+       "ooui-toolgroup-collapse": "Fewer",
+       "ooui-item-remove": "Remove",
+       "ooui-dialog-message-accept": "OK",
+       "ooui-dialog-message-reject": "Cancel",
+       "ooui-dialog-process-error": "Something went wrong",
+       "ooui-dialog-process-dismiss": "Dismiss",
+       "ooui-dialog-process-retry": "Try again",
+       "ooui-dialog-process-continue": "Continue",
+       "ooui-combobox-button-label": "Dropdown for combobox",
+       "ooui-selectfile-button-select": "Select a file",
+       "ooui-selectfile-not-supported": "File selection is not supported",
+       "ooui-selectfile-placeholder": "No file is selected",
+       "ooui-selectfile-dragdrop-placeholder": "Drop file here",
+       "ooui-field-help": "Help"
+};
+
 /*!
  * Mixin namespace.
  */
@@ -1301,75 +1283,107 @@ OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension )
  * Scroll element into view.
  *
  * @static
- * @param {HTMLElement} el Element to scroll into view
+ * @param {HTMLElement|Object} elOrPosition Element to scroll into view
  * @param {Object} [config] Configuration options
+ * @param {string} [config.animate=true] Animate to the new scroll offset.
  * @param {string} [config.duration='fast'] jQuery animation duration value
  * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
  *  to scroll in both directions
+ * @param {Object} [config.padding] Additional padding on the container to scroll past.
+ *  Object containing any of 'top', 'bottom', 'left', or 'right' as numbers.
+ * @param {Object} [config.scrollContainer] Scroll container. Defaults to
+ *  getClosestScrollableContainer of the element.
  * @return {jQuery.Promise} Promise which resolves when the scroll is complete
  */
-OO.ui.Element.static.scrollIntoView = function ( el, config ) {
-       var position, animations, container, $container, elementDimensions, containerDimensions,
-               $window,
+OO.ui.Element.static.scrollIntoView = function ( elOrPosition, config ) {
+       var position, animations, container, $container, elementPosition, containerDimensions,
+               $window, padding, animate, method,
                deferred = $.Deferred();
 
        // Configuration initialization
        config = config || {};
 
+       padding = $.extend( {
+               top: 0,
+               bottom: 0,
+               left: 0,
+               right: 0
+       }, config.padding );
+
+       animate = config.animate !== false;
+
        animations = {};
-       container = this.getClosestScrollableContainer( el, config.direction );
+       elementPosition = elOrPosition instanceof HTMLElement ?
+               this.getDimensions( elOrPosition ).rect :
+               elOrPosition;
+       container = config.scrollContainer || (
+               elOrPosition instanceof HTMLElement ?
+                       this.getClosestScrollableContainer( elOrPosition, config.direction ) :
+                       // No scrollContainer or element
+                       this.getClosestScrollableContainer( document.body )
+       );
        $container = $( container );
-       elementDimensions = this.getDimensions( el );
        containerDimensions = this.getDimensions( container );
-       $window = $( this.getWindow( el ) );
+       $window = $( this.getWindow( container ) );
 
        // Compute the element's position relative to the container
        if ( $container.is( 'html, body' ) ) {
                // If the scrollable container is the root, this is easy
                position = {
-                       top: elementDimensions.rect.top,
-                       bottom: $window.innerHeight() - elementDimensions.rect.bottom,
-                       left: elementDimensions.rect.left,
-                       right: $window.innerWidth() - elementDimensions.rect.right
+                       top: elementPosition.top,
+                       bottom: $window.innerHeight() - elementPosition.bottom,
+                       left: elementPosition.left,
+                       right: $window.innerWidth() - elementPosition.right
                };
        } else {
                // Otherwise, we have to subtract el's coordinates from container's coordinates
                position = {
-                       top: elementDimensions.rect.top -
+                       top: elementPosition.top -
                                ( containerDimensions.rect.top + containerDimensions.borders.top ),
                        bottom: containerDimensions.rect.bottom - containerDimensions.borders.bottom -
-                               containerDimensions.scrollbar.bottom - elementDimensions.rect.bottom,
-                       left: elementDimensions.rect.left -
+                               containerDimensions.scrollbar.bottom - elementPosition.bottom,
+                       left: elementPosition.left -
                                ( containerDimensions.rect.left + containerDimensions.borders.left ),
                        right: containerDimensions.rect.right - containerDimensions.borders.right -
-                               containerDimensions.scrollbar.right - elementDimensions.rect.right
+                               containerDimensions.scrollbar.right - elementPosition.right
                };
        }
 
        if ( !config.direction || config.direction === 'y' ) {
-               if ( position.top < 0 ) {
-                       animations.scrollTop = containerDimensions.scroll.top + position.top;
-               } else if ( position.top > 0 && position.bottom < 0 ) {
+               if ( position.top < padding.top ) {
+                       animations.scrollTop = containerDimensions.scroll.top + position.top - padding.top;
+               } else if ( position.bottom < padding.bottom ) {
                        animations.scrollTop = containerDimensions.scroll.top +
-                               Math.min( position.top, -position.bottom );
+                               // Scroll the bottom into view, but not at the expense
+                               // of scrolling the top out of view
+                               Math.min( position.top - padding.top, -position.bottom + padding.bottom );
                }
        }
        if ( !config.direction || config.direction === 'x' ) {
-               if ( position.left < 0 ) {
-                       animations.scrollLeft = containerDimensions.scroll.left + position.left;
-               } else if ( position.left > 0 && position.right < 0 ) {
+               if ( position.left < padding.left ) {
+                       animations.scrollLeft = containerDimensions.scroll.left + position.left - padding.left;
+               } else if ( position.right < padding.right ) {
                        animations.scrollLeft = containerDimensions.scroll.left +
-                               Math.min( position.left, -position.right );
+                               // Scroll the right into view, but not at the expense
+                               // of scrolling the left out of view
+                               Math.min( position.left - padding.left, -position.right + padding.right );
                }
        }
        if ( !$.isEmptyObject( animations ) ) {
-               // eslint-disable-next-line no-jquery/no-animate
-               $container.stop( true ).animate( animations, config.duration === undefined ?
-                       'fast' : config.duration );
-               $container.queue( function ( next ) {
+               if ( animate ) {
+                       // eslint-disable-next-line no-jquery/no-animate
+                       $container.stop( true ).animate( animations, config.duration === undefined ? 'fast' : config.duration );
+                       $container.queue( function ( next ) {
+                               deferred.resolve();
+                               next();
+                       } );
+               } else {
+                       $container.stop( true );
+                       for ( method in animations ) {
+                               $container[ method ]( animations[ method ] );
+                       }
                        deferred.resolve();
-                       next();
-               } );
+               }
        } else {
                deferred.resolve();
        }
@@ -2118,7 +2132,7 @@ OO.ui.mixin.TabIndexedElement.prototype.getInputId = function () {
 OO.ui.mixin.TabIndexedElement.prototype.isLabelableNode = function ( $node ) {
        var
                labelableTags = [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ],
-               tagName = $node.prop( 'tagName' ).toLowerCase();
+               tagName = ( $node.prop( 'tagName' ) || '' ).toLowerCase();
 
        if ( tagName === 'input' && $node.attr( 'type' ) !== 'hidden' ) {
                return true;
@@ -3250,10 +3264,14 @@ OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
        this.$flagged = null;
 
        // Initialization
-       this.setFlags( config.flags );
+       this.setFlags( config.flags || this.constructor.static.flags );
        this.setFlaggedElement( config.$flagged || this.$element );
 };
 
+/* Setup */
+
+OO.initClass( OO.ui.mixin.FlaggedElement );
+
 /* Events */
 
 /**
@@ -3266,6 +3284,17 @@ OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
  * that the flag was added, `false` that the flag was removed.
  */
 
+/* Static Properties */
+
+/**
+ * Initial value to pass to setFlags if no value is provided in config.
+ *
+ * @static
+ * @inheritable
+ * @property {string|string[]|Object.<string, boolean>}
+ */
+OO.ui.mixin.FlaggedElement.static.flags = null;
+
 /* Methods */
 
 /**
@@ -3456,6 +3485,9 @@ OO.initClass( OO.ui.mixin.TitledElement );
  * The title text, a function that returns text, or `null` for no title. The value of the static
  * property is overridden if the #title config option is used.
  *
+ * If the element has a default title (e.g. `<input type=file>`), `null` will allow that title to be
+ * shown. Use empty string to suppress it.
+ *
  * @static
  * @inheritable
  * @property {string|Function|null}
@@ -3480,9 +3512,7 @@ OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
        }
 
        this.$titled = $titled;
-       if ( this.title ) {
-               this.updateTitle();
-       }
+       this.updateTitle();
 };
 
 /**
@@ -3495,7 +3525,7 @@ OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
  */
 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
        title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
-       title = ( typeof title === 'string' && title.length ) ? title : null;
+       title = typeof title === 'string' ? title : null;
 
        if ( this.title !== title ) {
                this.title = title;
@@ -6420,6 +6450,7 @@ OO.ui.OptionWidget.prototype.getMatchText = function () {
  *  Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
  *  the [OOUI documentation on MediaWiki] [2] for examples.
  *  [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options
+ * @cfg {boolean} [multiselect] Allow for multiple selections
  */
 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        // Configuration initialization
@@ -6436,6 +6467,7 @@ OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
        // Properties
        this.pressed = false;
        this.selecting = null;
+       this.multiselect = !!config.multiselect;
        this.onDocumentMouseUpHandler = this.onDocumentMouseUp.bind( this );
        this.onDocumentMouseMoveHandler = this.onDocumentMouseMove.bind( this );
        this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
@@ -6496,13 +6528,16 @@ OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
  * A `select` event is emitted when the selection is modified programmatically with the #selectItem
  * method.
  *
- * @param {OO.ui.OptionWidget|null} item Selected item
+ * @param {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} items Currently selected items
  */
 
 /**
  * @event choose
+ *
  * A `choose` event is emitted when an item is chosen with the #chooseItem method.
+ *
  * @param {OO.ui.OptionWidget} item Chosen item
+ * @param {boolean} selected Item is selected
  */
 
 /**
@@ -6699,12 +6734,13 @@ OO.ui.SelectWidget.prototype.onMouseLeave = function () {
 OO.ui.SelectWidget.prototype.onDocumentKeyDown = function ( e ) {
        var nextItem,
                handled = false,
-               currentItem = this.findHighlightedItem() || this.findSelectedItem();
+               currentItem = this.findHighlightedItem(),
+               firstItem = this.getItems()[ 0 ];
 
        if ( !this.isDisabled() && this.isVisible() ) {
                switch ( e.keyCode ) {
                        case OO.ui.Keys.ENTER:
-                               if ( currentItem && currentItem.constructor.static.highlightable ) {
+                               if ( currentItem ) {
                                        // Was only highlighted, now let's select it. No-op if already selected.
                                        this.chooseItem( currentItem );
                                        handled = true;
@@ -6713,18 +6749,20 @@ OO.ui.SelectWidget.prototype.onDocumentKeyDown = function ( e ) {
                        case OO.ui.Keys.UP:
                        case OO.ui.Keys.LEFT:
                                this.clearKeyPressBuffer();
-                               nextItem = this.findRelativeSelectableItem( currentItem, -1 );
+                               nextItem = currentItem ?
+                                       this.findRelativeSelectableItem( currentItem, -1 ) : firstItem;
                                handled = true;
                                break;
                        case OO.ui.Keys.DOWN:
                        case OO.ui.Keys.RIGHT:
                                this.clearKeyPressBuffer();
-                               nextItem = this.findRelativeSelectableItem( currentItem, 1 );
+                               nextItem = currentItem ?
+                                       this.findRelativeSelectableItem( currentItem, 1 ) : firstItem;
                                handled = true;
                                break;
                        case OO.ui.Keys.ESCAPE:
                        case OO.ui.Keys.TAB:
-                               if ( currentItem && currentItem.constructor.static.highlightable ) {
+                               if ( currentItem ) {
                                        currentItem.setHighlighted( false );
                                }
                                this.unbindDocumentKeyDownListener();
@@ -6944,20 +6982,36 @@ OO.ui.SelectWidget.prototype.findTargetItem = function ( e ) {
        return $option.data( 'oo-ui-optionWidget' ) || null;
 };
 
+/**
+ * Find all selected items, if there are any. If the widget allows for multiselect
+ * it will return an array of selected options. If the widget doesn't allow for
+ * multiselect, it will return the selected option or null if no item is selected.
+ *
+ * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
+ *  then return an array of selected items (or empty array),
+ *  if the widget is not multiselect, return a single selected item, or `null`
+ *  if no item is selected
+ */
+OO.ui.SelectWidget.prototype.findSelectedItems = function () {
+       var selected = this.items.filter( function ( item ) {
+               return item.isSelected();
+       } );
+
+       return this.multiselect ?
+               selected :
+               selected[ 0 ] || null;
+};
+
 /**
  * Find selected item.
  *
- * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
+ * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect
+ *  then return an array of selected items (or empty array),
+ *  if the widget is not multiselect, return a single selected item, or `null`
+ *  if no item is selected
  */
 OO.ui.SelectWidget.prototype.findSelectedItem = function () {
-       var i, len;
-
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               if ( this.items[ i ].isSelected() ) {
-                       return this.items[ i ];
-               }
-       }
-       return null;
+       return this.findSelectedItems();
 };
 
 /**
@@ -7104,6 +7158,30 @@ OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
        return this.selectItem( itemFromData );
 };
 
+/**
+ * Programmatically unselect an option by its reference. If the widget
+ * allows for multiple selections, there may be other items still selected;
+ * otherwise, no items will be selected.
+ * If no item is given, all selected items will be unselected.
+ *
+ * @param {OO.ui.OptionWidget} [item] Item to unselect
+ * @fires select
+ * @chainable
+ * @return {OO.ui.Widget} The widget, for chaining
+ */
+OO.ui.SelectWidget.prototype.unselectItem = function ( item ) {
+       if ( item ) {
+               item.setSelected( false );
+       } else {
+               this.items.forEach( function ( item ) {
+                       item.setSelected( false );
+               } );
+       }
+
+       this.emit( 'select', this.findSelectedItems() );
+       return this;
+};
+
 /**
  * Programmatically select an option by its reference. If the `item` parameter is omitted,
  * all options will be deselected.
@@ -7117,14 +7195,20 @@ OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
        var i, len, selected,
                changed = false;
 
-       for ( i = 0, len = this.items.length; i < len; i++ ) {
-               selected = this.items[ i ] === item;
-               if ( this.items[ i ].isSelected() !== selected ) {
-                       this.items[ i ].setSelected( selected );
-                       changed = true;
+       if ( this.multiselect && item ) {
+               // Select the item directly
+               item.setSelected( true );
+       } else {
+               for ( i = 0, len = this.items.length; i < len; i++ ) {
+                       selected = this.items[ i ] === item;
+                       if ( this.items[ i ].isSelected() !== selected ) {
+                               this.items[ i ].setSelected( selected );
+                               changed = true;
+                       }
                }
        }
        if ( changed ) {
+               // TODO: When should a non-highlightable element be selected?
                if ( item && !item.constructor.static.highlightable ) {
                        if ( item ) {
                                this.$focusOwner.attr( 'aria-activedescendant', item.getElementId() );
@@ -7132,7 +7216,7 @@ OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
                                this.$focusOwner.removeAttr( 'aria-activedescendant' );
                        }
                }
-               this.emit( 'select', item );
+               this.emit( 'select', this.findSelectedItems() );
        }
 
        return this;
@@ -7185,8 +7269,13 @@ OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
  */
 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
        if ( item ) {
-               this.selectItem( item );
-               this.emit( 'choose', item );
+               if ( this.multiselect && item.isSelected() ) {
+                       this.unselectItem( item );
+               } else {
+                       this.selectItem( item );
+               }
+
+               this.emit( 'choose', item, item.isSelected() );
        }
 
        return this;
@@ -7470,6 +7559,7 @@ OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
        this.$element
                .addClass( 'oo-ui-menuSectionOptionWidget' )
                .removeAttr( 'role aria-selected' );
+       this.selected = false;
 };
 
 /* Setup */
@@ -7654,7 +7744,7 @@ OO.ui.MenuSelectWidget.prototype.onDocumentKeyDown = function ( e ) {
                                break;
                        case OO.ui.Keys.ESCAPE:
                        case OO.ui.Keys.TAB:
-                               if ( currentItem ) {
+                               if ( currentItem && !this.multiselect ) {
                                        currentItem.setHighlighted( false );
                                }
                                this.toggle( false );
@@ -7711,10 +7801,6 @@ OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
                        section.toggle( showAll || !sectionEmpty );
                }
 
-               if ( anyVisible && this.items.length && !exactMatch ) {
-                       this.scrollItemIntoView( this.items[ 0 ] );
-               }
-
                if ( !anyVisible ) {
                        this.highlightItem( null );
                }
@@ -7871,7 +7957,7 @@ OO.ui.MenuSelectWidget.prototype.clearItems = function () {
  * @inheritdoc
  */
 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
-       var change, originalHeight, flippedHeight;
+       var change, originalHeight, flippedHeight, selectedItem;
 
        visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
        change = visible !== this.isVisible();
@@ -7936,9 +8022,13 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
 
                        this.$focusOwner.attr( 'aria-expanded', 'true' );
 
-                       if ( this.findSelectedItem() ) {
-                               this.$focusOwner.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() );
-                               this.findSelectedItem().scrollElementIntoView( { duration: 0 } );
+                       selectedItem = this.findSelectedItem();
+                       if ( !this.multiselect && selectedItem ) {
+                               // TODO: Verify if this is even needed; This is already done on highlight changes
+                               // in SelectWidget#highlightItem, so we should just need to highlight the item
+                               // we need to highlight here and not bother with attr or checking selections.
+                               this.$focusOwner.attr( 'aria-activedescendant', selectedItem.getElementId() );
+                               selectedItem.scrollElementIntoView( { duration: 0 } );
                        }
 
                        // Auto-hide
@@ -7961,6 +8051,13 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
        return this;
 };
 
+/**
+ * Scroll to the top of the menu
+ */
+OO.ui.MenuSelectWidget.prototype.scrollToTop = function () {
+       this.$element.scrollTop( 0 );
+};
+
 /**
  * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
  * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
@@ -9400,6 +9497,7 @@ OO.ui.ButtonInputWidget.prototype.getInputId = function () {
  * @param {Object} [config] Configuration options
  * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is
  *  not selected.
+ * @cfg {boolean} [indeterminate=false] Whether the checkbox is in the indeterminate state.
  */
 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
        // Configuration initialization
@@ -9420,12 +9518,24 @@ OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
                // Required for pretty styling in WikimediaUI theme
                .append( this.checkIcon.$element );
        this.setSelected( config.selected !== undefined ? config.selected : false );
+       this.setIndeterminate( config.indeterminate !== undefined ? config.indeterminate : false );
 };
 
 /* Setup */
 
 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
 
+/* Events */
+
+/**
+ * @event change
+ *
+ * A change event is emitted when the state of the input changes.
+ *
+ * @param {boolean} selected
+ * @param {boolean} indeterminate
+ */
+
 /* Static Properties */
 
 /**
@@ -9464,6 +9574,7 @@ OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
                // Allow the stack to clear so the value will be updated
                setTimeout( function () {
                        widget.setSelected( widget.$input.prop( 'checked' ) );
+                       widget.setIndeterminate( widget.$input.prop( 'indeterminate' ) );
                } );
        }
 };
@@ -9471,16 +9582,20 @@ OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
 /**
  * Set selection state of this checkbox.
  *
- * @param {boolean} state `true` for selected
+ * @param {boolean} state Selected state
+ * @param {boolean} internal Used for internal calls to suppress events
  * @chainable
- * @return {OO.ui.Widget} The widget, for chaining
+ * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
  */
-OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
+OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state, internal ) {
        state = !!state;
        if ( this.selected !== state ) {
                this.selected = state;
                this.$input.prop( 'checked', this.selected );
-               this.emit( 'change', this.selected );
+               if ( !internal ) {
+                       this.setIndeterminate( false, true );
+                       this.emit( 'change', this.selected, this.indeterminate );
+               }
        }
        // The first time that the selection state is set (probably while constructing the widget),
        // remember it in defaultSelected. This property can be later used to check whether
@@ -9507,6 +9622,42 @@ OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
        return this.selected;
 };
 
+/**
+ * Set indeterminate state of this checkbox.
+ *
+ * @param {boolean} state Indeterminate state
+ * @param {boolean} internal Used for internal calls to suppress events
+ * @chainable
+ * @return {OO.ui.CheckboxInputWidget} The widget, for chaining
+ */
+OO.ui.CheckboxInputWidget.prototype.setIndeterminate = function ( state, internal ) {
+       state = !!state;
+       if ( this.indeterminate !== state ) {
+               this.indeterminate = state;
+               this.$input.prop( 'indeterminate', this.indeterminate );
+               if ( !internal ) {
+                       this.setSelected( false, true );
+                       this.emit( 'change', this.selected, this.indeterminate );
+               }
+       }
+       return this;
+};
+
+/**
+ * Check if this checkbox is selected.
+ *
+ * @return {boolean} Checkbox is selected
+ */
+OO.ui.CheckboxInputWidget.prototype.isIndeterminate = function () {
+       // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
+       // it, and we won't know unless they're kind enough to trigger a 'change' event.
+       var indeterminate = this.$input.prop( 'indeterminate' );
+       if ( this.indeterminate !== indeterminate ) {
+               this.setIndeterminate( indeterminate );
+       }
+       return this.indeterminate;
+};
+
 /**
  * @inheritdoc
  */
@@ -10396,7 +10547,7 @@ OO.ui.CheckboxMultiselectInputWidget.prototype.focus = function () {
  *     // A TextInputWidget.
  *     var textInput = new OO.ui.TextInputWidget( {
  *         value: 'Text input'
- *     } )
+ *     } );
  *     $( document.body ).append( textInput.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
@@ -11116,6 +11267,7 @@ OO.ui.SearchInputWidget = function OoUiSearchInputWidget( config ) {
        this.connect( this, {
                change: 'onChange'
        } );
+       this.$indicator.on( 'click', this.onIndicatorClick.bind( this ) );
 
        // Initialization
        this.updateSearchIndicator();
@@ -11139,9 +11291,12 @@ OO.ui.SearchInputWidget.prototype.getSaneType = function () {
 };
 
 /**
- * @inheritdoc
+ * Handle click events on the indicator
+ *
+ * @param {jQuery.Event} e Click event
+ * @return {boolean}
  */
-OO.ui.SearchInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
+OO.ui.SearchInputWidget.prototype.onIndicatorClick = function ( e ) {
        if ( e.which === OO.ui.MouseButtons.LEFT ) {
                // Clear the text field
                this.setValue( '' );
@@ -11204,8 +11359,8 @@ OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) {
  *     // A MultilineTextInputWidget.
  *     var multilineTextInput = new OO.ui.MultilineTextInputWidget( {
  *         value: 'Text input on multiple lines'
- *     } )
- *     $( 'body' ).append( multilineTextInput.$element );
+ *     } );
+ *     $( document.body ).append( multilineTextInput.$element );
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget
  *
@@ -12359,21 +12514,21 @@ OO.ui.FieldsetLayout.static.tagName = 'fieldset';
  * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs
  *
  *     @example
- *     // Example of a form layout that wraps a fieldset layout
+ *     // Example of a form layout that wraps a fieldset layout.
  *     var input1 = new OO.ui.TextInputWidget( {
- *         placeholder: 'Username'
- *     } );
- *     var input2 = new OO.ui.TextInputWidget( {
- *         placeholder: 'Password',
- *         type: 'password'
- *     } );
- *     var submit = new OO.ui.ButtonInputWidget( {
- *         label: 'Submit'
- *     } );
+ *             placeholder: 'Username'
+ *         } ),
+ *         input2 = new OO.ui.TextInputWidget( {
+ *             placeholder: 'Password',
+ *             type: 'password'
+ *         } ),
+ *         submit = new OO.ui.ButtonInputWidget( {
+ *             label: 'Submit'
+ *         } ),
+ *         fieldset = new OO.ui.FieldsetLayout( {
+ *             label: 'A form layout'
+ *         } );
  *
- *     var fieldset = new OO.ui.FieldsetLayout( {
- *         label: 'A form layout'
- *     } );
  *     fieldset.addItems( [
  *         new OO.ui.FieldLayout( input1, {
  *             label: 'Username',
@@ -12564,7 +12719,7 @@ OO.ui.PanelLayout.prototype.focus = function () {
  * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
  *
  *     @example
- *     // HorizontalLayout with a text input and a label
+ *     // HorizontalLayout with a text input and a label.
  *     var layout = new OO.ui.HorizontalLayout( {
  *       items: [
  *         new OO.ui.LabelWidget( { label: 'Label' } ),
@@ -12960,19 +13115,303 @@ OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
        }
 };
 
+/**
+ * Update the disabled state of the controls
+ *
+ * @chainable
+ * @protected
+ * @return {OO.ui.NumberInputWidget} The widget, for chaining
+ */
+OO.ui.NumberInputWidget.prototype.updateControlsDisabled = function () {
+       var disabled = this.isDisabled() || this.isReadOnly();
+       if ( this.minusButton ) {
+               this.minusButton.setDisabled( disabled );
+       }
+       if ( this.plusButton ) {
+               this.plusButton.setDisabled( disabled );
+       }
+       return this;
+};
+
 /**
  * @inheritdoc
  */
 OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
        // Parent method
        OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
+       this.updateControlsDisabled();
+       return this;
+};
 
-       if ( this.minusButton ) {
-               this.minusButton.setDisabled( this.isDisabled() );
+/**
+ * @inheritdoc
+ */
+OO.ui.NumberInputWidget.prototype.setReadOnly = function () {
+       // Parent method
+       OO.ui.NumberInputWidget.parent.prototype.setReadOnly.apply( this, arguments );
+       this.updateControlsDisabled();
+       return this;
+};
+
+/**
+ * SelectFileInputWidgets allow for selecting files, using <input type="file">. These
+ * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link
+ * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}.
+ * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples.
+ *
+ * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename.
+ *
+ *     @example
+ *     // A file select input widget.
+ *     var selectFile = new OO.ui.SelectFileInputWidget();
+ *     $( document.body ).append( selectFile.$element );
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets
+ *
+ * @class
+ * @extends OO.ui.InputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
+ * @cfg {boolean} [multiple=false] Allow multiple files to be selected.
+ * @cfg {string} [placeholder] Text to display when no file is selected.
+ * @cfg {Object} [button] Config to pass to select file button.
+ * @cfg {string} [icon] Icon to show next to file info
+ */
+OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
+       var widget = this;
+
+       config = config || {};
+
+       // Construct buttons before parent method is called (calling setDisabled)
+       this.selectButton = new OO.ui.ButtonWidget( $.extend( {
+               $element: $( '<label>' ),
+               classes: [ 'oo-ui-selectFileInputWidget-selectButton' ],
+               label: OO.ui.msg( 'ooui-selectfile-button-select' )
+       }, config.button ) );
+
+       // Configuration initialization
+       config = $.extend( {
+               accept: null,
+               placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
+               $tabIndexed: this.selectButton.$tabIndexed
+       }, config );
+
+       this.info = new OO.ui.SearchInputWidget( {
+               classes: [ 'oo-ui-selectFileInputWidget-info' ],
+               placeholder: config.placeholder,
+               // Pass an empty collection so that .focus() always does nothing
+               $tabIndexed: $( [] )
+       } ).setIcon( config.icon );
+       // Set tabindex manually on $input as $tabIndexed has been overridden
+       this.info.$input.attr( 'tabindex', -1 );
+
+       // Parent constructor
+       OO.ui.SelectFileInputWidget.parent.call( this, config );
+
+       // Properties
+       this.currentFiles = this.filterFiles( this.$input[ 0 ].files || [] );
+       if ( Array.isArray( config.accept ) ) {
+               this.accept = config.accept;
+       } else {
+               this.accept = null;
        }
-       if ( this.plusButton ) {
-               this.plusButton.setDisabled( this.isDisabled() );
+       this.multiple = !!config.multiple;
+
+       // Events
+       this.info.connect( this, { change: 'onInfoChange' } );
+       this.selectButton.$button.on( {
+               keypress: this.onKeyPress.bind( this )
+       } );
+       this.$input.on( {
+               change: this.onFileSelected.bind( this ),
+               // Support: IE11
+               // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
+               // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
+               // Since this messes with our custom styling (the file input has large dimensions and this
+               // causes the label to scroll out of view), scroll the button back to top. (T192131)
+               focus: function () {
+                       widget.$input.parent().prop( 'scrollTop', 0 );
+               }
+       } );
+       this.connect( this, { change: 'updateUI' } );
+
+       this.fieldLayout = new OO.ui.ActionFieldLayout( this.info, this.selectButton, { align: 'top' } );
+
+       this.$input
+               .attr( {
+                       type: 'file',
+                       // this.selectButton is tabindexed
+                       tabindex: -1,
+                       // Infused input may have previously by
+                       // TabIndexed, so remove aria-disabled attr.
+                       'aria-disabled': null
+               } );
+
+       if ( this.accept ) {
+               this.$input.attr( 'accept', this.accept.join( ', ' ) );
+       }
+       if ( this.multiple ) {
+               this.$input.attr( 'multiple', '' );
+       }
+       this.selectButton.$button.append( this.$input );
+
+       this.$element
+               .addClass( 'oo-ui-selectFileInputWidget' )
+               .append( this.fieldLayout.$element );
+
+       this.updateUI();
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.SelectFileInputWidget, OO.ui.InputWidget );
+
+/* Static properties */
+
+// Set empty title so that browser default tooltips like "No file chosen" don't appear.
+// On SelectFileWidget this tooltip will often be incorrect, so create a consistent
+// experience on SelectFileInputWidget.
+OO.ui.SelectFileInputWidget.static.title = '';
+
+/* Methods */
+
+/**
+ * Get the filename of the currently selected file.
+ *
+ * @return {string} Filename
+ */
+OO.ui.SelectFileInputWidget.prototype.getFilename = function () {
+       if ( this.currentFiles.length ) {
+               return this.currentFiles.map( function ( file ) {
+                       return file.name;
+               } ).join( ', ' );
+       } else {
+               // Try to strip leading fakepath.
+               return this.getValue().split( '\\' ).pop();
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.SelectFileInputWidget.prototype.setValue = function ( value ) {
+       if ( value === undefined ) {
+               // Called during init, don't replace value if just infusing.
+               return;
+       }
+       if ( value ) {
+               // We need to update this.value, but without trying to modify
+               // the DOM value, which would throw an exception.
+               if ( this.value !== value ) {
+                       this.value = value;
+                       this.emit( 'change', this.value );
+               }
+       } else {
+               this.currentFiles = [];
+               // Parent method
+               OO.ui.SelectFileInputWidget.super.prototype.setValue.call( this, '' );
+       }
+};
+
+/**
+ * Handle file selection from the input.
+ *
+ * @protected
+ * @param {jQuery.Event} e
+ */
+OO.ui.SelectFileInputWidget.prototype.onFileSelected = function ( e ) {
+       this.currentFiles = this.filterFiles( e.target.files || [] );
+};
+
+/**
+ * Update the user interface when a file is selected or unselected.
+ *
+ * @protected
+ */
+OO.ui.SelectFileInputWidget.prototype.updateUI = function () {
+       this.info.setValue( this.getFilename() );
+};
+
+/**
+ * Determine if we should accept this file.
+ *
+ * @private
+ * @param {FileList|File[]} files Files to filter
+ * @return {File[]} Filter files
+ */
+OO.ui.SelectFileInputWidget.prototype.filterFiles = function ( files ) {
+       var accept = this.accept;
+
+       function mimeAllowed( file ) {
+               var i, mimeTest,
+                       mimeType = file.type;
+
+               if ( !accept || !mimeType ) {
+                       return true;
+               }
+
+               for ( i = 0; i < accept.length; i++ ) {
+                       mimeTest = accept[ i ];
+                       if ( mimeTest === mimeType ) {
+                               return true;
+                       } else if ( mimeTest.substr( -2 ) === '/*' ) {
+                               mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
+                               if ( mimeType.substr( 0, mimeTest.length ) === mimeTest ) {
+                                       return true;
+                               }
+                       }
+               }
+               return false;
+       }
+
+       return Array.prototype.filter.call( files, mimeAllowed );
+};
+
+/**
+ * Handle info input change events
+ *
+ * The info widget can only be changed by the user
+ * with the clear button.
+ *
+ * @private
+ * @param {string} value
+ */
+OO.ui.SelectFileInputWidget.prototype.onInfoChange = function ( value ) {
+       if ( value === '' ) {
+               this.setValue( null );
        }
+};
+
+/**
+ * Handle key press events.
+ *
+ * @private
+ * @param {jQuery.Event} e Key press event
+ * @return {undefined/boolean} False to prevent default if event is handled
+ */
+OO.ui.SelectFileInputWidget.prototype.onKeyPress = function ( e ) {
+       if ( !this.isDisabled() && this.$input &&
+               ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
+       ) {
+               // Emit a click to open the file selector.
+               this.$input.trigger( 'click' );
+               // Taking focus from the selectButton means keyUp isn't fired, so fire it manually.
+               this.selectButton.onDocumentKeyUp( e );
+               return false;
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.SelectFileInputWidget.prototype.setDisabled = function ( disabled ) {
+       // Parent method
+       OO.ui.SelectFileInputWidget.parent.prototype.setDisabled.call( this, disabled );
+
+       this.selectButton.setDisabled( disabled );
+       this.info.setDisabled( disabled );
 
        return this;
 };