RevisionStoreDbTestBase, remove redundant needsDB override
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui-core.js
index 68efe07..6f22972 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOUI v0.27.0
+ * OOUI v0.28.0
  * https://www.mediawiki.org/wiki/OOUI
  *
  * Copyright 2011–2018 OOUI Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2018-05-09T00:44:45Z
+ * Date: 2018-08-14T23:16:18Z
  */
 ( function ( OO ) {
 
@@ -333,11 +333,12 @@ OO.ui.now = Date.now || function () {
  *
  * @param {string|HTMLElement|jQuery} idOrNode
  *   A DOM id (if a string) or node for the widget to infuse.
+ * @param {Object} [config] Configuration options
  * @return {OO.ui.Element}
  *   The `OO.ui.Element` corresponding to this (infusable) document node.
  */
-OO.ui.infuse = function ( idOrNode ) {
-       return OO.ui.Element.static.infuse( idOrNode );
+OO.ui.infuse = function ( idOrNode, config ) {
+       return OO.ui.Element.static.infuse( idOrNode, config );
 };
 
 ( function () {
@@ -685,14 +686,15 @@ OO.ui.Element.static.tagName = 'div';
  *
  * @param {string|HTMLElement|jQuery} idOrNode
  *   A DOM id (if a string) or node for the widget to infuse.
+ * @param {Object} [config] Configuration options
  * @return {OO.ui.Element}
  *   The `OO.ui.Element` corresponding to this (infusable) document node.
  *   For `Tag` objects emitted on the HTML side (used occasionally for content)
  *   the value returned is a newly-created Element wrapping around the existing
  *   DOM node.
  */
-OO.ui.Element.static.infuse = function ( idOrNode ) {
-       var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
+OO.ui.Element.static.infuse = function ( idOrNode, config ) {
+       var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, config, false );
        // Verify that the type matches up.
        // FIXME: uncomment after T89721 is fixed, see T90929.
        /*
@@ -709,12 +711,13 @@ OO.ui.Element.static.infuse = function ( idOrNode ) {
  *
  * @private
  * @param {string|HTMLElement|jQuery} idOrNode
- * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
+ * @param {Object} [config] Configuration options
+ * @param {jQuery.Promise} [domPromise] A promise that will be resolved
  *     when the top-level widget of this infusion is inserted into DOM,
- *     replacing the original node; or false for top-level invocation.
+ *     replacing the original node; only used internally.
  * @return {OO.ui.Element}
  */
-OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
+OO.ui.Element.static.unsafeInfuse = function ( idOrNode, config, domPromise ) {
        // look for a cached result of a previous infusion.
        var id, $elem, error, data, cls, parts, parent, obj, top, state, infusedChildren;
        if ( typeof idOrNode === 'string' ) {
@@ -772,7 +775,7 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
        }
        if ( data._ === 'Tag' ) {
                // Special case: this is a raw Tag; wrap existing node, don't rebuild.
-               return new OO.ui.Element( { $element: $elem } );
+               return new OO.ui.Element( $.extend( {}, config, { $element: $elem } ) );
        }
        parts = data._.split( '.' );
        cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
@@ -796,7 +799,7 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
                throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
        }
 
-       if ( domPromise === false ) {
+       if ( !domPromise ) {
                top = $.Deferred();
                domPromise = top.promise();
        }
@@ -807,7 +810,7 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
                var infused;
                if ( OO.isPlainObject( value ) ) {
                        if ( value.tag ) {
-                               infused = OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
+                               infused = OO.ui.Element.static.unsafeInfuse( value.tag, config, domPromise );
                                infusedChildren.push( infused );
                                // Flatten the structure
                                infusedChildren.push.apply( infusedChildren, infused.$element.data( 'ooui-infused-children' ) || [] );
@@ -825,7 +828,7 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
        state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
        // rebuild widget
        // eslint-disable-next-line new-cap
-       obj = new cls( data );
+       obj = new cls( $.extend( {}, config, data ) );
        // If anyone is holding a reference to the old DOM element,
        // let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
        // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
@@ -2669,6 +2672,7 @@ OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
 
        this.$icon = $icon
                .addClass( 'oo-ui-iconElement-icon' )
+               .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon )
                .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
        if ( this.iconTitle !== null ) {
                this.$icon.attr( 'title', this.iconTitle );
@@ -2703,6 +2707,9 @@ OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
        }
 
        this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
+       if ( this.$icon ) {
+               this.$icon.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon );
+       }
        this.updateThemeClasses();
 
        return this;
@@ -2840,6 +2847,7 @@ OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicat
 
        this.$indicator = $indicator
                .addClass( 'oo-ui-indicatorElement-indicator' )
+               .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator )
                .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
        if ( this.indicatorTitle !== null ) {
                this.$indicator.attr( 'title', this.indicatorTitle );
@@ -2870,6 +2878,9 @@ OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
        }
 
        this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
+       if ( this.$indicator ) {
+               this.$indicator.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator );
+       }
        this.updateThemeClasses();
 
        return this;
@@ -3602,7 +3613,7 @@ OO.ui.mixin.AccessKeyedElement.prototype.formatTitleWithAccessKey = function ( t
  *     var button = new OO.ui.ButtonWidget( {
  *         label: 'Button with Icon',
  *         icon: 'trash',
- *         iconTitle: 'Remove'
+ *         title: 'Remove'
  *     } );
  *     $( 'body' ).append( button.$element );
  *
@@ -3907,7 +3918,7 @@ OO.ui.ButtonGroupWidget.prototype.simulateLabelClick = function () {
  *     // An icon widget with a label
  *     var myIcon = new OO.ui.IconWidget( {
  *         icon: 'help',
- *         iconTitle: 'Help'
+ *         title: 'Help'
  *      } );
  *      // Create a label.
  *      var iconLabel = new OO.ui.LabelWidget( {
@@ -4118,7 +4129,7 @@ OO.ui.LabelWidget.static.tagName = 'label';
  *
  *     MessageDialog.prototype.initialize = function () {
  *         MessageDialog.parent.prototype.initialize.apply( this, arguments );
- *         this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
+ *         this.content = new OO.ui.PanelLayout( { padded: true } );
  *         this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.</p>' );
  *         this.$body.append( this.content.$element );
  *     };
@@ -5045,7 +5056,7 @@ OO.ui.mixin.ClippableElement.prototype.clip = function () {
 /**
  * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
  * By default, each popup has an anchor that points toward its origin.
- * Please see the [OOUI documentation on Mediawiki] [1] for more information and examples.
+ * Please see the [OOUI documentation on MediaWiki.org] [1] for more information and examples.
  *
  * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
  *
@@ -5071,8 +5082,8 @@ OO.ui.mixin.ClippableElement.prototype.clip = function () {
  *
  * @constructor
  * @param {Object} [config] Configuration options
- * @cfg {number} [width=320] Width of popup in pixels
- * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
+ * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
+ * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
  * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
  * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
  *  'above': Put popup above $floatableContainer; anchor points down to the horizontal center
@@ -5137,19 +5148,18 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
        this.$container = config.$container;
        this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
        this.autoClose = !!config.autoClose;
-       this.$autoCloseIgnore = config.$autoCloseIgnore;
        this.transitionTimeout = null;
        this.anchored = false;
-       this.width = config.width !== undefined ? config.width : 320;
-       this.height = config.height !== undefined ? config.height : null;
        this.onMouseDownHandler = this.onMouseDown.bind( this );
        this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
 
        // Initialization
+       this.setSize( config.width, config.height );
        this.toggleAnchor( config.anchor === undefined || config.anchor );
        this.setAlignment( config.align || 'center' );
        this.setPosition( config.position || 'below' );
        this.setAutoFlip( config.autoFlip === undefined || config.autoFlip );
+       this.setAutoCloseIgnore( config.$autoCloseIgnore );
        this.$body.addClass( 'oo-ui-popupWidget-body' );
        this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
        this.$popup
@@ -5231,6 +5241,13 @@ OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
        // Capture clicks outside popup
        this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
+       // We add 'click' event because iOS safari needs to respond to this event.
+       // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
+       // then it will trigger when scrolling. While iOS Safari has some reported behavior
+       // of occasionally not emitting 'click' properly, that event seems to be the standard
+       // that it should be emitting, so we add it to this and will operate the event handler
+       // on whichever of these events was triggered first
+       this.getElementDocument().addEventListener( 'click', this.onMouseDownHandler, true );
 };
 
 /**
@@ -5251,6 +5268,7 @@ OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
  */
 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
        this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
+       this.getElementDocument().removeEventListener( 'click', this.onMouseDownHandler, true );
 };
 
 /**
@@ -5447,13 +5465,13 @@ OO.ui.PopupWidget.prototype.toggle = function ( show ) {
  *
  * Changing the size may also change the popup's position depending on the alignment.
  *
- * @param {number} width Width in pixels
- * @param {number} height Height in pixels
+ * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
+ * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
  * @param {boolean} [transition=false] Use a smooth transition
  * @chainable
  */
 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
-       this.width = width;
+       this.width = width !== undefined ? width : 320;
        this.height = height !== undefined ? height : null;
        if ( this.isVisible() ) {
                this.updateDimensions( transition );
@@ -5543,7 +5561,7 @@ OO.ui.PopupWidget.prototype.computePosition = function () {
        // Set height and width before we do anything else, since it might cause our measurements
        // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
        this.$popup.css( {
-               width: this.width,
+               width: this.width !== null ? this.width : 'auto',
                height: this.height !== null ? this.height : 'auto'
        } );
 
@@ -5560,7 +5578,7 @@ OO.ui.PopupWidget.prototype.computePosition = function () {
        near = vertical ? 'top' : 'left';
        far = vertical ? 'bottom' : 'right';
        sizeProp = vertical ? 'Height' : 'Width';
-       popupSize = vertical ? ( this.height || this.$popup.height() ) : this.width;
+       popupSize = vertical ? ( this.height || this.$popup.height() ) : ( this.width || this.$popup.width() );
 
        this.setAnchorEdge( anchorEdgeMap[ popupPosition ] );
        this.horizontalPosition = vertical ? popupPosition : hPosMap[ align ];
@@ -5715,6 +5733,17 @@ OO.ui.PopupWidget.prototype.setAutoFlip = function ( autoFlip ) {
        }
 };
 
+/**
+ * Set which elements will not close the popup when clicked.
+ *
+ * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
+ *
+ * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
+ */
+OO.ui.PopupWidget.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore ) {
+       this.$autoCloseIgnore = $autoCloseIgnore;
+};
+
 /**
  * Get an ID of the body element, this can be used as the
  * `aria-describedby` attribute for an input field.
@@ -5819,8 +5848,7 @@ OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
 
        // Initialization
        this.$element
-               .addClass( 'oo-ui-popupButtonWidget' )
-               .attr( 'aria-haspopup', 'true' );
+               .addClass( 'oo-ui-popupButtonWidget' );
        this.popup.$element
                .addClass( 'oo-ui-popupButtonWidget-popup' )
                .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
@@ -7281,6 +7309,11 @@ OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
        OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
        OO.ui.mixin.FloatableElement.call( this, config );
 
+       // Initial vertical positions other than 'center' will result in
+       // the menu being flipped if there is not enough space in the container.
+       // Store the original position so we know what to reset to.
+       this.originalVerticalPosition = this.verticalPosition;
+
        // Properties
        this.autoHide = config.autoHide === undefined || !!config.autoHide;
        this.hideOnChoose = config.hideOnChoose === undefined || !!config.hideOnChoose;
@@ -7320,6 +7353,21 @@ OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.FloatableElement );
  * The menu is ready: it is visible and has been positioned and clipped.
  */
 
+/* Static properties */
+
+/**
+ * Positions to flip to if there isn't room in the container for the
+ * menu in a specific direction.
+ *
+ * @property {Object.<string,string>}
+ */
+OO.ui.MenuSelectWidget.static.flippedPositions = {
+       below: 'above',
+       above: 'below',
+       top: 'bottom',
+       bottom: 'top'
+};
+
 /* Methods */
 
 /**
@@ -7382,8 +7430,7 @@ OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
  * @protected
  */
 OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
-       var i, item, visible, section, sectionEmpty, filter, exactFilter,
-               firstItemFound = false,
+       var i, item, items, visible, section, sectionEmpty, filter, exactFilter,
                anyVisible = false,
                len = this.items.length,
                showAll = !this.isVisible(),
@@ -7392,7 +7439,6 @@ OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
        if ( this.$input && this.filterFromInput ) {
                filter = showAll ? null : this.getItemMatcher( this.$input.val() );
                exactFilter = this.getItemMatcher( this.$input.val(), true );
-
                // Hide non-matching options, and also hide section headers if all options
                // in their section are hidden.
                for ( i = 0; i < len; i++ ) {
@@ -7410,11 +7456,6 @@ OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
                                anyVisible = anyVisible || visible;
                                sectionEmpty = sectionEmpty && !visible;
                                item.toggle( visible );
-                               if ( this.highlightOnFilter && visible && !firstItemFound ) {
-                                       // Highlight the first item in the list
-                                       this.highlightItem( item );
-                                       firstItemFound = true;
-                               }
                        }
                }
                // Process the final section
@@ -7427,6 +7468,20 @@ OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
                }
 
                this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
+
+               if ( this.highlightOnFilter ) {
+                       // Highlight the first item on the list
+                       item = null;
+                       items = this.getItems();
+                       for ( i = 0; i < items.length; i++ ) {
+                               if ( items[ i ].isVisible() ) {
+                                       item = items[ i ];
+                                       break;
+                               }
+                       }
+                       this.highlightItem( item );
+               }
+
        }
 
        // Reevaluate clipping
@@ -7551,7 +7606,7 @@ OO.ui.MenuSelectWidget.prototype.clearItems = function () {
  * @inheritdoc
  */
 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
-       var change, belowHeight, aboveHeight;
+       var change, originalHeight, flippedHeight;
 
        visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
        change = visible !== this.isVisible();
@@ -7561,15 +7616,10 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
                this.warnedUnattached = true;
        }
 
-       if ( change ) {
-               if ( visible && ( this.width || this.$floatableContainer ) ) {
-                       this.setIdealSize( this.width || this.$floatableContainer.width() );
-               }
-               if ( visible ) {
-                       // Reset position before showing the popup again. It's possible we no longer need to flip
-                       // (e.g. if the user scrolled).
-                       this.setVerticalPosition( 'below' );
-               }
+       if ( change && visible ) {
+               // Reset position before showing the popup again. It's possible we no longer need to flip
+               // (e.g. if the user scrolled).
+               this.setVerticalPosition( this.originalVerticalPosition );
        }
 
        // Parent method
@@ -7577,22 +7627,42 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
 
        if ( change ) {
                if ( visible ) {
+
+                       if ( this.width ) {
+                               this.setIdealSize( this.width );
+                       } else if ( this.$floatableContainer ) {
+                               this.$clippable.css( 'width', 'auto' );
+                               this.setIdealSize(
+                                       this.$floatableContainer[ 0 ].offsetWidth > this.$clippable[ 0 ].offsetWidth ?
+                                               // Dropdown is smaller than handle so expand to width
+                                               this.$floatableContainer[ 0 ].offsetWidth :
+                                               // Dropdown is larger than handle so auto size
+                                               'auto'
+                               );
+                               this.$clippable.css( 'width', '' );
+                       }
+
                        this.togglePositioning( !!this.$floatableContainer );
                        this.toggleClipping( true );
 
                        this.bindKeyDownListener();
                        this.bindKeyPressListener();
 
-                       if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
-                               // If opening the menu downwards causes it to be clipped, flip it to open upwards instead
-                               belowHeight = this.$element.height();
-                               this.setVerticalPosition( 'above' );
+                       if (
+                               ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
+                               this.originalVerticalPosition !== 'center'
+                       ) {
+                               // If opening the menu in one direction causes it to be clipped, flip it
+                               originalHeight = this.$element.height();
+                               this.setVerticalPosition(
+                                       this.constructor.static.flippedPositions[ this.originalVerticalPosition ]
+                               );
                                if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
-                                       // If opening upwards also causes it to be clipped, flip it to open in whichever direction
+                                       // If flipping also causes it to be clipped, open in whichever direction
                                        // we have more space
-                                       aboveHeight = this.$element.height();
-                                       if ( aboveHeight < belowHeight ) {
-                                               this.setVerticalPosition( 'below' );
+                                       flippedHeight = this.$element.height();
+                                       if ( originalHeight > flippedHeight ) {
+                                               this.setVerticalPosition( this.originalVerticalPosition );
                                        }
                                }
                        }
@@ -9181,13 +9251,22 @@ OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
  * @param {Object} [config] Configuration options
  * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
  * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
+ * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
+ *  the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
+ *  containing `<div>` and has a larger area. By default, the menu uses relative positioning.
+ *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
  */
 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
        // Configuration initialization
        config = config || {};
 
        // Properties (must be done before parent constructor which calls #setDisabled)
-       this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
+       this.dropdownWidget = new OO.ui.DropdownWidget( $.extend(
+               {
+                       $overlay: config.$overlay
+               },
+               config.dropdown
+       ) );
        // Set up the options before parent constructor, which uses them to validate config.value.
        // Use this instead of setOptions() because this.$input is not set up yet.
        this.setOptionsData( config.options || [] );
@@ -10776,6 +10855,8 @@ OO.ui.MultilineTextInputWidget = function OoUiMultilineTextInputWidget( config )
        if ( this.autosize ) {
                this.$clone = this.$input
                        .clone()
+                       .removeAttr( 'id' )
+                       .removeAttr( 'name' )
                        .insertAfter( this.$input )
                        .attr( 'aria-hidden', 'true' )
                        .addClass( 'oo-ui-element-hidden' );
@@ -11249,7 +11330,14 @@ OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
  * - **inline**: The label is placed after the field-widget and aligned to the left.
  *   An inline-alignment is best used with checkboxes or radio buttons.
  *
- * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
+ * Help text can either be:
+ *
+ * - accessed via a help icon that appears in the upper right corner of the rendered field layout, or
+ * - shown as a subtle explanation below the label.
+ *
+ * If the help text is brief, or is essential to always espose it, set `helpInline` to `true`. If it
+ * is long or not essential, leave `helpInline` to its default, `false`.
+ *
  * Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
  *
  * [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
@@ -11262,15 +11350,25 @@ OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
  * @constructor
  * @param {OO.ui.Widget} fieldWidget Field widget
  * @param {Object} [config] Configuration options
- * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
- * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
+ * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
+ *  or 'inline'
+ * @cfg {Array} [errors] Error messages about the widget, which will be
+ *  displayed below the widget.
  *  The array may contain strings or OO.ui.HtmlSnippet instances.
- * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
+ * @cfg {Array} [notices] Notices about the widget, which will be displayed
+ *  below the widget.
  *  The array may contain strings or OO.ui.HtmlSnippet instances.
- * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
- *  in the upper-right corner of the rendered field; clicking it will display the text in a popup.
- *  For important messages, you are advised to use `notices`, as they are always shown.
- * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
+ *  These are more visible than `help` messages when `helpInline` is set, and so
+ *  might be good for transient messages.
+ * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
+ *  and `helpInline` is `false`, a "help" icon will appear in the upper-right
+ *  corner of the rendered field; clicking it will display the text in a popup.
+ *  If `helpInline` is `true`, then a subtle description will be shown after the
+ *  label.
+ * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
+ *  or shown when the "help" icon is clicked.
+ * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
+ * `help` is given.
  *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
  *
  * @throws {Error} An error is thrown if no widget is specified
@@ -11288,7 +11386,7 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
        }
 
        // Configuration initialization
-       config = $.extend( { align: 'left' }, config );
+       config = $.extend( { align: 'left', helpInline: false }, config );
 
        // Parent constructor
        OO.ui.FieldLayout.parent.call( this, config );
@@ -11308,49 +11406,29 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
        this.$header = $( '<span>' );
        this.$body = $( '<div>' );
        this.align = null;
-       if ( config.help ) {
-               this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
-                       $overlay: config.$overlay,
-                       popup: {
-                               padded: true
-                       },
-                       classes: [ 'oo-ui-fieldLayout-help' ],
-                       framed: false,
-                       icon: 'info',
-                       label: OO.ui.msg( 'ooui-field-help' )
-               } );
-               if ( config.help instanceof OO.ui.HtmlSnippet ) {
-                       this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
-               } else {
-                       this.popupButtonWidget.getPopup().$body.text( config.help );
-               }
-               this.$help = this.popupButtonWidget.$element;
-       } else {
-               this.$help = $( [] );
-       }
+       this.helpInline = config.helpInline;
 
        // Events
        this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
 
        // Initialization
-       if ( config.help ) {
-               // Set the 'aria-describedby' attribute on the fieldWidget
-               // Preference given to an input or a button
-               (
-                       this.fieldWidget.$input ||
-                       this.fieldWidget.$button ||
-                       this.fieldWidget.$element
-               ).attr(
-                       'aria-describedby',
-                       this.popupButtonWidget.getPopup().getBodyId()
-               );
-       }
+       this.$help = config.help ?
+               this.createHelpElement( config.help, config.$overlay ) :
+               $( [] );
        if ( this.fieldWidget.getInputId() ) {
                this.$label.attr( 'for', this.fieldWidget.getInputId() );
+               if ( this.helpInline ) {
+                       this.$help.attr( 'for', this.fieldWidget.getInputId() );
+               }
        } else {
                this.$label.on( 'click', function () {
                        this.fieldWidget.simulateLabelClick();
                }.bind( this ) );
+               if ( this.helpInline ) {
+                       this.$help.on( 'click', function () {
+                               this.fieldWidget.simulateLabelClick();
+                       }.bind( this ) );
+               }
        }
        this.$element
                .addClass( 'oo-ui-fieldLayout' )
@@ -11450,15 +11528,29 @@ OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
                        value = 'top';
                }
                // Reorder elements
-               if ( value === 'top' ) {
-                       this.$header.append( this.$help, this.$label );
-                       this.$body.append( this.$header, this.$field );
-               } else if ( value === 'inline' ) {
-                       this.$header.append( this.$help, this.$label );
-                       this.$body.append( this.$field, this.$header );
+
+               if ( this.helpInline ) {
+                       if ( value === 'top' ) {
+                               this.$header.append( this.$label );
+                               this.$body.append( this.$header, this.$field, this.$help );
+                       } else if ( value === 'inline' ) {
+                               this.$header.append( this.$label, this.$help );
+                               this.$body.append( this.$field, this.$header );
+                       } else {
+                               this.$header.append( this.$label, this.$help );
+                               this.$body.append( this.$header, this.$field );
+                       }
                } else {
-                       this.$header.append( this.$label );
-                       this.$body.append( this.$header, this.$help, this.$field );
+                       if ( value === 'top' ) {
+                               this.$header.append( this.$help, this.$label );
+                               this.$body.append( this.$header, this.$field );
+                       } else if ( value === 'inline' ) {
+                               this.$header.append( this.$help, this.$label );
+                               this.$body.append( this.$field, this.$header );
+                       } else {
+                               this.$header.append( this.$label );
+                               this.$body.append( this.$header, this.$help, this.$field );
+                       }
                }
                // Set classes. The following classes can be used here:
                // * oo-ui-fieldLayout-align-left
@@ -11540,6 +11632,56 @@ OO.ui.FieldLayout.prototype.formatTitleWithAccessKey = function ( title ) {
        return title;
 };
 
+/**
+ * Creates and returns the help element. Also sets the `aria-describedby`
+ * attribute on the main element of the `fieldWidget`.
+ *
+ * @private
+ * @param {string|OO.ui.HtmlSnippet} [help] Help text.
+ * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
+ * @return {jQuery} The element that should become `this.$help`.
+ */
+OO.ui.FieldLayout.prototype.createHelpElement = function ( help, $overlay ) {
+       var helpId, helpWidget;
+
+       if ( this.helpInline ) {
+               helpWidget = new OO.ui.LabelWidget( {
+                       label: help,
+                       classes: [ 'oo-ui-inline-help' ]
+               } );
+
+               helpId = helpWidget.getElementId();
+       } else {
+               helpWidget = new OO.ui.PopupButtonWidget( {
+                       $overlay: $overlay,
+                       popup: {
+                               padded: true
+                       },
+                       classes: [ 'oo-ui-fieldLayout-help' ],
+                       framed: false,
+                       icon: 'info',
+                       label: OO.ui.msg( 'ooui-field-help' )
+               } );
+               if ( help instanceof OO.ui.HtmlSnippet ) {
+                       helpWidget.getPopup().$body.html( help.toString() );
+               } else {
+                       helpWidget.getPopup().$body.text( help );
+               }
+
+               helpId = helpWidget.getPopup().getBodyId();
+       }
+
+       // Set the 'aria-describedby' attribute on the fieldWidget
+       // Preference given to an input or a button
+       (
+               this.fieldWidget.$input ||
+               this.fieldWidget.$button ||
+               this.fieldWidget.$element
+       ).attr( 'aria-describedby', helpId );
+
+       return helpWidget.$element;
+};
+
 /**
  * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
  * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
@@ -12342,4 +12484,4 @@ OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
 
 }( OO ) );
 
-//# sourceMappingURL=oojs-ui-core.js.map
\ No newline at end of file
+//# sourceMappingURL=oojs-ui-core.js.map.json
\ No newline at end of file