/*!
- * OOjs UI v0.18.3
+ * OOjs UI v0.19.1
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-01-04T00:22:40Z
+ * Date: 2017-02-08T00:38:31Z
*/
( function ( OO ) {
/**
* @property {number}
+ * @private
*/
OO.ui.elementId = 0;
/**
* Generate a unique ID for element
*
- * @return {string} [id]
+ * @return {string} ID
*/
OO.ui.generateElementId = function () {
- OO.ui.elementId += 1;
+ OO.ui.elementId++;
return 'oojsui-' + OO.ui.elementId;
};
/**
* Get a localized message.
*
- * In environments that provide a localization system, this function should be overridden to
- * return the message translated in the user's language. The default implementation always returns
- * English messages.
- *
* After the message key, message parameters may optionally be passed. In the default implementation,
* any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
* Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
* they support unnamed, ordered message parameters.
*
+ * In environments that provide a localization system, this function should be overridden to
+ * return the message translated in the user's language. The default implementation always returns
+ * English messages. An example of doing this with [jQuery.i18n](https://github.com/wikimedia/jquery.i18n)
+ * follows.
+ *
+ * @example
+ * var i, iLen, button,
+ * messagePath = 'oojs-ui/dist/i18n/',
+ * languages = [ $.i18n().locale, 'ur', 'en' ],
+ * languageMap = {};
+ *
+ * for ( i = 0, iLen = languages.length; i < iLen; i++ ) {
+ * languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
+ * }
+ *
+ * $.i18n().load( languageMap ).done( function() {
+ * // Replace the built-in `msg` only once we've loaded the internationalization.
+ * // OOjs UI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
+ * // you put off creating any widgets until this promise is complete, no English
+ * // will be displayed.
+ * OO.ui.msg = $.i18n;
+ *
+ * // A button displaying "OK" in the default locale
+ * button = new OO.ui.ButtonWidget( {
+ * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
+ * icon: 'check'
+ * } );
+ * $( 'body' ).append( button.$element );
+ *
+ * // A button displaying "OK" in Urdu
+ * $.i18n().locale = 'ur';
+ * button = new OO.ui.ButtonWidget( {
+ * label: OO.ui.msg( 'ooui-dialog-message-accept' ),
+ * icon: 'check'
+ * } );
+ * $( 'body' ).append( button.$element );
+ * } );
+ *
* @param {string} key Message key
* @param {...Mixed} [params] Message parameters
* @return {string} Translated message with parameters substituted
this.$element = config.$element ||
$( document.createElement( this.getTagName() ) );
this.elementGroup = null;
- this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses );
// Initialization
if ( Array.isArray( config.classes ) ) {
};
/**
- * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
+ * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of an HTML DOM node
* (and its children) that represent an Element of the same class and the given configuration,
* generated by the PHP implementation.
*
animations = {};
callback = typeof config.complete === 'function' && config.complete;
+ if ( callback ) {
+ OO.ui.warnDeprecation( 'Element#scrollIntoView: The `complete` callback config option is deprecated. Use the return promise instead.' );
+ }
container = this.getClosestScrollableContainer( el, config.direction );
$container = $( container );
elementDimensions = this.getDimensions( el );
* guaranteeing that theme updates do not occur within an element's constructor
*/
OO.ui.Element.prototype.updateThemeClasses = function () {
- this.debouncedUpdateThemeClassesHandler();
-};
-
-/**
- * @private
- * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
- * make them synchronous.
- */
-OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
- OO.ui.theme.updateElementClasses( this );
+ OO.ui.theme.queueUpdateElementClasses( this );
};
/**
/* Static Properties */
/**
- * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
+ * Whether this widget will behave reasonably when wrapped in an HTML `<label>`. If this is true,
* wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
* handling.
*
*
* @constructor
*/
-OO.ui.Theme = function OoUiTheme() {};
+OO.ui.Theme = function OoUiTheme() {
+ this.elementClassesQueue = [];
+ this.debouncedUpdateQueuedElementClasses = OO.ui.debounce( this.updateQueuedElementClasses );
+};
/* Setup */
.addClass( classes.on.join( ' ' ) );
};
+/**
+ * @private
+ */
+OO.ui.Theme.prototype.updateQueuedElementClasses = function () {
+ var i;
+ for ( i = 0; i < this.elementClassesQueue.length; i++ ) {
+ this.updateElementClasses( this.elementClassesQueue[ i ] );
+ }
+ // Clear the queue
+ this.elementClassesQueue = [];
+};
+
+/**
+ * Queue #updateElementClasses to be called for this element.
+ *
+ * @localdoc QUnit tests override this method to directly call #queueUpdateElementClasses,
+ * to make them synchronous.
+ *
+ * @param {OO.ui.Element} element Element for which to update classes
+ */
+OO.ui.Theme.prototype.queueUpdateElementClasses = function ( element ) {
+ // Keep items in the queue unique. Use lastIndexOf to start checking from the end because that's
+ // the most common case (this method is often called repeatedly for the same element).
+ if ( this.elementClassesQueue.lastIndexOf( element ) !== -1 ) {
+ return;
+ }
+ this.elementClassesQueue.push( element );
+ this.debouncedUpdateQueuedElementClasses();
+};
+
/**
* Get the transition duration in milliseconds for dialogs opening/closing
*
return this.label;
};
-/**
- * Fit the label.
- *
- * @chainable
- * @deprecated since 0.16.0
- */
-OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
- return this;
-};
-
/**
* Set the content of the label.
*
/* Static Properties */
/**
+ * @static
* @inheritdoc
*/
OO.ui.ButtonWidget.static.cancelButtonMouseDownEvents = false;
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.IconWidget.static.tagName = 'span';
/**
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.IndicatorWidget.static.tagName = 'span';
/**
// Properties
this.input = config.input;
- // Events
+ // Initialization
if ( this.input instanceof OO.ui.InputWidget ) {
- this.$element.on( 'click', this.onClick.bind( this ) );
+ this.$element.attr( 'for', this.input.getInputId() );
}
-
- // Initialization
this.$element.addClass( 'oo-ui-labelWidget' );
};
/* Static Properties */
-OO.ui.LabelWidget.static.tagName = 'span';
-
-/* Methods */
-
/**
- * Handles label mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
+ * @static
+ * @inheritdoc
*/
-OO.ui.LabelWidget.prototype.onClick = function () {
- this.input.simulateLabelClick();
- return false;
-};
+OO.ui.LabelWidget.static.tagName = 'label';
/**
* PendingElement is a mixin that is used to create elements that notify users that something is happening
* }
* OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
*
+ * MessageDialog.static.name = 'myMessageDialog';
* MessageDialog.static.actions = [
* { action: 'save', label: 'Done', flags: 'primary' },
* { label: 'Cancel', flags: 'safe' }
};
/**
- * Element that can be automatically clipped to visible boundaries.
+ * Element that will stick under a specified container, even when it is inserted elsewhere in the
+ * document (for example, in a OO.ui.Window's $overlay).
*
- * Whenever the element's natural height changes, you have to call
- * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
- * clipping correctly.
+ * The elements's position is automatically calculated and maintained when window is resized or the
+ * page is scrolled. If you reposition the container manually, you have to call #position to make
+ * sure the element is still placed correctly.
*
- * The dimensions of #$clippableContainer will be compared to the boundaries of the
- * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
- * then #$clippable will be given a fixed reduced height and/or width and will be made
- * scrollable. By default, #$clippable and #$clippableContainer are the same element,
- * but you can build a static footer by setting #$clippableContainer to an element that contains
- * #$clippable and the footer.
+ * As positioning is only possible when both the element and the container are attached to the DOM
+ * and visible, it's only done after you call #togglePositioning. You might want to do this inside
+ * the #toggle method to display a floating popup, for example.
*
* @abstract
* @class
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
- * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
- * omit to use #$clippable
+ * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
+ * @cfg {jQuery} [$floatableContainer] Node to position below
*/
-OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
+OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
// Configuration initialization
config = config || {};
// Properties
- this.$clippable = null;
- this.$clippableContainer = null;
- this.clipping = false;
- this.clippedHorizontally = false;
- this.clippedVertically = false;
- this.$clippableScrollableContainer = null;
- this.$clippableScroller = null;
- this.$clippableWindow = null;
- this.idealWidth = null;
- this.idealHeight = null;
- this.onClippableScrollHandler = this.clip.bind( this );
- this.onClippableWindowResizeHandler = this.clip.bind( this );
+ this.$floatable = null;
+ this.$floatableContainer = null;
+ this.$floatableWindow = null;
+ this.$floatableClosestScrollable = null;
+ this.onFloatableScrollHandler = this.position.bind( this );
+ this.onFloatableWindowResizeHandler = this.position.bind( this );
// Initialization
- if ( config.$clippableContainer ) {
- this.setClippableContainer( config.$clippableContainer );
- }
- this.setClippableElement( config.$clippable || this.$element );
+ this.setFloatableContainer( config.$floatableContainer );
+ this.setFloatableElement( config.$floatable || this.$element );
};
/* Methods */
/**
- * Set clippable element.
+ * Set floatable element.
*
* If an element is already set, it will be cleaned up before setting up the new element.
*
- * @param {jQuery} $clippable Element to make clippable
+ * @param {jQuery} $floatable Element to make floatable
*/
-OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
- if ( this.$clippable ) {
- this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
- this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
- OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
+OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
+ if ( this.$floatable ) {
+ this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
+ this.$floatable.css( { left: '', top: '' } );
}
- this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
- this.clip();
+ this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
+ this.position();
};
/**
- * Set clippable container.
- *
- * This is the container that will be measured when deciding whether to clip. When clipping,
- * #$clippable will be resized in order to keep the clippable container fully visible.
+ * Set floatable container.
*
- * If the clippable container is unset, #$clippable will be used.
+ * The element will be always positioned under the specified container.
*
- * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
+ * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
*/
-OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
- this.$clippableContainer = $clippableContainer;
- if ( this.$clippable ) {
- this.clip();
+OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
+ this.$floatableContainer = $floatableContainer;
+ if ( this.$floatable ) {
+ this.position();
}
};
/**
- * Toggle clipping.
+ * Toggle positioning.
*
- * Do not turn clipping on until after the element is attached to the DOM and visible.
+ * Do not turn positioning on until after the element is attached to the DOM and visible.
*
- * @param {boolean} [clipping] Enable clipping, omit to toggle
+ * @param {boolean} [positioning] Enable positioning, omit to toggle
* @chainable
*/
-OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
- clipping = clipping === undefined ? !this.clipping : !!clipping;
+OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
+ var closestScrollableOfContainer;
- if ( this.clipping !== clipping ) {
- this.clipping = clipping;
- if ( clipping ) {
- this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
- // If the clippable container is the root, we have to listen to scroll events and check
- // jQuery.scrollTop on the window because of browser inconsistencies
- this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
- $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
- this.$clippableScrollableContainer;
- this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
- this.$clippableWindow = $( this.getElementWindow() )
- .on( 'resize', this.onClippableWindowResizeHandler );
- // Initial clip after visible
- this.clip();
+ if ( !this.$floatable || !this.$floatableContainer ) {
+ return this;
+ }
+
+ positioning = positioning === undefined ? !this.positioning : !!positioning;
+
+ if ( this.positioning !== positioning ) {
+ this.positioning = positioning;
+
+ closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
+ this.needsCustomPosition = !OO.ui.contains( this.$floatableContainer[ 0 ], this.$floatable[ 0 ] );
+ // If the scrollable is the root, we have to listen to scroll events
+ // on the window because of browser inconsistencies.
+ if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
+ closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
+ }
+
+ if ( positioning ) {
+ this.$floatableWindow = $( this.getElementWindow() );
+ this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
+
+ this.$floatableClosestScrollable = $( closestScrollableOfContainer );
+ this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
+
+ // Initial position after visible
+ this.position();
} else {
- this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
- OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
+ if ( this.$floatableWindow ) {
+ this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
+ this.$floatableWindow = null;
+ }
- this.$clippableScrollableContainer = null;
- this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
- this.$clippableScroller = null;
- this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
- this.$clippableWindow = null;
+ if ( this.$floatableClosestScrollable ) {
+ this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
+ this.$floatableClosestScrollable = null;
+ }
+
+ this.$floatable.css( { left: '', top: '' } );
}
}
};
/**
- * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
+ * Check whether the bottom edge of the given element is within the viewport of the given container.
*
- * @return {boolean} Element will be clipped to the visible area
+ * @private
+ * @param {jQuery} $element
+ * @param {jQuery} $container
+ * @return {boolean}
*/
-OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
- return this.clipping;
+OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
+ var elemRect, contRect,
+ leftEdgeInBounds = false,
+ bottomEdgeInBounds = false,
+ rightEdgeInBounds = false;
+
+ elemRect = $element[ 0 ].getBoundingClientRect();
+ if ( $container[ 0 ] === window ) {
+ contRect = {
+ top: 0,
+ left: 0,
+ right: document.documentElement.clientWidth,
+ bottom: document.documentElement.clientHeight
+ };
+ } else {
+ contRect = $container[ 0 ].getBoundingClientRect();
+ }
+
+ // For completeness, if we still cared about topEdgeInBounds, that'd be:
+ // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
+ if ( elemRect.left >= contRect.left && elemRect.left <= contRect.right ) {
+ leftEdgeInBounds = true;
+ }
+ if ( elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom ) {
+ bottomEdgeInBounds = true;
+ }
+ if ( elemRect.right >= contRect.left && elemRect.right <= contRect.right ) {
+ rightEdgeInBounds = true;
+ }
+
+ // We only care that any part of the bottom edge is visible
+ return bottomEdgeInBounds && ( leftEdgeInBounds || rightEdgeInBounds );
};
/**
- * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
+ * Position the floatable below its container.
*
- * @return {boolean} Part of the element is being clipped
- */
+ * This should only be done when both of them are attached to the DOM and visible.
+ *
+ * @chainable
+ */
+OO.ui.mixin.FloatableElement.prototype.position = function () {
+ var pos;
+
+ if ( !this.positioning ) {
+ return this;
+ }
+
+ if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
+ this.$floatable.addClass( 'oo-ui-element-hidden' );
+ return;
+ } else {
+ this.$floatable.removeClass( 'oo-ui-element-hidden' );
+ }
+
+ if ( !this.needsCustomPosition ) {
+ return;
+ }
+
+ pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
+
+ // Position under container
+ pos.top += this.$floatableContainer.height();
+ this.$floatable.css( pos );
+
+ // We updated the position, so re-evaluate the clipping state.
+ // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
+ // will not notice the need to update itself.)
+ // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
+ // it not listen to the right events in the right places?
+ if ( this.clip ) {
+ this.clip();
+ }
+
+ return this;
+};
+
+/**
+ * Element that can be automatically clipped to visible boundaries.
+ *
+ * Whenever the element's natural height changes, you have to call
+ * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
+ * clipping correctly.
+ *
+ * The dimensions of #$clippableContainer will be compared to the boundaries of the
+ * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
+ * then #$clippable will be given a fixed reduced height and/or width and will be made
+ * scrollable. By default, #$clippable and #$clippableContainer are the same element,
+ * but you can build a static footer by setting #$clippableContainer to an element that contains
+ * #$clippable and the footer.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
+ * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
+ * omit to use #$clippable
+ */
+OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
+ // Configuration initialization
+ config = config || {};
+
+ // Properties
+ this.$clippable = null;
+ this.$clippableContainer = null;
+ this.clipping = false;
+ this.clippedHorizontally = false;
+ this.clippedVertically = false;
+ this.$clippableScrollableContainer = null;
+ this.$clippableScroller = null;
+ this.$clippableWindow = null;
+ this.idealWidth = null;
+ this.idealHeight = null;
+ this.onClippableScrollHandler = this.clip.bind( this );
+ this.onClippableWindowResizeHandler = this.clip.bind( this );
+
+ // Initialization
+ if ( config.$clippableContainer ) {
+ this.setClippableContainer( config.$clippableContainer );
+ }
+ this.setClippableElement( config.$clippable || this.$element );
+};
+
+/* Methods */
+
+/**
+ * Set clippable element.
+ *
+ * If an element is already set, it will be cleaned up before setting up the new element.
+ *
+ * @param {jQuery} $clippable Element to make clippable
+ */
+OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
+ if ( this.$clippable ) {
+ this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
+ this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
+ }
+
+ this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
+ this.clip();
+};
+
+/**
+ * Set clippable container.
+ *
+ * This is the container that will be measured when deciding whether to clip. When clipping,
+ * #$clippable will be resized in order to keep the clippable container fully visible.
+ *
+ * If the clippable container is unset, #$clippable will be used.
+ *
+ * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
+ */
+OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
+ this.$clippableContainer = $clippableContainer;
+ if ( this.$clippable ) {
+ this.clip();
+ }
+};
+
+/**
+ * Toggle clipping.
+ *
+ * Do not turn clipping on until after the element is attached to the DOM and visible.
+ *
+ * @param {boolean} [clipping] Enable clipping, omit to toggle
+ * @chainable
+ */
+OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
+ clipping = clipping === undefined ? !this.clipping : !!clipping;
+
+ if ( this.clipping !== clipping ) {
+ this.clipping = clipping;
+ if ( clipping ) {
+ this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
+ // If the clippable container is the root, we have to listen to scroll events and check
+ // jQuery.scrollTop on the window because of browser inconsistencies
+ this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
+ $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
+ this.$clippableScrollableContainer;
+ this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
+ this.$clippableWindow = $( this.getElementWindow() )
+ .on( 'resize', this.onClippableWindowResizeHandler );
+ // Initial clip after visible
+ this.clip();
+ } else {
+ this.$clippable.css( {
+ width: '',
+ height: '',
+ maxWidth: '',
+ maxHeight: '',
+ overflowX: '',
+ overflowY: ''
+ } );
+ OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
+
+ this.$clippableScrollableContainer = null;
+ this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
+ this.$clippableScroller = null;
+ this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
+ this.$clippableWindow = null;
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
+ *
+ * @return {boolean} Element will be clipped to the visible area
+ */
+OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
+ return this.clipping;
+};
+
+/**
+ * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
+ *
+ * @return {boolean} Part of the element is being clipped
+ */
OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
return this.clippedHorizontally || this.clippedVertically;
};
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.LabelElement
* @mixins OO.ui.mixin.ClippableElement
+ * @mixins OO.ui.mixin.FloatableElement
*
* @constructor
* @param {Object} [config] Configuration options
$clippable: this.$body,
$clippableContainer: this.$popup
} ) );
+ OO.ui.mixin.FloatableElement.call( this, config );
// Properties
this.$anchor = $( '<div>' );
OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
+OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.FloatableElement );
/* Methods */
OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
if ( change ) {
+ this.togglePositioning( show && !!this.$floatableContainer );
+
if ( show ) {
if ( this.autoClose ) {
this.bindMouseDownListener();
/**
* Set popup alignment
*
- * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
+ * @param {string} [align=center] Alignment of the popup, `center`, `force-left`, `force-right`,
* `backwards` or `forwards`.
*/
OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
- // Validate alignment and transform deprecated values
- if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
- this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
+ // Transform values deprecated since v0.11.0
+ if ( align === 'left' || align === 'right' ) {
+ OO.ui.warnDeprecation( 'PopupWidget#setAlignment parameter value `' + align + '` is deprecated. Use `force-right` or `force-left` instead.' );
+ align = { left: 'force-right', right: 'force-left' }[ align ];
+ }
+
+ // Validate alignment
+ if ( [ 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
+ this.align = align;
} else {
this.align = 'center';
}
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {jQuery} [$overlay] Render the popup into a separate layer. This configuration is useful in cases where
+ * the expanded popup 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 popup uses relative positioning.
*/
OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
// Parent constructor
OO.ui.PopupButtonWidget.parent.call( this, config );
// Mixin constructors
- OO.ui.mixin.PopupElement.call( this, config );
+ OO.ui.mixin.PopupElement.call( this, $.extend( true, {}, config, {
+ popup: {
+ $floatableContainer: this.$element
+ }
+ } ) );
+
+ // Properties
+ this.$overlay = config.$overlay || this.$element;
// Events
this.connect( this, { click: 'onAction' } );
// Initialization
this.$element
.addClass( 'oo-ui-popupButtonWidget' )
- .attr( 'aria-haspopup', 'true' )
- .append( this.popup.$element );
+ .attr( 'aria-haspopup', 'true' );
+ this.popup.$element
+ .addClass( 'oo-ui-popupButtonWidget-popup' )
+ .toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
+ .toggleClass( 'oo-ui-popupButtonWidget-frameless-popup', !this.isFramed() );
+ this.$overlay.append( this.popup.$element );
};
/* Setup */
/* Static Properties */
+/**
+ * Whether this option can be selected. See #setSelected.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
OO.ui.OptionWidget.static.selectable = true;
+/**
+ * Whether this option can be highlighted. See #setHighlighted.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
OO.ui.OptionWidget.static.highlightable = true;
+/**
+ * Whether this option can be pressed. See #setPressed.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
OO.ui.OptionWidget.static.pressable = true;
+/**
+ * Whether this option will be scrolled into view when it is selected.
+ *
+ * @static
+ * @inheritable
+ * @property {boolean}
+ */
OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
/* Methods */
return this;
};
+/**
+ * Get text to match search strings against.
+ *
+ * The default implementation returns the label text, but subclasses
+ * can override this to provide more complex behavior.
+ *
+ * @return {string|boolean} String to match search string against
+ */
+OO.ui.OptionWidget.prototype.getMatchText = function () {
+ var label = this.getLabel();
+ return typeof label === 'string' ? label : this.$label.text();
+};
+
/**
* A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
* select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
* @protected
* @param {string} s String to match against items
* @param {boolean} [exact=false] Only accept exact matches
- * @return {Function} function ( OO.ui.OptionItem ) => boolean
+ * @return {Function} function ( OO.ui.OptionWidget ) => boolean
*/
OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
var re;
}
re = new RegExp( re, 'i' );
return function ( item ) {
- var l = item.getLabel();
- if ( typeof l !== 'string' ) {
- l = item.$label.text();
- }
- if ( l.normalize ) {
- l = l.normalize();
+ var matchText = item.getMatchText();
+ if ( matchText.normalize ) {
+ matchText = matchText.normalize();
}
- return re.test( l );
+ return re.test( matchText );
};
};
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
/**
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.MenuSectionOptionWidget.static.selectable = false;
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.MenuSectionOptionWidget.static.highlightable = false;
/**
* @protected
*/
OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
- var i, item,
+ var i, item, visible,
+ anyVisible = false,
len = this.items.length,
showAll = !this.isVisible(),
filter = showAll ? null : this.getItemMatcher( this.$input.val() );
for ( i = 0; i < len; i++ ) {
item = this.items[ i ];
if ( item instanceof OO.ui.OptionWidget ) {
- item.toggle( showAll || filter( item ) );
+ visible = showAll || filter( item );
+ anyVisible = anyVisible || visible;
+ item.toggle( visible );
}
}
+ this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
+
// Reevaluate clipping
this.clip();
};
* OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
* users can interact with it.
*
- * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
+ * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
* OO.ui.DropdownInputWidget instead.
*
* @example
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioOptionWidget.static.highlightable = false;
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioOptionWidget.static.pressable = false;
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioOptionWidget.static.tagName = 'label';
/* Methods */
* an interface for adding, removing and selecting options.
* Please see the [OOjs UI documentation on MediaWiki][1] for more information.
*
- * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
+ * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
* OO.ui.RadioSelectInputWidget instead.
*
* @example
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.CheckboxMultioptionWidget.static.tagName = 'label';
/* Methods */
* CheckboxMultiselectWidget provides an interface for adding, removing and selecting options.
* Please see the [OOjs UI documentation on MediaWiki][1] for more information.
*
- * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
+ * If you want to use this within an HTML form, such as a OO.ui.FormLayout, use
* OO.ui.CheckboxMultiselectInputWidget instead.
*
* @example
* items: [ option1, option2 ]
* } );
*
- * $( 'body' ).append( multiselect.$element );
- *
- * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
- *
- * @class
- * @extends OO.ui.MultiselectWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-OO.ui.CheckboxMultiselectWidget = function OoUiCheckboxMultiselectWidget( config ) {
- // Parent constructor
- OO.ui.CheckboxMultiselectWidget.parent.call( this, config );
-
- // Properties
- this.$lastClicked = null;
-
- // Events
- this.$group.on( 'click', this.onClick.bind( this ) );
-
- // Initialization
- this.$element
- .addClass( 'oo-ui-checkboxMultiselectWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( OO.ui.CheckboxMultiselectWidget, OO.ui.MultiselectWidget );
-
-/* Methods */
-
-/**
- * Get an option by its position relative to the specified item (or to the start of the option array,
- * if item is `null`). The direction in which to search through the option array is specified with a
- * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
- * `null` if there are no options in the array.
- *
- * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
- * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
- * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
- */
-OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function ( item, direction ) {
- var currentIndex, nextIndex, i,
- increase = direction > 0 ? 1 : -1,
- len = this.items.length;
-
- if ( item ) {
- currentIndex = this.items.indexOf( item );
- nextIndex = ( currentIndex + increase + len ) % len;
- } else {
- // If no item is selected and moving forward, start at the beginning.
- // If moving backward, start at the end.
- nextIndex = direction > 0 ? 0 : len - 1;
- }
-
- for ( i = 0; i < len; i++ ) {
- item = this.items[ nextIndex ];
- if ( item && !item.isDisabled() ) {
- return item;
- }
- nextIndex = ( nextIndex + increase + len ) % len;
- }
- return null;
-};
-
-/**
- * Handle click events on checkboxes.
- *
- * @param {jQuery.Event} e
- */
-OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
- var $options, lastClickedIndex, nowClickedIndex, i, direction, wasSelected, items,
- $lastClicked = this.$lastClicked,
- $nowClicked = $( e.target ).closest( '.oo-ui-checkboxMultioptionWidget' )
- .not( '.oo-ui-widget-disabled' );
-
- // Allow selecting multiple options at once by Shift-clicking them
- if ( $lastClicked && $nowClicked.length && e.shiftKey ) {
- $options = this.$group.find( '.oo-ui-checkboxMultioptionWidget' );
- lastClickedIndex = $options.index( $lastClicked );
- nowClickedIndex = $options.index( $nowClicked );
- // If it's the same item, either the user is being silly, or it's a fake event generated by the
- // browser. In either case we don't need custom handling.
- if ( nowClickedIndex !== lastClickedIndex ) {
- items = this.items;
- wasSelected = items[ nowClickedIndex ].isSelected();
- direction = nowClickedIndex > lastClickedIndex ? 1 : -1;
-
- // This depends on the DOM order of the items and the order of the .items array being the same.
- for ( i = lastClickedIndex; i !== nowClickedIndex; i += direction ) {
- if ( !items[ i ].isDisabled() ) {
- items[ i ].setSelected( !wasSelected );
- }
- }
- // For the now-clicked element, use immediate timeout to allow the browser to do its own
- // handling first, then set our value. The order in which events happen is different for
- // clicks on the <input> and on the <label> and there are additional fake clicks fired for
- // non-click actions that change the checkboxes.
- e.preventDefault();
- setTimeout( function () {
- if ( !items[ nowClickedIndex ].isDisabled() ) {
- items[ nowClickedIndex ].setSelected( !wasSelected );
- }
- } );
- }
- }
-
- if ( $nowClicked.length ) {
- this.$lastClicked = $nowClicked;
- }
-};
-
-/**
- * Element that will stick under a specified container, even when it is inserted elsewhere in the
- * document (for example, in a OO.ui.Window's $overlay).
- *
- * The elements's position is automatically calculated and maintained when window is resized or the
- * page is scrolled. If you reposition the container manually, you have to call #position to make
- * sure the element is still placed correctly.
- *
- * As positioning is only possible when both the element and the container are attached to the DOM
- * and visible, it's only done after you call #togglePositioning. You might want to do this inside
- * the #toggle method to display a floating popup, for example.
- *
- * @abstract
- * @class
- *
- * @constructor
- * @param {Object} [config] Configuration options
- * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
- * @cfg {jQuery} [$floatableContainer] Node to position below
- */
-OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
- // Configuration initialization
- config = config || {};
-
- // Properties
- this.$floatable = null;
- this.$floatableContainer = null;
- this.$floatableWindow = null;
- this.$floatableClosestScrollable = null;
- this.onFloatableScrollHandler = this.position.bind( this );
- this.onFloatableWindowResizeHandler = this.position.bind( this );
-
- // Initialization
- this.setFloatableContainer( config.$floatableContainer );
- this.setFloatableElement( config.$floatable || this.$element );
-};
-
-/* Methods */
-
-/**
- * Set floatable element.
- *
- * If an element is already set, it will be cleaned up before setting up the new element.
- *
- * @param {jQuery} $floatable Element to make floatable
- */
-OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
- if ( this.$floatable ) {
- this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
- this.$floatable.css( { left: '', top: '' } );
- }
-
- this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
- this.position();
-};
-
-/**
- * Set floatable container.
- *
- * The element will be always positioned under the specified container.
- *
- * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
- */
-OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
- this.$floatableContainer = $floatableContainer;
- if ( this.$floatable ) {
- this.position();
- }
-};
-
-/**
- * Toggle positioning.
- *
- * Do not turn positioning on until after the element is attached to the DOM and visible.
+ * $( 'body' ).append( multiselect.$element );
*
- * @param {boolean} [positioning] Enable positioning, omit to toggle
- * @chainable
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
+ *
+ * @class
+ * @extends OO.ui.MultiselectWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
*/
-OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
- var closestScrollableOfContainer, closestScrollableOfFloatable;
-
- positioning = positioning === undefined ? !this.positioning : !!positioning;
-
- if ( this.positioning !== positioning ) {
- this.positioning = positioning;
-
- closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
- closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
- this.needsCustomPosition = closestScrollableOfContainer !== closestScrollableOfFloatable;
- // If the scrollable is the root, we have to listen to scroll events
- // on the window because of browser inconsistencies.
- if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
- closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
- }
+OO.ui.CheckboxMultiselectWidget = function OoUiCheckboxMultiselectWidget( config ) {
+ // Parent constructor
+ OO.ui.CheckboxMultiselectWidget.parent.call( this, config );
- if ( positioning ) {
- this.$floatableWindow = $( this.getElementWindow() );
- this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
+ // Properties
+ this.$lastClicked = null;
- this.$floatableClosestScrollable = $( closestScrollableOfContainer );
- this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
+ // Events
+ this.$group.on( 'click', this.onClick.bind( this ) );
- // Initial position after visible
- this.position();
- } else {
- if ( this.$floatableWindow ) {
- this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
- this.$floatableWindow = null;
- }
+ // Initialization
+ this.$element
+ .addClass( 'oo-ui-checkboxMultiselectWidget' );
+};
- if ( this.$floatableClosestScrollable ) {
- this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
- this.$floatableClosestScrollable = null;
- }
+/* Setup */
- this.$floatable.css( { left: '', top: '' } );
- }
- }
+OO.inheritClass( OO.ui.CheckboxMultiselectWidget, OO.ui.MultiselectWidget );
- return this;
-};
+/* Methods */
/**
- * Check whether the bottom edge of the given element is within the viewport of the given container.
+ * Get an option by its position relative to the specified item (or to the start of the option array,
+ * if item is `null`). The direction in which to search through the option array is specified with a
+ * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
+ * `null` if there are no options in the array.
*
- * @private
- * @param {jQuery} $element
- * @param {jQuery} $container
- * @return {boolean}
+ * @param {OO.ui.CheckboxMultioptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
+ * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
+ * @return {OO.ui.CheckboxMultioptionWidget|null} Item at position, `null` if there are no items in the select
*/
-OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
- var elemRect, contRect,
- leftEdgeInBounds = false,
- bottomEdgeInBounds = false,
- rightEdgeInBounds = false;
+OO.ui.CheckboxMultiselectWidget.prototype.getRelativeFocusableItem = function ( item, direction ) {
+ var currentIndex, nextIndex, i,
+ increase = direction > 0 ? 1 : -1,
+ len = this.items.length;
- elemRect = $element[ 0 ].getBoundingClientRect();
- if ( $container[ 0 ] === window ) {
- contRect = {
- top: 0,
- left: 0,
- right: document.documentElement.clientWidth,
- bottom: document.documentElement.clientHeight
- };
+ if ( item ) {
+ currentIndex = this.items.indexOf( item );
+ nextIndex = ( currentIndex + increase + len ) % len;
} else {
- contRect = $container[ 0 ].getBoundingClientRect();
+ // If no item is selected and moving forward, start at the beginning.
+ // If moving backward, start at the end.
+ nextIndex = direction > 0 ? 0 : len - 1;
}
- // For completeness, if we still cared about topEdgeInBounds, that'd be:
- // elemRect.top >= contRect.top && elemRect.top <= contRect.bottom
- if ( elemRect.left >= contRect.left && elemRect.left <= contRect.right ) {
- leftEdgeInBounds = true;
- }
- if ( elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom ) {
- bottomEdgeInBounds = true;
- }
- if ( elemRect.right >= contRect.left && elemRect.right <= contRect.right ) {
- rightEdgeInBounds = true;
+ for ( i = 0; i < len; i++ ) {
+ item = this.items[ nextIndex ];
+ if ( item && !item.isDisabled() ) {
+ return item;
+ }
+ nextIndex = ( nextIndex + increase + len ) % len;
}
-
- // We only care that any part of the bottom edge is visible
- return bottomEdgeInBounds && ( leftEdgeInBounds || rightEdgeInBounds );
+ return null;
};
/**
- * Position the floatable below its container.
- *
- * This should only be done when both of them are attached to the DOM and visible.
+ * Handle click events on checkboxes.
*
- * @chainable
+ * @param {jQuery.Event} e
*/
-OO.ui.mixin.FloatableElement.prototype.position = function () {
- var pos;
-
- if ( !this.positioning ) {
- return this;
- }
+OO.ui.CheckboxMultiselectWidget.prototype.onClick = function ( e ) {
+ var $options, lastClickedIndex, nowClickedIndex, i, direction, wasSelected, items,
+ $lastClicked = this.$lastClicked,
+ $nowClicked = $( e.target ).closest( '.oo-ui-checkboxMultioptionWidget' )
+ .not( '.oo-ui-widget-disabled' );
- if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
- this.$floatable.addClass( 'oo-ui-element-hidden' );
- return;
- } else {
- this.$floatable.removeClass( 'oo-ui-element-hidden' );
- }
+ // Allow selecting multiple options at once by Shift-clicking them
+ if ( $lastClicked && $nowClicked.length && e.shiftKey ) {
+ $options = this.$group.find( '.oo-ui-checkboxMultioptionWidget' );
+ lastClickedIndex = $options.index( $lastClicked );
+ nowClickedIndex = $options.index( $nowClicked );
+ // If it's the same item, either the user is being silly, or it's a fake event generated by the
+ // browser. In either case we don't need custom handling.
+ if ( nowClickedIndex !== lastClickedIndex ) {
+ items = this.items;
+ wasSelected = items[ nowClickedIndex ].isSelected();
+ direction = nowClickedIndex > lastClickedIndex ? 1 : -1;
- if ( !this.needsCustomPosition ) {
- return;
+ // This depends on the DOM order of the items and the order of the .items array being the same.
+ for ( i = lastClickedIndex; i !== nowClickedIndex; i += direction ) {
+ if ( !items[ i ].isDisabled() ) {
+ items[ i ].setSelected( !wasSelected );
+ }
+ }
+ // For the now-clicked element, use immediate timeout to allow the browser to do its own
+ // handling first, then set our value. The order in which events happen is different for
+ // clicks on the <input> and on the <label> and there are additional fake clicks fired for
+ // non-click actions that change the checkboxes.
+ e.preventDefault();
+ setTimeout( function () {
+ if ( !items[ nowClickedIndex ].isDisabled() ) {
+ items[ nowClickedIndex ].setSelected( !wasSelected );
+ }
+ } );
+ }
}
- pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
-
- // Position under container
- pos.top += this.$floatableContainer.height();
- this.$floatable.css( pos );
-
- // We updated the position, so re-evaluate the clipping state.
- // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
- // will not notice the need to update itself.)
- // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
- // it not listen to the right events in the right places?
- if ( this.clip ) {
- this.clip();
+ if ( $nowClicked.length ) {
+ this.$lastClicked = $nowClicked;
}
-
- return this;
};
/**
OO.inheritClass( OO.ui.FloatingMenuSelectWidget, OO.ui.MenuSelectWidget );
OO.mixinClass( OO.ui.FloatingMenuSelectWidget, OO.ui.mixin.FloatableElement );
-// For backwards compatibility
-OO.ui.TextInputMenuSelectWidget = OO.ui.FloatingMenuSelectWidget;
-
/* Methods */
/**
return this;
};
+/*
+ * The old name for the FloatingMenuSelectWidget widget, provided for backwards-compatibility.
+ *
+ * @class
+ * @extends OO.ui.FloatingMenuSelectWidget
+ *
+ * @constructor
+ * @deprecated since v0.12.5.
+ */
+OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget() {
+ OO.ui.warnDeprecation( 'TextInputMenuSelectWidget is deprecated. Use the FloatingMenuSelectWidget instead.' );
+ // Parent constructor
+ OO.ui.TextInputMenuSelectWidget.parent.apply( this, arguments );
+};
+
+OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.FloatingMenuSelectWidget );
+
/**
* Progress bars visually display the status of an operation, such as a download,
* and can be either determinate or indeterminate:
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.ProgressBarWidget.static.tagName = 'div';
/* Methods */
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.InputWidget.static.supportsSimpleLabel = true;
/* Static Methods */
return $( '<input>' );
};
+/**
+ * Get input element's ID.
+ *
+ * If the element already has an ID then that is returned, otherwise unique ID is
+ * generated, set on the element, and returned.
+ *
+ * @return {string} The ID of the element
+ */
+OO.ui.InputWidget.prototype.getInputId = function () {
+ var id = this.$input.attr( 'id' );
+
+ if ( id === undefined ) {
+ id = OO.ui.generateElementId();
+ this.$input.attr( 'id', id );
+ }
+
+ return id;
+};
+
/**
* Handle potentially value-changing events.
*
* called directly.
*/
OO.ui.InputWidget.prototype.simulateLabelClick = function () {
+ OO.ui.warnDeprecation( 'InputWidget: simulateLabelClick() is deprecated.' );
if ( !this.isDisabled() ) {
if ( this.$input.is( ':checkbox, :radio' ) ) {
this.$input.click();
/**
* 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.
+ *
+ * @static
+ * @inheritdoc
*/
OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
* in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
* alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
*
- * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
+ * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
*
* @example
* // An example of selected, unselected, and disabled checkbox inputs
/**
* DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
- * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
+ * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
* of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
* more information about input widgets.
*
* with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
* please see the [OOjs UI documentation on MediaWiki][1].
*
- * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
+ * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
*
* @example
* // An example of selected, unselected, and disabled radio inputs
/**
* RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
- * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
+ * within an HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
* of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
* more information about input widgets.
*
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
/* Static Methods */
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
+ * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: …, disabled: … }`
*/
OO.ui.CheckboxMultiselectInputWidget = function OoUiCheckboxMultiselectInputWidget( config ) {
// Configuration initialization
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.CheckboxMultiselectInputWidget.static.supportsSimpleLabel = false;
/* Static Methods */
/**
* Set the options available for this input.
*
- * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: …, disabled: … }`
* @chainable
*/
OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options ) {
this.checkboxMultiselectWidget
.clearItems()
.addItems( options.map( function ( opt ) {
- var optValue, item;
+ var optValue, item, optDisabled;
optValue =
OO.ui.CheckboxMultiselectInputWidget.parent.prototype.cleanUpValue.call( widget, opt.data );
+ optDisabled = opt.disabled !== undefined ? opt.disabled : false;
item = new OO.ui.CheckboxMultioptionWidget( {
data: optValue,
- label: opt.label !== undefined ? opt.label : optValue
+ label: opt.label !== undefined ? opt.label : optValue,
+ disabled: optDisabled
} );
// Set the 'name' and 'value' for form submission
item.checkbox.$input.attr( 'name', widget.inputName );
* which modifies incoming values rather than validating them.
* Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
*
- * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
+ * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
*
* @example
* // Example of a text input widget
* - by choosing a value from the menu. The value of the chosen option will then appear in the text
* input field.
*
- * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
+ * This widget can be used inside an HTML form, such as a OO.ui.FormLayout.
*
* For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
*
* @throws {Error} An error is thrown if no widget is specified
*/
OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
- var hasInputWidget, $div;
-
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
config = fieldWidget;
throw new Error( 'Widget not found' );
}
- hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
-
// Configuration initialization
config = $.extend( { align: 'left' }, config );
OO.ui.FieldLayout.parent.call( this, config );
// Mixin constructors
- OO.ui.mixin.LabelElement.call( this, config );
+ OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, {
+ $label: $( '<label>' )
+ } ) );
OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
// Properties
this.notices = [];
this.$field = $( '<div>' );
this.$messages = $( '<ul>' );
- this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
+ this.$header = $( '<div>' );
+ this.$body = $( '<div>' );
this.align = null;
if ( config.help ) {
this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ popup: {
+ padded: true
+ },
classes: [ 'oo-ui-fieldLayout-help' ],
framed: false,
icon: 'info'
} );
-
- $div = $( '<div>' );
if ( config.help instanceof OO.ui.HtmlSnippet ) {
- $div.html( config.help.toString() );
+ this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
} else {
- $div.text( config.help );
+ this.popupButtonWidget.getPopup().$body.text( config.help );
}
- this.popupButtonWidget.getPopup().$body.append(
- $div.addClass( 'oo-ui-fieldLayout-help-content' )
- );
this.$help = this.popupButtonWidget.$element;
} else {
this.$help = $( [] );
}
// Events
- if ( hasInputWidget ) {
- this.$label.on( 'click', this.onLabelClick.bind( this ) );
- }
this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
// Initialization
+ if ( fieldWidget.constructor.static.supportsSimpleLabel ) {
+ this.$label.attr( 'for', this.fieldWidget.getInputId() );
+ }
this.$element
.addClass( 'oo-ui-fieldLayout' )
.toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget.isDisabled() )
- .append( this.$help, this.$body );
+ .append( this.$body );
this.$body.addClass( 'oo-ui-fieldLayout-body' );
+ this.$header.addClass( 'oo-ui-fieldLayout-header' );
this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
this.$field
.addClass( 'oo-ui-fieldLayout-field' )
this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
};
-/**
- * Handle label mouse click events.
- *
- * @private
- * @param {jQuery.Event} e Mouse click event
- */
-OO.ui.FieldLayout.prototype.onLabelClick = function () {
- this.fieldWidget.simulateLabelClick();
- return false;
-};
-
/**
* Get the widget contained by the field.
*
value = 'left';
}
// Reorder elements
- if ( value === 'inline' ) {
- this.$body.append( this.$field, this.$label );
+ if ( value === 'top' ) {
+ this.$header.append( this.$label, this.$help );
+ this.$body.append( this.$header, this.$field );
+ } else if ( value === 'inline' ) {
+ this.$header.append( this.$label, this.$help );
+ this.$body.append( this.$field, this.$header );
} else {
- this.$body.append( this.$label, this.$field );
+ 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
* For important messages, you are advised to use `notices`, as they are always shown.
*/
OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
- var $div;
-
// Configuration initialization
config = config || {};
OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: $( '<div>' ) } ) );
OO.ui.mixin.GroupElement.call( this, config );
+ // Properties
+ this.$header = $( '<div>' );
if ( config.help ) {
this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ popup: {
+ padded: true
+ },
classes: [ 'oo-ui-fieldsetLayout-help' ],
framed: false,
icon: 'info'
} );
-
- $div = $( '<div>' );
if ( config.help instanceof OO.ui.HtmlSnippet ) {
- $div.html( config.help.toString() );
+ this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
} else {
- $div.text( config.help );
+ this.popupButtonWidget.getPopup().$body.text( config.help );
}
- this.popupButtonWidget.getPopup().$body.append(
- $div.addClass( 'oo-ui-fieldsetLayout-help-content' )
- );
this.$help = this.popupButtonWidget.$element;
} else {
this.$help = $( [] );
}
// Initialization
+ this.$header
+ .addClass( 'oo-ui-fieldsetLayout-header' )
+ .append( this.$icon, this.$label, this.$help );
this.$group.addClass( 'oo-ui-fieldsetLayout-group' );
this.$element
.addClass( 'oo-ui-fieldsetLayout' )
- .prepend( this.$label, this.$help, this.$icon, this.$group );
+ .prepend( this.$header, this.$group );
if ( Array.isArray( config.items ) ) {
this.addItems( config.items );
}
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.FieldsetLayout.static.tagName = 'fieldset';
/**
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.FormLayout.static.tagName = 'form';
/* Methods */