Update OOjs UI to v0.1.0-pre (880100c45e)
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
index ea086a1..7dd3735 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * OOjs UI v0.1.0-pre (bd008e8aed)
+ * OOjs UI v0.1.0-pre (880100c45e)
  * https://www.mediawiki.org/wiki/OOjs_UI
  *
  * Copyright 2011–2014 OOjs Team and other contributors.
  * Released under the MIT license
  * http://oojs.mit-license.org
  *
- * Date: 2014-09-11T23:10:10Z
+ * Date: 2014-09-23T22:28:43Z
  */
 ( function ( OO ) {
 
@@ -188,6 +188,88 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) {
 
 } )();
 
+/**
+ * Element that can be marked as pending.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.PendingElement = function OoUiPendingElement( config ) {
+       // Config initialisation
+       config = config || {};
+
+       // Properties
+       this.pending = 0;
+       this.$pending = null;
+
+       // Initialisation
+       this.setPendingElement( config.$pending || this.$element );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.PendingElement );
+
+/* Methods */
+
+/**
+ * Set the pending element (and clean up any existing one).
+ *
+ * @param {jQuery} $pending The element to set to pending.
+ */
+OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) {
+       if ( this.$pending ) {
+               this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
+       }
+
+       this.$pending = $pending;
+       if ( this.pending > 0 ) {
+               this.$pending.addClass( 'oo-ui-pendingElement-pending' );
+       }
+};
+
+/**
+ * Check if input is pending.
+ *
+ * @return {boolean}
+ */
+OO.ui.PendingElement.prototype.isPending = function () {
+       return !!this.pending;
+};
+
+/**
+ * Increase the pending stack.
+ *
+ * @chainable
+ */
+OO.ui.PendingElement.prototype.pushPending = function () {
+       if ( this.pending === 0 ) {
+               this.$pending.addClass( 'oo-ui-pendingElement-pending' );
+       }
+       this.pending++;
+
+       return this;
+};
+
+/**
+ * Reduce the pending stack.
+ *
+ * Clamped at zero.
+ *
+ * @chainable
+ */
+OO.ui.PendingElement.prototype.popPending = function () {
+       if ( this.pending === 1 ) {
+               this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
+       }
+       this.pending = Math.max( 0, this.pending - 1 );
+
+       return this;
+};
+
 /**
  * List of actions.
  *
@@ -1562,17 +1644,26 @@ OO.ui.Window.prototype.getSize = function () {
  * @return {number} Content height
  */
 OO.ui.Window.prototype.getContentHeight = function () {
+       // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements
+       var bodyHeight, oldHeight = this.$frame[0].style.height;
+       this.$frame[0].style.height = '1px';
+       bodyHeight = this.getBodyHeight();
+       this.$frame[0].style.height = oldHeight;
+
        return Math.round(
                // Add buffer for border
                ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
                // Use combined heights of children
-               ( this.$head.outerHeight( true ) + this.getBodyHeight() + this.$foot.outerHeight( true ) )
+               ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
        );
 };
 
 /**
  * Get the height of the dialog contents.
  *
+ * When this function is called, the dialog will temporarily have been resized
+ * to height=1px, so .scrollHeight measurements can be taken accurately.
+ *
  * @return {number} Height of content
  */
 OO.ui.Window.prototype.getBodyHeight = function () {
@@ -2056,6 +2147,7 @@ OO.ui.Window.prototype.load = function () {
  * @abstract
  * @class
  * @extends OO.ui.Window
+ * @mixins OO.ui.PendingElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -2064,11 +2156,13 @@ OO.ui.Dialog = function OoUiDialog( config ) {
        // Parent constructor
        OO.ui.Dialog.super.call( this, config );
 
+       // Mixin constructors
+       OO.ui.PendingElement.call( this );
+
        // Properties
        this.actions = new OO.ui.ActionSet();
        this.attachedActions = [];
        this.currentAction = null;
-       this.pending = 0;
 
        // Events
        this.actions.connect( this, {
@@ -2086,6 +2180,7 @@ OO.ui.Dialog = function OoUiDialog( config ) {
 /* Setup */
 
 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
+OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement );
 
 /* Static Properties */
 
@@ -2173,15 +2268,6 @@ OO.ui.Dialog.prototype.onActionsChange = function () {
        }
 };
 
-/**
- * Check if input is pending.
- *
- * @return {boolean}
- */
-OO.ui.Dialog.prototype.isPending = function () {
-       return !!this.pending;
-};
-
 /**
  * Get set of actions.
  *
@@ -2216,7 +2302,7 @@ OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
  * @inheritdoc
  *
  * @param {Object} [data] Dialog opening data
- * @param {jQuery|string|Function|null} [data.label] Dialog label, omit to use #static-label
+ * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
  * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
  *   action item, omit to use #static-actions
  */
@@ -2272,6 +2358,7 @@ OO.ui.Dialog.prototype.initialize = function () {
 
        // Initialization
        this.$content.addClass( 'oo-ui-dialog-content' );
+       this.setPendingElement( this.$head );
 };
 
 /**
@@ -2309,38 +2396,6 @@ OO.ui.Dialog.prototype.executeAction = function ( action ) {
                .always( OO.ui.bind( this.popPending, this ) );
 };
 
-/**
- * Increase the pending stack.
- *
- * @chainable
- */
-OO.ui.Dialog.prototype.pushPending = function () {
-       if ( this.pending === 0 ) {
-               this.$content.addClass( 'oo-ui-actionDialog-content-pending' );
-               this.$head.addClass( 'oo-ui-texture-pending' );
-       }
-       this.pending++;
-
-       return this;
-};
-
-/**
- * Reduce the pending stack.
- *
- * Clamped at zero.
- *
- * @chainable
- */
-OO.ui.Dialog.prototype.popPending = function () {
-       if ( this.pending === 1 ) {
-               this.$content.removeClass( 'oo-ui-actionDialog-content-pending' );
-               this.$head.removeClass( 'oo-ui-texture-pending' );
-       }
-       this.pending = Math.max( 0, this.pending - 1 );
-
-       return this;
-};
-
 /**
  * Collection of windows.
  *
@@ -2754,7 +2809,6 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
                                        manager.opening.notify( { state: 'setup' } );
                                        setTimeout( function () {
                                                win.ready( data ).then( function () {
-                                                       manager.updateWindowSize( win );
                                                        manager.opening.notify( { state: 'ready' } );
                                                        manager.opening = null;
                                                        manager.opened = $.Deferred();
@@ -4679,7 +4733,7 @@ OO.ui.ClippableElement.prototype.isClippedVertically = function () {
 };
 
 /**
- * Set the ideal size.
+ * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
  *
  * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
  * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
@@ -4687,6 +4741,12 @@ OO.ui.ClippableElement.prototype.isClippedVertically = function () {
 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
        this.idealWidth = width;
        this.idealHeight = height;
+
+       if ( !this.clipping ) {
+               // Update dimensions
+               this.$clippable.css( { width: width, height: height } );
+       }
+       // While clipping, idealWidth and idealHeight are not considered
 };
 
 /**
@@ -6429,15 +6489,19 @@ OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) {
        if ( config.help ) {
                this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
                        $: this.$,
+                       classes: [ 'oo-ui-fieldLayout-help' ],
                        framed: false,
-                       icon: 'info',
-                       title: config.help
+                       icon: 'info'
                } );
 
-               this.popupButtonWidget.getPopup().$body.append( this.$( '<span>' ).text( config.help ) );
+               this.popupButtonWidget.getPopup().$body.append(
+                       this.$( '<div>' )
+                               .text( config.help )
+                               .addClass( 'oo-ui-fieldLayout-help-content' )
+               );
                this.$help = this.popupButtonWidget.$element;
        } else {
-               this.$help = this.$( '<div>' );
+               this.$help = this.$( [] );
        }
 
        // Events
@@ -8148,11 +8212,13 @@ OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
  *
  * @class
  * @extends OO.ui.ButtonWidget
+ * @mixins OO.ui.PendingElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
  * @cfg {string} [action] Symbolic action name
  * @cfg {string[]} [modes] Symbolic mode names
+ * @cfg {boolean} [framed=false] Render button with a frame
  */
 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
        // Config intialization
@@ -8161,6 +8227,9 @@ OO.ui.ActionWidget = function OoUiActionWidget( config ) {
        // Parent constructor
        OO.ui.ActionWidget.super.call( this, config );
 
+       // Mixin constructors
+       OO.ui.PendingElement.call( this, config );
+
        // Properties
        this.action = config.action || '';
        this.modes = config.modes || [];
@@ -8174,6 +8243,7 @@ OO.ui.ActionWidget = function OoUiActionWidget( config ) {
 /* Setup */
 
 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
+OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement );
 
 /* Events */
 
@@ -8592,6 +8662,7 @@ OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) {
  * @abstract
  * @class
  * @extends OO.ui.Widget
+ * @mixins OO.ui.FlaggedElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -8607,6 +8678,9 @@ OO.ui.InputWidget = function OoUiInputWidget( config ) {
        // Parent constructor
        OO.ui.InputWidget.super.call( this, config );
 
+       // Mixin constructors
+       OO.ui.FlaggedElement.call( this, config );
+
        // Properties
        this.$input = this.getInputElement( config );
        this.value = '';
@@ -8628,6 +8702,7 @@ OO.ui.InputWidget = function OoUiInputWidget( config ) {
 /* Setup */
 
 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
+OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
 
 /* Events */
 
@@ -8868,6 +8943,7 @@ OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
  * @extends OO.ui.InputWidget
  * @mixins OO.ui.IconElement
  * @mixins OO.ui.IndicatorElement
+ * @mixins OO.ui.PendingElement
  *
  * @constructor
  * @param {Object} [config] Configuration options
@@ -8875,6 +8951,8 @@ OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
  * @cfg {boolean} [multiline=false] Allow multiple lines of text
  * @cfg {boolean} [autosize=false] Automatically resize to fit content
  * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
+ * @cfg {RegExp|string} [validate] Regular expression (or symbolic name referencing
+ *  one, see #static-validationPatterns)
  */
 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        // Configuration initialization
@@ -8886,15 +8964,19 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
        // Mixin constructors
        OO.ui.IconElement.call( this, config );
        OO.ui.IndicatorElement.call( this, config );
+       OO.ui.PendingElement.call( this, config );
 
        // Properties
-       this.pending = 0;
        this.multiline = !!config.multiline;
        this.autosize = !!config.autosize;
        this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
+       this.validate = config.validate || null;
 
        // Events
-       this.$input.on( 'keypress', OO.ui.bind( this.onKeyPress, this ) );
+       this.$input.on( {
+               keypress: OO.ui.bind( this.onKeyPress, this ),
+               blur: OO.ui.bind( this.setValidityFlag, this )
+       } );
        this.$element.on( 'DOMNodeInsertedIntoDocument', OO.ui.bind( this.onElementAttach, this ) );
        this.$icon.on( 'mousedown', OO.ui.bind( this.onIconMouseDown, this ) );
        this.$indicator.on( 'mousedown', OO.ui.bind( this.onIndicatorMouseDown, this ) );
@@ -8914,6 +8996,14 @@ OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
+OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
+
+/* Static properties */
+
+OO.ui.TextInputWidget.static.validationPatterns = {
+       'non-empty': /.+/,
+       integer: /^\d+$/
+};
 
 /* Events */
 
@@ -9005,6 +9095,7 @@ OO.ui.TextInputWidget.prototype.setValue = function ( value ) {
        // Parent method
        OO.ui.TextInputWidget.super.prototype.setValue.call( this, value );
 
+       this.setValidityFlag();
        this.adjustSize();
        return this;
 };
@@ -9077,54 +9168,38 @@ OO.ui.TextInputWidget.prototype.isAutosizing = function () {
 };
 
 /**
- * Check if input is pending.
- *
- * @return {boolean}
- */
-OO.ui.TextInputWidget.prototype.isPending = function () {
-       return !!this.pending;
-};
-
-/**
- * Increase the pending stack.
+ * Select the contents of the input.
  *
  * @chainable
  */
-OO.ui.TextInputWidget.prototype.pushPending = function () {
-       if ( this.pending === 0 ) {
-               this.$element.addClass( 'oo-ui-textInputWidget-pending' );
-               this.$input.addClass( 'oo-ui-texture-pending' );
-       }
-       this.pending++;
-
+OO.ui.TextInputWidget.prototype.select = function () {
+       this.$input.select();
        return this;
 };
 
 /**
- * Reduce the pending stack.
- *
- * Clamped at zero.
- *
- * @chainable
+ * Sets the 'invalid' flag appropriately.
  */
-OO.ui.TextInputWidget.prototype.popPending = function () {
-       if ( this.pending === 1 ) {
-               this.$element.removeClass( 'oo-ui-textInputWidget-pending' );
-               this.$input.removeClass( 'oo-ui-texture-pending' );
-       }
-       this.pending = Math.max( 0, this.pending - 1 );
-
-       return this;
+OO.ui.TextInputWidget.prototype.setValidityFlag = function () {
+       this.isValid().done( OO.ui.bind( function ( valid ) {
+               this.setFlags( { invalid: !valid } );
+       }, this ) );
 };
 
 /**
- * Select the contents of the input.
+ * Returns whether or not the current value is considered valid, according to the
+ * supplied validation pattern.
  *
- * @chainable
+ * @return {jQuery.Deferred}
  */
-OO.ui.TextInputWidget.prototype.select = function () {
-       this.$input.select();
-       return this;
+OO.ui.TextInputWidget.prototype.isValid = function () {
+       var validationRegexp;
+       if ( this.validate instanceof RegExp ) {
+               validationRegexp = this.validate;
+       } else {
+               validationRegexp = this.constructor.static.validationPatterns[this.validate];
+       }
+       return $.Deferred().resolve( !!this.getValue().match( validationRegexp ) ).promise();
 };
 
 /**
@@ -10052,6 +10127,9 @@ OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
                this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
        }
 
+       // Reevaluate clipping state since we've relocated and resized the popup
+       this.clip();
+
        return this;
 };
 
@@ -11075,6 +11153,13 @@ OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) {
 
        var change = visible !== this.isVisible();
 
+       if ( change && visible ) {
+               // Make sure the width is set before the parent method runs.
+               // After this we have to call this.position(); again to actually
+               // position ourselves correctly.
+               this.position();
+       }
+
        // Parent method
        OO.ui.TextInputMenuWidget.super.prototype.toggle.call( this, visible );
 
@@ -11086,6 +11171,7 @@ OO.ui.TextInputMenuWidget.prototype.toggle = function ( visible ) {
                        this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
                }
        }
+
        return this;
 };
 
@@ -11120,6 +11206,8 @@ OO.ui.TextInputMenuWidget.prototype.position = function () {
        }
        this.$element.css( dimensions );
        this.setIdealSize( $container.width() );
+       // We updated the position, so re-evaluate the clipping state
+       this.clip();
 
        return this;
 };