Update OOjs UI to v0.12.0
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
index 218acf3..98ec673 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.11.6
+ * OOjs UI v0.12.0
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2015 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2015-06-23T21:49:33Z
+ * Date: 2015-07-13T23:47:04Z
  */
 ( function ( OO ) {
 
@@ -1097,7 +1097,7 @@ OO.ui.Element.static.infuse = function ( idOrNode ) {
  */
 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) {
        // look for a cached result of a previous infusion.
-       var id, $elem, data, cls, obj;
+       var id, $elem, data, cls, parts, parent, obj;
        if ( typeof idOrNode === 'string' ) {
                id = idOrNode;
                $elem = $( document.getElementById( id ) );
@@ -1132,10 +1132,33 @@ OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) {
                // Special case: this is a raw Tag; wrap existing node, don't rebuild.
                return new OO.ui.Element( { $element: $elem } );
        }
-       cls = OO.ui[data._];
-       if ( !cls ) {
-               throw new Error( 'Unknown widget type: ' + id );
+       parts = data._.split( '.' );
+       cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
+       if ( cls === undefined ) {
+               // The PHP output might be old and not including the "OO.ui" prefix
+               // TODO: Remove this back-compat after next major release
+               cls = OO.getProp.apply( OO, [ OO.ui ].concat( parts ) );
+               if ( cls === undefined ) {
+                       throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
+               }
+       }
+
+       // Verify that we're creating an OO.ui.Element instance
+       parent = cls.parent;
+
+       while ( parent !== undefined ) {
+               if ( parent === OO.ui.Element ) {
+                       // Safe
+                       break;
+               }
+
+               parent = parent.parent;
        }
+
+       if ( parent !== OO.ui.Element ) {
+               throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
+       }
+
        $elem.data( 'ooui-infused', true ); // prevent loops
        data.id = id; // implicit
        data = OO.copy( data, null, function deserialize( value ) {
@@ -1436,11 +1459,12 @@ OO.ui.Element.static.getRootScrollableElement = function ( el ) {
  */
 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
        var i, val,
-               props = [ 'overflow' ],
+               // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
+               props = [ 'overflow-x', 'overflow-y' ],
                $parent = $( el ).parent();
 
        if ( dimension === 'x' || dimension === 'y' ) {
-               props.push( 'overflow-' + dimension );
+               props = [ 'overflow-' + dimension ];
        }
 
        while ( $parent.length ) {
@@ -5990,6 +6014,28 @@ OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
        return [];
 };
 
+/**
+ * Set the read-only state of the widget.
+ *
+ * This will also disable/enable the lookups functionality.
+ *
+ * @param {boolean} readOnly Make input read-only
+ * @chainable
+ */
+OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
+       // Parent method
+       // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
+       OO.ui.TextInputWidget.prototype.setReadOnly.call( this, readOnly );
+
+       this.setLookupsDisabled( readOnly );
+       // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
+       if ( readOnly && this.lookupMenu ) {
+               this.closeLookupMenu();
+       }
+
+       return this;
+};
+
 /**
  * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
  * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
@@ -6515,8 +6561,9 @@ OO.ui.mixin.ClippableElement.prototype.clip = function () {
                ccHeight = $container.innerHeight() - buffer,
                ccWidth = $container.innerWidth() - buffer,
                cWidth = this.$clippable.outerWidth() + buffer,
-               scrollTop = this.$clippableScroller[0] === this.$clippableWindow[0] ? this.$clippableScroller.scrollTop() : 0,
-               scrollLeft = this.$clippableScroller.scrollLeft(),
+               scrollerIsWindow = this.$clippableScroller[0] === this.$clippableWindow[0],
+               scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0,
+               scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0,
                desiredWidth = cOffset.left < 0 ?
                        cWidth + cOffset.left :
                        ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
@@ -6538,7 +6585,7 @@ OO.ui.mixin.ClippableElement.prototype.clip = function () {
        }
 
        // If we stopped clipping in at least one of the dimensions
-       if ( !clipWidth || !clipHeight ) {
+       if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
                OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
        }
 
@@ -8404,7 +8451,7 @@ OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
  * @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 {string} [help] Help text. When help text is specified, a help icon will appear
+ * @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.
  */
 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
@@ -8414,7 +8461,8 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
                fieldWidget = config.fieldWidget;
        }
 
-       var hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
+       var hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel,
+               div;
 
        // Configuration initialization
        config = $.extend( { align: 'left' }, config );
@@ -8437,10 +8485,14 @@ OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
                        icon: 'info'
                } );
 
+               div = $( '<div>' );
+               if ( config.help instanceof OO.ui.HtmlSnippet ) {
+                       div.html( config.help.toString() );
+               } else {
+                       div.text( config.help );
+               }
                this.popupButtonWidget.getPopup().$body.append(
-                       $( '<div>' )
-                               .text( config.help )
-                               .addClass( 'oo-ui-fieldLayout-help-content' )
+                       div.addClass( 'oo-ui-fieldLayout-help-content' )
                );
                this.$help = this.popupButtonWidget.$element;
        } else {
@@ -11517,7 +11569,8 @@ OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
 /**
  * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
  * Controls include moving items up and down, removing items, and adding different kinds of items.
- * ####Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.####
+ *
+ * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
  *
  * @class
  * @extends OO.ui.Widget
@@ -13038,7 +13091,9 @@ OO.ui.InputWidget = function OoUiInputWidget( config ) {
        this.$input
                .attr( 'name', config.name )
                .prop( 'disabled', this.isDisabled() );
-       this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
+       this.$element
+               .addClass( 'oo-ui-inputWidget' )
+               .append( this.$input );
        this.setValue( config.value );
 };
 
@@ -13273,6 +13328,14 @@ OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
 
+/* Static Properties */
+
+/**
+ * Disable generating `<label>` elements for buttons. One would very rarely need additional label
+ * for a button, and it's already a big clickable target, and it causes unexpected rendering.
+ */
+OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
+
 /* Methods */
 
 /**
@@ -13379,7 +13442,10 @@ OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
        OO.ui.CheckboxInputWidget.parent.call( this, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-checkboxInputWidget' );
+       this.$element
+               .addClass( 'oo-ui-checkboxInputWidget' )
+               // Required for pretty styling in MediaWiki theme
+               .append( $( '<span>' ) );
        this.setSelected( config.selected !== undefined ? config.selected : false );
 };
 
@@ -13521,6 +13587,7 @@ OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
  * @inheritdoc
  */
 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
+       value = this.cleanUpValue( value );
        this.dropdownWidget.getMenu().selectItemByData( value );
        OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
        return this;
@@ -13542,15 +13609,18 @@ OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
  * @chainable
  */
 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
-       var value = this.getValue();
+       var
+               value = this.getValue(),
+               widget = this;
 
        // Rebuild the dropdown menu
        this.dropdownWidget.getMenu()
                .clearItems()
                .addItems( options.map( function ( opt ) {
+                       var optValue = widget.cleanUpValue( opt.data );
                        return new OO.ui.MenuOptionWidget( {
-                               data: opt.data,
-                               label: opt.label !== undefined ? opt.label : opt.data
+                               data: optValue,
+                               label: opt.label !== undefined ? opt.label : optValue
                        } );
                } ) );
 
@@ -13633,7 +13703,10 @@ OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
        OO.ui.RadioInputWidget.parent.call( this, config );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-radioInputWidget' );
+       this.$element
+               .addClass( 'oo-ui-radioInputWidget' )
+               // Required for pretty styling in MediaWiki theme
+               .append( $( '<span>' ) );
        this.setSelected( config.selected !== undefined ? config.selected : false );
 };
 
@@ -13759,6 +13832,7 @@ OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
  * @inheritdoc
  */
 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
+       value = this.cleanUpValue( value );
        this.radioSelectWidget.selectItemByData( value );
        OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
        return this;
@@ -13780,15 +13854,18 @@ OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
  * @chainable
  */
 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
-       var value = this.getValue();
+       var
+               value = this.getValue(),
+               widget = this;
 
        // Rebuild the radioSelect menu
        this.radioSelectWidget
                .clearItems()
                .addItems( options.map( function ( opt ) {
+                       var optValue = widget.cleanUpValue( opt.data );
                        return new OO.ui.RadioOptionWidget( {
-                               data: opt.data,
-                               label: opt.label !== undefined ? opt.label : opt.data
+                               data: optValue,
+                               label: opt.label !== undefined ? opt.label : optValue
                        } );
                } ) );
 
@@ -13842,12 +13919,16 @@ OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
  * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
  * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
  * @cfg {boolean} [multiline=false] Allow multiple lines of text
+ * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
+ *  specifies minimum number of rows to display.
  * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
  *  Use the #maxRows config to specify a maximum number of displayed rows.
- * @cfg {boolean} [maxRows=10] Maximum number of rows to display when #autosize is set to true.
+ * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
+ *  Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
  * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
  *  the value or placeholder text: `'before'` or `'after'`
  * @cfg {boolean} [required=false] Mark the field as required
+ * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
  * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
  *  pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
  *  (the value must contain only numbers); when RegExp, a regular expression that must match the
@@ -13858,8 +13939,7 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        // Configuration initialization
        config = $.extend( {
                type: 'text',
-               labelPosition: 'after',
-               maxRows: 10
+               labelPosition: 'after'
        }, config );
 
        // Parent constructor
@@ -13875,7 +13955,8 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        this.readOnly = false;
        this.multiline = !!config.multiline;
        this.autosize = !!config.autosize;
-       this.maxRows = config.maxRows;
+       this.minRows = config.rows !== undefined ? config.rows : '';
+       this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
        this.validate = null;
 
        // Clone for resizing
@@ -13921,6 +14002,12 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
                this.$input.attr( 'required', 'required' );
                this.$input.attr( 'aria-required', 'true' );
        }
+       if ( config.autocomplete === false ) {
+               this.$input.attr( 'autocomplete', 'off' );
+       }
+       if ( this.multiline && config.rows ) {
+               this.$input.attr( 'rows', config.rows );
+       }
        if ( this.label || config.autosize ) {
                this.installParentChangeDetector();
        }
@@ -14109,7 +14196,7 @@ OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
                };
 
                // Create a fake parent and observe it
-               fakeParentNode = $( '<div>' ).append( this.$element )[0];
+               fakeParentNode = $( '<div>' ).append( topmostNode )[0];
                mutationObserver.observe( fakeParentNode, { childList: true } );
        } else {
                // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
@@ -14131,7 +14218,7 @@ OO.ui.TextInputWidget.prototype.adjustSize = function () {
        if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
                this.$clone
                        .val( this.$input.val() )
-                       .attr( 'rows', '' )
+                       .attr( 'rows', this.minRows )
                        // Set inline height property to 0 to measure scroll height
                        .css( 'height', 0 );
 
@@ -14707,6 +14794,7 @@ OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
        this.$element
                .data( 'oo-ui-optionWidget', this )
                .attr( 'role', 'option' )
+               .attr( 'aria-selected', 'false' )
                .addClass( 'oo-ui-optionWidget' )
                .append( this.$label );
 };
@@ -14992,8 +15080,13 @@ OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
        this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
 
        // Initialization
+       // Remove implicit role, we're handling it ourselves
+       this.radio.$input.attr( 'role', 'presentation' );
        this.$element
                .addClass( 'oo-ui-radioOptionWidget' )
+               .attr( 'role', 'radio' )
+               .attr( 'aria-checked', 'false' )
+               .removeAttr( 'aria-selected' )
                .prepend( this.radio.$element );
 };
 
@@ -15029,6 +15122,9 @@ OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
        OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
 
        this.radio.setSelected( state );
+       this.$element
+               .attr( 'aria-checked', state.toString() )
+               .removeAttr( 'aria-selected' );
 
        return this;
 };
@@ -15832,10 +15928,6 @@ OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
                change: 'onQueryChange',
                enter: 'onQueryEnter'
        } );
-       this.results.connect( this, {
-               highlight: 'onResultsHighlight',
-               select: 'onResultsSelect'
-       } );
        this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
 
        // Initialization
@@ -15854,28 +15946,6 @@ OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
 
 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
 
-/* Events */
-
-/**
- * A 'highlight' event is emitted when an item is highlighted. The highlight indicates which
- * item will be selected. When a user mouses over a menu item, it is highlighted. If a search
- * string is typed into the query field instead, the first menu item that matches the query
- * will be highlighted.
-
- * @event highlight
- * @deprecated Connect straight to getResults() events instead
- * @param {Object|null} item Item data or null if no item is highlighted
- */
-
-/**
- * A 'select' event is emitted when an item is selected. A menu item is selected when it is clicked,
- * or when a user types a search query, a menu result is highlighted, and the user presses enter.
- *
- * @event select
- * @deprecated Connect straight to getResults() events instead
- * @param {Object|null} item Item data or null if no item is selected
- */
-
 /* Methods */
 
 /**
@@ -15915,38 +15985,14 @@ OO.ui.SearchWidget.prototype.onQueryChange = function () {
 /**
  * Handle select widget enter key events.
  *
- * Selects highlighted item.
+ * Chooses highlighted item.
  *
  * @private
  * @param {string} value New value
  */
 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
        // Reset
-       this.results.selectItem( this.results.getHighlightedItem() );
-};
-
-/**
- * Handle select widget highlight events.
- *
- * @private
- * @deprecated Connect straight to getResults() events instead
- * @param {OO.ui.OptionWidget} item Highlighted item
- * @fires highlight
- */
-OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
-       this.emit( 'highlight', item ? item.getData() : null );
-};
-
-/**
- * Handle select widget select events.
- *
- * @private
- * @deprecated Connect straight to getResults() events instead
- * @param {OO.ui.OptionWidget} item Selected item
- * @fires select
- */
-OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
-       this.emit( 'select', item ? item.getData() : null );
+       this.results.chooseItem( this.results.getHighlightedItem() );
 };
 
 /**
@@ -16863,7 +16909,9 @@ OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
        } );
 
        // Initialization
-       this.$element.addClass( 'oo-ui-radioSelectWidget' );
+       this.$element
+               .addClass( 'oo-ui-radioSelectWidget' )
+               .attr( 'role', 'radiogroup' );
 };
 
 /* Setup */
@@ -17258,7 +17306,7 @@ OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
  * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
  * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
  *
- * ####Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.####
+ * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
  *
  * @class
  * @extends OO.ui.SelectWidget
@@ -17292,7 +17340,7 @@ OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
 /**
  * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
  *
- * ####Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.####
+ * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
  *
  * @class
  * @extends OO.ui.SelectWidget