Merge "Don't fallback from uk to ru"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui-core.js
index 2a1427b..fd4e033 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.17.6
+ * OOjs UI v0.18.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2016 OOjs UI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2016-07-12T20:26:03Z
+ * Date: 2016-11-09T00:52:37Z
  */
 ( function ( OO ) {
 
@@ -260,6 +260,18 @@ OO.ui.debounce = function ( func, wait, immediate ) {
        };
 };
 
+/**
+ * Puts a console warning with provided message.
+ *
+ * @param {string} message
+ */
+OO.ui.warnDeprecation = function ( message ) {
+       if ( OO.getProp( window, 'console', 'warn' ) !== undefined ) {
+               // eslint-disable-next-line no-console
+               console.warn( message );
+       }
+};
+
 /**
  * Returns a function, that, when invoked, will only be triggered at most once
  * during a given window of time. If called again during that window, it will
@@ -312,30 +324,6 @@ OO.ui.now = Date.now || function () {
        return new Date().getTime();
 };
 
-/**
- * Proxy for `node.addEventListener( eventName, handler, true )`.
- *
- * @param {HTMLElement} node
- * @param {string} eventName
- * @param {Function} handler
- * @deprecated since 0.15.0
- */
-OO.ui.addCaptureEventListener = function ( node, eventName, handler ) {
-       node.addEventListener( eventName, handler, true );
-};
-
-/**
- * Proxy for `node.removeEventListener( eventName, handler, true )`.
- *
- * @param {HTMLElement} node
- * @param {string} eventName
- * @param {Function} handler
- * @deprecated since 0.15.0
- */
-OO.ui.removeCaptureEventListener = function ( node, eventName, handler ) {
-       node.removeEventListener( eventName, handler, true );
-};
-
 /**
  * Reconstitute a JavaScript object corresponding to a widget created by
  * the PHP implementation.
@@ -747,9 +735,8 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
        // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
        state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
        // rebuild widget
-       // jscs:disable requireCapitalizedConstructors
+       // eslint-disable-next-line new-cap
        obj = new cls( data );
-       // jscs:enable requireCapitalizedConstructors
        // now replace old DOM with this new DOM.
        if ( top ) {
                // An efficient constructor might be able to reuse the entire DOM tree of the original element,
@@ -1584,12 +1571,8 @@ OO.ui.Widget.prototype.updateDisabled = function () {
  * @class
  *
  * @constructor
- * @param {Object} [config] Configuration options
  */
-OO.ui.Theme = function OoUiTheme( config ) {
-       // Configuration initialization
-       config = config || {};
-};
+OO.ui.Theme = function OoUiTheme() {};
 
 /* Setup */
 
@@ -1616,7 +1599,6 @@ OO.ui.Theme.prototype.getElementClasses = function () {
  * For elements with theme logic hooks, this should be called any time there's a state change.
  *
  * @param {OO.ui.Element} element Element for which to update classes
- * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
  */
 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
        var $elements = $( [] ),
@@ -1809,7 +1791,7 @@ OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
        // Properties
        this.$button = null;
        this.framed = null;
-       this.active = false;
+       this.active = config.active !== undefined && config.active;
        this.onMouseUpHandler = this.onMouseUp.bind( this );
        this.onMouseDownHandler = this.onMouseDown.bind( this );
        this.onKeyDownHandler = this.onKeyDown.bind( this );
@@ -1877,13 +1859,18 @@ OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
 
        this.$button = $button
                .addClass( 'oo-ui-buttonElement-button' )
-               .attr( { role: 'button' } )
                .on( {
                        mousedown: this.onMouseDownHandler,
                        keydown: this.onKeyDownHandler,
                        click: this.onClickHandler,
                        keypress: this.onKeyPressHandler
                } );
+
+       // Add `role="button"` on `<a>` elements, where it's needed
+       // `toUppercase()` is added for XHTML documents
+       if ( this.$button.prop( 'tagName' ).toUpperCase() === 'A' ) {
+               this.$button.attr( 'role', 'button' );
+       }
 };
 
 /**
@@ -2026,6 +2013,7 @@ OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
        this.active = !!value;
        this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
+       this.updateThemeClasses();
        return this;
 };
 
@@ -3398,31 +3386,14 @@ OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
 
-/* Methods */
+/* Static Properties */
 
 /**
  * @inheritdoc
  */
-OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
-       if ( !this.isDisabled() ) {
-               // Remove the tab-index while the button is down to prevent the button from stealing focus
-               this.$button.removeAttr( 'tabindex' );
-       }
-
-       return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
-};
+OO.ui.ButtonWidget.static.cancelButtonMouseDownEvents = false;
 
-/**
- * @inheritdoc
- */
-OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
-       if ( !this.isDisabled() ) {
-               // Restore the tab-index after the button is up to restore the button's accessibility
-               this.$button.attr( 'tabindex', this.tabIndex );
-       }
-
-       return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
-};
+/* Methods */
 
 /**
  * Get hyperlink location.
@@ -6366,7 +6337,10 @@ OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
                keypress: this.menu.onKeyPressHandler,
                blur: this.menu.clearKeyPressBuffer.bind( this.menu )
        } );
-       this.menu.connect( this, { select: 'onMenuSelect' } );
+       this.menu.connect( this, {
+               select: 'onMenuSelect',
+               toggle: 'onMenuToggle'
+       } );
 
        // Initialization
        this.$handle
@@ -6422,6 +6396,16 @@ OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
        this.setLabel( selectedLabel );
 };
 
+/**
+ * Handle menu toggle events.
+ *
+ * @private
+ * @param {boolean} isVisible Menu toggle event
+ */
+OO.ui.DropdownWidget.prototype.onMenuToggle = function ( isVisible ) {
+       this.$element.toggleClass( 'oo-ui-dropdownWidget-open', isVisible );
+};
+
 /**
  * Handle mouse click events.
  *
@@ -6994,7 +6978,7 @@ OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function (
  * @param {jQuery.Event} e
  */
 OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
-       var $options, checked,
+       var $options, lastClickedIndex, nowClickedIndex, i, direction, wasSelected, items,
                $lastClicked = this.$lastClicked,
                $nowClicked = $( e.target ).closest( '.oo-ui-checkboxMultioptionWidget' )
                        .not( '.oo-ui-widget-disabled' );
@@ -7002,19 +6986,32 @@ OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
        // Allow selecting multiple options at once by Shift-clicking them
        if ( $lastClicked && $nowClicked.length && e.shiftKey ) {
                $options = this.$group.find( '.oo-ui-checkboxMultioptionWidget' );
-               checked = $nowClicked.find( 'input' ).prop( 'checked' );
-
-               $options
-                       .slice(
-                               Math.min( $options.index( $lastClicked ), $options.index( $nowClicked ) ),
-                               Math.max( $options.index( $lastClicked ), $options.index( $nowClicked ) ) + 1
-                       )
-                       .find( 'input' )
-                       .filter( function () {
-                               return !this.disabled;
-                       } )
-                       .prop( 'checked', checked )
-                       .trigger( 'change' );
+               lastClickedIndex = $options.index( $lastClicked );
+               nowClickedIndex = $options.index( $nowClicked );
+               // If it's the same item, either the user is being silly, or it's a fake event generated by the
+               // browser. In either case we don't need custom handling.
+               if ( nowClickedIndex !== lastClickedIndex ) {
+                       items = this.items;
+                       wasSelected = items[ nowClickedIndex ].isSelected();
+                       direction = nowClickedIndex > lastClickedIndex ? 1 : -1;
+
+                       // This depends on the DOM order of the items and the order of the .items array being the same.
+                       for ( i = lastClickedIndex; i !== nowClickedIndex; i += direction ) {
+                               if ( !items[ i ].isDisabled() ) {
+                                       items[ i ].setSelected( !wasSelected );
+                               }
+                       }
+                       // For the now-clicked element, use immediate timeout to allow the browser to do its own
+                       // handling first, then set our value. The order in which events happen is different for
+                       // clicks on the <input> and on the <label> and there are additional fake clicks fired for
+                       // non-click actions that change the checkboxes.
+                       e.preventDefault();
+                       setTimeout( function () {
+                               if ( !items[ nowClickedIndex ].isDisabled() ) {
+                                       items[ nowClickedIndex ].setSelected( !wasSelected );
+                               }
+                       } );
+               }
        }
 
        if ( $nowClicked.length ) {
@@ -7154,7 +7151,6 @@ OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positionin
  */
 OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
        var elemRect, contRect,
-               topEdgeInBounds = false,
                leftEdgeInBounds = false,
                bottomEdgeInBounds = false,
                rightEdgeInBounds = false;
@@ -7171,9 +7167,8 @@ OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element
                contRect = $container[ 0 ].getBoundingClientRect();
        }
 
-       if ( elemRect.top >= contRect.top && elemRect.top <= contRect.bottom ) {
-               topEdgeInBounds = true;
-       }
+       // For completeness, if we still cared about topEdgeInBounds, that'd be:
+       // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
        if ( elemRect.left >= contRect.left && elemRect.left <= contRect.right ) {
                leftEdgeInBounds = true;
        }
@@ -7312,6 +7307,103 @@ OO.ui.FloatingMenuSelectWidget.prototype.toggle = function ( visible ) {
        return this;
 };
 
+/**
+ * Progress bars visually display the status of an operation, such as a download,
+ * and can be either determinate or indeterminate:
+ *
+ * - **determinate** process bars show the percent of an operation that is complete.
+ *
+ * - **indeterminate** process bars use a visual display of motion to indicate that an operation
+ *   is taking place. Because the extent of an indeterminate operation is unknown, the bar does
+ *   not use percentages.
+ *
+ * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
+ *
+ *     @example
+ *     // Examples of determinate and indeterminate progress bars.
+ *     var progressBar1 = new OO.ui.ProgressBarWidget( {
+ *         progress: 33
+ *     } );
+ *     var progressBar2 = new OO.ui.ProgressBarWidget();
+ *
+ *     // Create a FieldsetLayout to layout progress bars
+ *     var fieldset = new OO.ui.FieldsetLayout;
+ *     fieldset.addItems( [
+ *        new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
+ *        new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
+ *     ] );
+ *     $( 'body' ).append( fieldset.$element );
+ *
+ * @class
+ * @extends OO.ui.Widget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
+ *  To create a determinate progress bar, specify a number that reflects the initial percent complete.
+ *  By default, the progress bar is indeterminate.
+ */
+OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
+       // Configuration initialization
+       config = config || {};
+
+       // Parent constructor
+       OO.ui.ProgressBarWidget.parent.call( this, config );
+
+       // Properties
+       this.$bar = $( '<div>' );
+       this.progress = null;
+
+       // Initialization
+       this.setProgress( config.progress !== undefined ? config.progress : false );
+       this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
+       this.$element
+               .attr( {
+                       role: 'progressbar',
+                       'aria-valuemin': 0,
+                       'aria-valuemax': 100
+               } )
+               .addClass( 'oo-ui-progressBarWidget' )
+               .append( this.$bar );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
+
+/* Static Properties */
+
+OO.ui.ProgressBarWidget.static.tagName = 'div';
+
+/* Methods */
+
+/**
+ * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
+ *
+ * @return {number|boolean} Progress percent
+ */
+OO.ui.ProgressBarWidget.prototype.getProgress = function () {
+       return this.progress;
+};
+
+/**
+ * Set the percent of the process completed or `false` for an indeterminate process.
+ *
+ * @param {number|boolean} progress Progress percent or `false` for indeterminate
+ */
+OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
+       this.progress = progress;
+
+       if ( progress !== false ) {
+               this.$bar.css( 'width', this.progress + '%' );
+               this.$element.attr( 'aria-valuenow', this.progress );
+       } else {
+               this.$bar.css( 'width', '' );
+               this.$element.removeAttr( 'aria-valuenow' );
+       }
+       this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', progress === false );
+};
+
 /**
  * InputWidget is the base class for all input widgets, which
  * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
@@ -7466,18 +7558,6 @@ OO.ui.InputWidget.prototype.getValue = function () {
        return this.value;
 };
 
-/**
- * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
- *
- * @deprecated since v0.13.1; use #setDir directly
- * @param {boolean} isRTL Directionality is right-to-left
- * @chainable
- */
-OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
-       this.setDir( isRTL ? 'rtl' : 'ltr' );
-       return this;
-};
-
 /**
  * Set the directionality of the input.
  *
@@ -8496,7 +8576,7 @@ OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options )
  * @constructor
  * @param {Object} [config] Configuration options
  * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
- *  'email', 'url', 'date' or 'number'. Ignored if `multiline` is true.
+ *  'email', 'url', 'date', 'month' or 'number'. Ignored if `multiline` is true.
  *
  *  Some values of `type` result in additional behaviors:
  *
@@ -8530,17 +8610,14 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
                type: 'text',
                labelPosition: 'after'
        }, config );
+
        if ( config.type === 'search' ) {
+               OO.ui.warnDeprecation( 'TextInputWidget: config.type=\'search\' is deprecated. Use the SearchInputWidget instead. See T148471 for details.' );
                if ( config.icon === undefined ) {
                        config.icon = 'search';
                }
                // indicator: 'clear' is set dynamically later, depending on value
        }
-       if ( config.required ) {
-               if ( config.indicator === undefined ) {
-                       config.indicator = 'required';
-               }
-       }
 
        // Parent constructor
        OO.ui.TextInputWidget.parent.call( this, config );
@@ -8554,6 +8631,7 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        // Properties
        this.type = this.getSaneType( config );
        this.readOnly = false;
+       this.required = false;
        this.multiline = !!config.multiline;
        this.autosize = !!config.autosize;
        this.minRows = config.rows !== undefined ? config.rows : '';
@@ -8577,7 +8655,8 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        // Events
        this.$input.on( {
                keypress: this.onKeyPress.bind( this ),
-               blur: this.onBlur.bind( this )
+               blur: this.onBlur.bind( this ),
+               focus: this.onFocus.bind( this )
        } );
        this.$input.one( {
                focus: this.onElementAttach.bind( this )
@@ -8589,12 +8668,14 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
                change: 'onChange',
                disable: 'onDisable'
        } );
+       this.on( 'change', OO.ui.debounce( this.onDebouncedChange.bind( this ), 250 ) );
 
        // Initialization
        this.$element
                .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
                .append( this.$icon, this.$indicator );
        this.setReadOnly( !!config.readOnly );
+       this.setRequired( !!config.required );
        this.updateSearchIndicator();
        if ( config.placeholder !== undefined ) {
                this.$input.attr( 'placeholder', config.placeholder );
@@ -8605,10 +8686,6 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        if ( config.autofocus ) {
                this.$input.attr( 'autofocus', 'autofocus' );
        }
-       if ( config.required ) {
-               this.$input.attr( 'required', 'required' );
-               this.$input.attr( 'aria-required', 'true' );
-       }
        if ( config.autocomplete === false ) {
                this.$input.attr( 'autocomplete', 'off' );
                // Turning off autocompletion also disables "form caching" when the user navigates to a
@@ -8731,6 +8808,16 @@ OO.ui.TextInputWidget.prototype.onBlur = function () {
        this.setValidityFlag();
 };
 
+/**
+ * Handle focus events.
+ *
+ * @private
+ * @param {jQuery.Event} e Focus event
+ */
+OO.ui.TextInputWidget.prototype.onFocus = function () {
+       this.setValidityFlag( true );
+};
+
 /**
  * Handle element attach events.
  *
@@ -8752,10 +8839,19 @@ OO.ui.TextInputWidget.prototype.onElementAttach = function () {
  */
 OO.ui.TextInputWidget.prototype.onChange = function () {
        this.updateSearchIndicator();
-       this.setValidityFlag();
        this.adjustSize();
 };
 
+/**
+ * Handle debounced change events.
+ *
+ * @param {string} value
+ * @private
+ */
+OO.ui.TextInputWidget.prototype.onDebouncedChange = function () {
+       this.setValidityFlag();
+};
+
 /**
  * Handle disable events.
  *
@@ -8788,6 +8884,42 @@ OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
        return this;
 };
 
+/**
+ * Check if the input is {@link #required required}.
+ *
+ * @return {boolean}
+ */
+OO.ui.TextInputWidget.prototype.isRequired = function () {
+       return this.required;
+};
+
+/**
+ * Set the {@link #required required} state of the input.
+ *
+ * @param {boolean} state Make input required
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.setRequired = function ( state ) {
+       this.required = !!state;
+       if ( this.required ) {
+               this.$input
+                       .attr( 'required', 'required' )
+                       .attr( 'aria-required', 'true' );
+               if ( this.getIndicator() === null ) {
+                       this.setIndicator( 'required' );
+               }
+       } else {
+               this.$input
+                       .removeAttr( 'required' )
+                       .removeAttr( 'aria-required' );
+               if ( this.getIndicator() === 'required' ) {
+                       this.setIndicator( null );
+               }
+       }
+       this.updateSearchIndicator();
+       return this;
+};
+
 /**
  * Support function for making #onElementAttach work across browsers.
  *
@@ -8963,6 +9095,7 @@ OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
                'email',
                'url',
                'date',
+               'month',
                'number'
        ];
        return allowedTypes.indexOf( config.type ) !== -1 ? config.type : 'text';
@@ -9160,30 +9293,6 @@ OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
        }
 };
 
-/**
- * Check if a value is valid.
- *
- * This method returns a promise that resolves with a boolean `true` if the current value is
- * considered valid according to the supplied {@link #validate validation pattern}.
- *
- * @deprecated since v0.12.3
- * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
- */
-OO.ui.TextInputWidget.prototype.isValid = function () {
-       var result;
-
-       if ( this.validate instanceof Function ) {
-               result = this.validate( this.getValue() );
-               if ( result && $.isFunction( result.promise ) ) {
-                       return result.promise();
-               } else {
-                       return $.Deferred().resolve( !!result ).promise();
-               }
-       } else {
-               return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
-       }
-};
-
 /**
  * Get the validity of current value.
  *
@@ -9312,6 +9421,99 @@ OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
        }
 };
 
+/**
+ * @class
+ * @extends OO.ui.TextInputWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.SearchInputWidget = function OoUiSearchInputWidget( config ) {
+       config = $.extend( {
+               icon: 'search'
+       }, config );
+
+       // Set type to text so that TextInputWidget doesn't
+       // get stuck in an infinite loop.
+       config.type = 'text';
+
+       // Parent constructor
+       OO.ui.SearchInputWidget.parent.call( this, config );
+
+       // Initialization
+       this.$element.addClass( 'oo-ui-textInputWidget-type-search' );
+       this.updateSearchIndicator();
+       this.connect( this, {
+               disable: 'onDisable'
+       } );
+};
+
+/* Setup */
+
+OO.inheritClass( OO.ui.SearchInputWidget, OO.ui.TextInputWidget );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ * @protected
+ */
+OO.ui.SearchInputWidget.prototype.getInputElement = function () {
+       return $( '<input>' ).attr( 'type', 'search' );
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.SearchInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
+       if ( e.which === OO.ui.MouseButtons.LEFT ) {
+               // Clear the text field
+               this.setValue( '' );
+               this.$input[ 0 ].focus();
+               return false;
+       }
+};
+
+/**
+ * Update the 'clear' indicator displayed on type: 'search' text
+ * fields, hiding it when the field is already empty or when it's not
+ * editable.
+ */
+OO.ui.SearchInputWidget.prototype.updateSearchIndicator = function () {
+       if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
+               this.setIndicator( null );
+       } else {
+               this.setIndicator( 'clear' );
+       }
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.SearchInputWidget.prototype.onChange = function () {
+       OO.ui.SearchInputWidget.parent.prototype.onChange.call( this );
+       this.updateSearchIndicator();
+};
+
+/**
+ * Handle disable events.
+ *
+ * @param {boolean} disabled Element is disabled
+ * @private
+ */
+OO.ui.SearchInputWidget.prototype.onDisable = function () {
+       this.updateSearchIndicator();
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) {
+       OO.ui.SearchInputWidget.parent.prototype.setReadOnly.call( this, state );
+       this.updateSearchIndicator();
+       return this;
+};
+
 /**
  * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
  * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
@@ -9374,17 +9576,19 @@ OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
 OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
        // Configuration initialization
        config = $.extend( {
-               indicator: 'down',
                autocomplete: false
        }, config );
-       // For backwards-compatibility with ComboBoxWidget config
-       $.extend( config, config.input );
 
        // Parent constructor
        OO.ui.ComboBoxInputWidget.parent.call( this, config );
 
        // Properties
        this.$overlay = config.$overlay || this.$element;
+       this.dropdownButton = new OO.ui.ButtonWidget( {
+               classes: [ 'oo-ui-comboBoxInputWidget-dropdownButton' ],
+               indicator: 'down',
+               disabled: this.disabled
+       } );
        this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
                {
                        widget: this,
@@ -9394,18 +9598,15 @@ OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
                },
                config.menu
        ) );
-       // For backwards-compatibility with ComboBoxWidget
-       this.input = this;
 
        // Events
-       this.$indicator.on( {
-               click: this.onIndicatorClick.bind( this ),
-               keypress: this.onIndicatorKeyPress.bind( this )
-       } );
        this.connect( this, {
                change: 'onInputChange',
                enter: 'onInputEnter'
        } );
+       this.dropdownButton.connect( this, {
+               click: 'onDropdownButtonClick'
+       } );
        this.menu.connect( this, {
                choose: 'onMenuChoose',
                add: 'onMenuItemsChange',
@@ -9421,8 +9622,12 @@ OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
        if ( config.options !== undefined ) {
                this.setOptions( config.options );
        }
-       // Extra class for backwards-compatibility with ComboBoxWidget
-       this.$element.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
+       this.$field = $( '<div>' )
+               .addClass( 'oo-ui-comboBoxInputWidget-field' )
+               .append( this.$input, this.dropdownButton.$element );
+       this.$element
+               .addClass( 'oo-ui-comboBoxInputWidget' )
+               .append( this.$field );
        this.$overlay.append( this.menu.$element );
        this.onMenuItemsChange();
 };
@@ -9471,42 +9676,24 @@ OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
 };
 
 /**
- * Handle mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
- */
-OO.ui.ComboBoxInputWidget.prototype.onIndicatorClick = function ( e ) {
-       if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
-               this.menu.toggle();
-               this.$input[ 0 ].focus();
-       }
-       return false;
-};
-
-/**
- * Handle key press events.
+ * Handle input enter events.
  *
  * @private
- * @param {jQuery.Event} e Key press event
  */
-OO.ui.ComboBoxInputWidget.prototype.onIndicatorKeyPress = function ( e ) {
-       if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
-               this.menu.toggle();
-               this.$input[ 0 ].focus();
-               return false;
+OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
+       if ( !this.isDisabled() ) {
+               this.menu.toggle( false );
        }
 };
 
 /**
- * Handle input enter events.
+ * Handle button click events.
  *
  * @private
  */
-OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
-       if ( !this.isDisabled() ) {
-               this.menu.toggle( false );
-       }
+OO.ui.ComboBoxInputWidget.prototype.onDropdownButtonClick = function () {
+       this.menu.toggle();
+       this.$input[ 0 ].focus();
 };
 
 /**
@@ -9540,6 +9727,9 @@ OO.ui.ComboBoxInputWidget.prototype.setDisabled = function ( disabled ) {
        // Parent method
        OO.ui.ComboBoxInputWidget.parent.prototype.setDisabled.call( this, disabled );
 
+       if ( this.dropdownButton ) {
+               this.dropdownButton.setDisabled( this.isDisabled() );
+       }
        if ( this.menu ) {
                this.menu.setDisabled( this.isDisabled() );
        }
@@ -9566,12 +9756,6 @@ OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
        return this;
 };
 
-/**
- * @class
- * @deprecated since 0.13.2; use OO.ui.ComboBoxInputWidget instead
- */
-OO.ui.ComboBoxWidget = OO.ui.ComboBoxInputWidget;
-
 /**
  * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
  * which is a widget that is specified by reference before any optional configuration settings.
@@ -9676,12 +9860,12 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
        // Initialization
        this.$element
                .addClass( 'oo-ui-fieldLayout' )
+               .toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget.isDisabled() )
                .append( this.$help, this.$body );
        this.$body.addClass( 'oo-ui-fieldLayout-body' );
        this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
        this.$field
                .addClass( 'oo-ui-fieldLayout-field' )
-               .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
                .append( this.fieldWidget.$element );
 
        this.setErrors( config.errors || [] );
@@ -9878,6 +10062,7 @@ OO.ui.FieldLayout.prototype.updateMessages = function () {
  * @constructor
  * @param {OO.ui.Widget} fieldWidget Field widget
  * @param {OO.ui.ButtonWidget} buttonWidget Button widget
+ * @param {Object} config
  */
 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
        // Allow passing positional parameters inside the config object
@@ -9963,7 +10148,7 @@ OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
 
        // Mixin constructors
        OO.ui.mixin.IconElement.call( this, config );
-       OO.ui.mixin.LabelElement.call( this, config );
+       OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: $( '<legend>' ) } ) );
        OO.ui.mixin.GroupElement.call( this, config );
 
        if ( config.help ) {
@@ -9984,9 +10169,10 @@ OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
        }
 
        // Initialization
+       this.$group.addClass( 'oo-ui-fieldsetLayout-group' );
        this.$element
                .addClass( 'oo-ui-fieldsetLayout' )
-               .prepend( this.$help, this.$icon, this.$label, this.$group );
+               .prepend( this.$label, this.$help, this.$icon, this.$group );
        if ( Array.isArray( config.items ) ) {
                this.addItems( config.items );
        }
@@ -9999,6 +10185,10 @@ OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
 
+/* Static Properties */
+
+OO.ui.FieldsetLayout.static.tagName = 'fieldset';
+
 /**
  * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
  * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an