/*!
- * OOjs UI v0.1.0-pre (98e770ce46)
+ * OOjs UI v0.1.0-pre (d74a46ca6a)
* 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-26T22:57:14Z
+ * Date: 2014-10-15T00:40:17Z
*/
( function ( OO ) {
OO.ui.PendingElement.prototype.pushPending = function () {
if ( this.pending === 0 ) {
this.$pending.addClass( 'oo-ui-pendingElement-pending' );
+ this.updateThemeClasses();
}
this.pending++;
OO.ui.PendingElement.prototype.popPending = function () {
if ( this.pending === 1 ) {
this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
+ this.updateThemeClasses();
}
this.pending = Math.max( 0, this.pending - 1 );
this.$ = config.$ || OO.ui.Element.getJQuery( document );
this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
this.elementGroup = null;
+ this.debouncedUpdateThemeClassesHandler = OO.ui.bind(
+ this.debouncedUpdateThemeClasses, this
+ );
+ this.updateThemeClassesPending = false;
// Initialization
if ( $.isArray( config.classes ) ) {
}
};
+/**
+ * Bind a handler for an event on a DOM element.
+ *
+ * Used to be for working around a jQuery bug (jqbug.com/14180),
+ * but obsolete as of jQuery 1.11.0.
+ *
+ * @static
+ * @deprecated Use jQuery#on instead.
+ * @param {HTMLElement|jQuery} el DOM element
+ * @param {string} event Event to bind
+ * @param {Function} callback Callback to call when the event fires
+ */
+OO.ui.Element.onDOMEvent = function ( el, event, callback ) {
+ $( el ).on( event, callback );
+};
+
+/**
+ * Unbind a handler bound with #static-method-onDOMEvent.
+ *
+ * @deprecated Use jQuery#off instead.
+ * @static
+ * @param {HTMLElement|jQuery} el DOM element
+ * @param {string} event Event to unbind
+ * @param {Function} [callback] Callback to unbind
+ */
+OO.ui.Element.offDOMEvent = function ( el, event, callback ) {
+ $( el ).off( event, callback );
+};
+
/* Methods */
+/**
+ * Update the theme-provided classes.
+ *
+ * @localdoc This is called in element mixins and widget classes anytime state changes.
+ * Updating is debounced, minimizing overhead of changing multiple attributes and
+ * guaranteeing that theme updates do not occur within an element's constructor
+ */
+OO.ui.Element.prototype.updateThemeClasses = function () {
+ if ( !this.updateThemeClassesPending ) {
+ this.updateThemeClassesPending = true;
+ setTimeout( this.debouncedUpdateThemeClassesHandler );
+ }
+};
+
+/**
+ * @private
+ */
+OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
+ OO.ui.theme.updateElementClasses( this );
+ this.updateThemeClassesPending = false;
+};
+
/**
* Get the HTML tag name.
*
OO.ui.Element.offDOMEvent( this.$element, event, callback );
};
-( function () {
- /**
- * Bind a handler for an event on a DOM element.
- *
- * Used to be for working around a jQuery bug (jqbug.com/14180),
- * but obsolete as of jQuery 1.11.0.
- *
- * @static
- * @deprecated Use jQuery#on instead.
- * @param {HTMLElement|jQuery} el DOM element
- * @param {string} event Event to bind
- * @param {Function} callback Callback to call when the event fires
- */
- OO.ui.Element.onDOMEvent = function ( el, event, callback ) {
- $( el ).on( event, callback );
- };
-
- /**
- * Unbind a handler bound with #static-method-onDOMEvent.
- *
- * @deprecated Use jQuery#off instead.
- * @static
- * @param {HTMLElement|jQuery} el DOM element
- * @param {string} event Event to unbind
- * @param {Function} [callback] Callback to unbind
- */
- OO.ui.Element.offDOMEvent = function ( el, event, callback ) {
- $( el ).off( event, callback );
- };
-}() );
-
/**
* Container for elements.
*
this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
this.emit( 'disable', isDisabled );
+ this.updateThemeClasses();
}
this.wasDisabled = isDisabled;
this.loading = null;
this.size = config.size || this.constructor.static.size;
this.$frame = this.$( '<div>' );
+ this.$overlay = this.$( '<div>' );
// Initialization
this.$element
.addClass( 'oo-ui-window' )
- .append( this.$frame );
+ .append( this.$frame, this.$overlay );
this.$frame.addClass( 'oo-ui-window-frame' );
+ this.$overlay.addClass( 'oo-ui-window-overlay' );
// NOTE: Additional intitialization will occur when #setManager is called
};
OO.inheritClass( OO.ui.Window, OO.ui.Element );
OO.mixinClass( OO.ui.Window, OO.EventEmitter );
-/* Events */
-
-/**
- * @event resize
- * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
- */
-
/* Static Properties */
/**
this.$head = this.$( '<div>' );
this.$body = this.$( '<div>' );
this.$foot = this.$( '<div>' );
- this.$overlay = this.$( '<div>' );
+ this.$innerOverlay = this.$( '<div>' );
// Events
this.$element.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) );
this.$head.addClass( 'oo-ui-window-head' );
this.$body.addClass( 'oo-ui-window-body' );
this.$foot.addClass( 'oo-ui-window-foot' );
- this.$overlay.addClass( 'oo-ui-window-overlay' );
- this.$content.append( this.$head, this.$body, this.$foot, this.$overlay );
+ this.$innerOverlay.addClass( 'oo-ui-window-inner-overlay' );
+ this.$content.append( this.$head, this.$body, this.$foot, this.$innerOverlay );
return this;
};
doc.close();
// Properties
- this.$ = OO.ui.Element.getJQuery( doc, this.$element );
+ this.$ = OO.ui.Element.getJQuery( doc, this.$iframe );
this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
this.$document = this.$( doc );
* @param {Object} data Window closing data
*/
+/**
+ * Window was resized.
+ *
+ * @event resize
+ * @param {OO.ui.Window} win Window that was resized
+ */
+
/* Static Properties */
/**
this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
win.setDimensions( sizes[size] );
+ this.emit( 'resize', win );
+
return this;
};
];
};
+/**
+ * Theme logic.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+OO.ui.Theme = function OoUiTheme( config ) {
+ // Initialize config
+ config = config || {};
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.Theme );
+
+/* Methods */
+
+/**
+ * Get a list of classes to be applied to a widget.
+ *
+ * @localdoc The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or
+ * removes, otherwise state transitions will not work properly.
+ *
+ * @param {OO.ui.Element} element Element for which to get classes
+ * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
+ */
+OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
+ return { on: [], off: [] };
+};
+
+/**
+ * Update CSS classes provided by the theme.
+ *
+ * For elements with theme logic hooks, this should be called anytime there's a state change.
+ *
+ * @param {OO.ui.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 classes = this.getElementClasses( element );
+
+ element.$element
+ .removeClass( classes.off.join( ' ' ) )
+ .addClass( classes.on.join( ' ' ) );
+};
+
/**
* Element with a button.
*
this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
};
+/**
+ * Check if button has a frame.
+ *
+ * @return {boolean} Button is framed
+ */
+OO.ui.ButtonElement.prototype.isFramed = function () {
+ return this.framed;
+};
+
/**
* Toggle frame.
*
this.$element
.toggleClass( 'oo-ui-buttonElement-frameless', !framed )
.toggleClass( 'oo-ui-buttonElement-framed', framed );
+ this.updateThemeClasses();
}
return this;
}
this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
+ this.updateThemeClasses();
return this;
};
}
this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
+ this.updateThemeClasses();
return this;
};
this.$flagged.removeClass( remove.join( ' ' ) );
}
+ this.updateThemeClasses();
this.emit( 'flag', changes );
return this;
.removeClass( remove.join( ' ' ) );
}
+ this.updateThemeClasses();
this.emit( 'flag', changes );
return this;
clipHeight = desiredHeight < naturalHeight;
if ( clipWidth ) {
- this.$clippable.css( { overflowX: 'auto', width: desiredWidth } );
+ this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
} else {
this.$clippable.css( 'width', this.idealWidth || '' );
this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
this.$clippable.css( 'overflowX', '' );
}
if ( clipHeight ) {
- this.$clippable.css( { overflowY: 'auto', height: desiredHeight } );
+ this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
} else {
this.$clippable.css( 'height', this.idealHeight || '' );
this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.IconElement
+ * @mixins OO.ui.FlaggedElement
*
* @constructor
* @param {OO.ui.ToolGroup} toolGroup
// Mixin constructors
OO.ui.IconElement.call( this, config );
+ OO.ui.FlaggedElement.call( this, config );
// Properties
this.toolGroup = toolGroup;
OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
+OO.mixinClass( OO.ui.Tool, OO.ui.FlaggedElement );
/* Events */
group.type = 'list';
}
if ( group.label === undefined ) {
- group.label = 'ooui-toolbar-more';
+ group.label = OO.ui.msg( 'ooui-toolbar-more' );
}
}
// Check type has been registered
* @constructor
* @param {OO.ui.TextInputWidget} input Input widget
* @param {Object} [config] Configuration options
- * @cfg {jQuery} [$overlay=this.$( 'body' )] Overlay layer
+ * @cfg {jQuery} [$overlay] Overlay layer; defaults to the current window's overlay.
*/
OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
// Config intialization
// Properties
this.lookupInput = input;
- this.$overlay = config.$overlay || this.$( 'body,.oo-ui-window-overlay' ).last();
+ this.$overlay = config.$overlay || ( this.$.$iframe || this.$element ).closest( '.oo-ui-window' ).children( '.oo-ui-window-overlay' );
+ if ( this.$overlay.length === 0 ) {
+ this.$overlay = this.$( 'body' );
+ }
this.lookupMenu = new OO.ui.TextInputMenuWidget( this, {
$: OO.ui.Element.getJQuery( this.$overlay ),
input: this.lookupInput,
*/
OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
// Configuration initialization
- config = $.extend( { icon: 'add-item' }, config );
+ config = $.extend( { icon: 'add' }, config );
// Parent constructor
OO.ui.OutlineControlsWidget.super.call( this, config );
* Inline menus provide a control for accessing a menu and compose a menu within the widget, which
* can be accessed using the #getMenu method.
*
- * Use with OO.ui.MenuOptionWidget.
+ * Use with OO.ui.MenuItemWidget.
*
* @class
* @extends OO.ui.Widget
this.multiline = !!config.multiline;
this.autosize = !!config.autosize;
this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
- this.validate = config.validate || null;
+ this.validate = null;
+
+ this.setValidation( config.validate );
// Events
this.$input.on( {
* @chainable
*/
OO.ui.TextInputWidget.prototype.adjustSize = function () {
- var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, idealHeight;
+ var $clone, scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
if ( this.multiline && this.autosize ) {
$clone = this.$input.clone()
.val( this.$input.val() )
+ // Set inline height property to 0 to measure scroll height
.css( { height: 0 } )
.insertAfter( this.$input );
- // Set inline height property to 0 to measure scroll height
scrollHeight = $clone[0].scrollHeight;
// Remove inline height property to measure natural heights
$clone.css( 'height', '' );
innerHeight = $clone.innerHeight();
outerHeight = $clone.outerHeight();
// Measure max rows height
- $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' );
+ $clone.attr( 'rows', this.maxRows ).css( 'height', 'auto' ).val( '' );
maxInnerHeight = $clone.innerHeight();
- $clone.removeAttr( 'rows' ).css( 'height', '' );
+ // Difference between reported innerHeight and scrollHeight with no scrollbars present
+ // Equals 1 on Blink-based browsers and 0 everywhere else
+ measurementError = maxInnerHeight - $clone[0].scrollHeight;
$clone.remove();
- idealHeight = Math.min( maxInnerHeight, scrollHeight );
+ idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
// Only apply inline height when expansion beyond natural height is needed
- this.$input.css(
- 'height',
+ if ( idealHeight > innerHeight ) {
// Use the difference between the inner and outer height as a buffer
- idealHeight > outerHeight ? idealHeight + ( outerHeight - innerHeight ) : ''
- );
+ this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
+ } else {
+ this.$input.css( 'height', '' );
+ }
}
return this;
};
return this;
};
+/**
+ * Sets the validation pattern to use.
+ * @param validate {RegExp|string|null} Regular expression (or symbolic name referencing
+ * one, see #static-validationPatterns)
+ */
+OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
+ if ( validate instanceof RegExp ) {
+ this.validate = validate;
+ } else {
+ this.validate = this.constructor.static.validationPatterns[validate] || /.*/;
+ }
+};
+
/**
* Sets the 'invalid' flag appropriately.
*/
* @return {jQuery.Deferred}
*/
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();
+ return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
};
/**
* @param {Object} [config] Configuration options
* @cfg {Object} [menu] Configuration options to pass to menu widget
* @cfg {Object} [input] Configuration options to pass to input widget
+ * @cfg {jQuery} [$overlay] Overlay layer; defaults to the current window's overlay.
*/
OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
// Configuration initialization
OO.ui.ComboBoxWidget.super.call( this, config );
// Properties
+ this.$overlay = config.$overlay || ( this.$.$iframe || this.$element ).closest( '.oo-ui-window' ).children( '.oo-ui-window-overlay' );
+ if ( this.$overlay.length === 0 ) {
+ this.$overlay = this.$( 'body' );
+ }
this.input = new OO.ui.TextInputWidget( $.extend(
{ $: this.$, indicator: 'down', disabled: this.isDisabled() },
config.input
) );
- this.menu = new OO.ui.MenuWidget( $.extend(
+ this.menu = new OO.ui.TextInputMenuWidget( this.input, $.extend(
{ $: this.$, widget: this, input: this.input, disabled: this.isDisabled() },
config.menu
) );
} );
// Initialization
- this.$element.addClass( 'oo-ui-comboBoxWidget' ).append(
- this.input.$element,
- this.menu.$element
- );
+ this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
+ this.$overlay.append( this.menu.$element );
this.onMenuItemsChange();
};
// Mixin constructors
OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
+ OO.ui.TitledElement.call( this, config );
// Properties
this.input = config.input;
OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
+OO.mixinClass( OO.ui.LabelWidget, OO.ui.TitledElement );
/* Static Properties */
if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
this.scrollElementIntoView();
}
+ this.updateThemeClasses();
}
return this;
};
if ( this.constructor.static.highlightable ) {
this.highlighted = !!state;
this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
+ this.updateThemeClasses();
}
return this;
};
if ( this.constructor.static.pressable ) {
this.pressed = !!state;
this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
+ this.updateThemeClasses();
}
return this;
};
*/
OO.ui.OutlineItemWidget.prototype.setMovable = function ( movable ) {
this.movable = !!movable;
+ this.updateThemeClasses();
return this;
};
*/
OO.ui.OutlineItemWidget.prototype.setRemovable = function ( removable ) {
this.removable = !!removable;
+ this.updateThemeClasses();
return this;
};
this.$element.removeClass( levelClass + i );
}
}
+ this.updateThemeClasses();
return this;
};
}
// Position body relative to anchor
- this.$popup.css( 'left', popupOffset );
+ this.$popup.css( 'margin-left', popupOffset );
if ( transition ) {
// Prevent transitioning after transition is complete
/**
* Generic selection of options.
*
- * Items can contain any rendering, and are uniquely identified by a has of thier data. Any widget
+ * Items can contain any rendering, and are uniquely identified by a hash of their data. Any widget
* that provides options, from which the user must choose one, should be built on this class.
*
* Use together with OO.ui.OptionWidget.
* Menu for a text input widget.
*
* This menu is specially designed to be positioned beneath the text input widget. Even if the input
- * is in a different frame, the menu's position is automatically calulated and maintained when the
+ * is in a different frame, the menu's position is automatically calculated and maintained when the
* menu is toggled or the window is resized.
*
* @class
// Position under input
dimensions.top += $container.height();
- // Compensate for frame position if in a differnt frame
+ // Compensate for frame position if in a different frame
if ( this.input.$.$iframe && this.input.$.context !== this.$element[0].ownerDocument ) {
frameOffset = OO.ui.Element.getRelativePosition(
this.input.$.$iframe, this.$element.offsetParent()
if ( this.$element.css( 'direction' ) === 'rtl' ) {
dimensions.right = this.$element.parent().position().left -
$container.width() - dimensions.left;
- // Erase the value for 'left':
+ // Erase the value for 'left'
delete dimensions.left;
}
}