/*!
- * OOjs UI v0.19.0
+ * OOjs UI v0.19.5
* 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-02-01T23:04:40Z
+ * Date: 2017-03-07T22:57:01Z
*/
( function ( OO ) {
}
};
+/**
+ * Get the number of pixels that an element's content is scrolled to the left.
+ *
+ * Adapted from <https://github.com/othree/jquery.rtl-scroll-type>.
+ * Original code copyright 2012 Wei-Ko Kao, licensed under the MIT License.
+ *
+ * This function smooths out browser inconsistencies (nicely described in the README at
+ * <https://github.com/othree/jquery.rtl-scroll-type>) and produces a result consistent
+ * with Firefox's 'scrollLeft', which seems the sanest.
+ *
+ * @static
+ * @method
+ * @param {HTMLElement|Window} el Element to measure
+ * @return {number} Scroll position from the left.
+ * If the element's direction is LTR, this is a positive number between `0` (initial scroll position)
+ * and `el.scrollWidth - el.clientWidth` (furthest possible scroll position).
+ * If the element's direction is RTL, this is a negative number between `0` (initial scroll position)
+ * and `-el.scrollWidth + el.clientWidth` (furthest possible scroll position).
+ */
+OO.ui.Element.static.getScrollLeft = ( function () {
+ var rtlScrollType = null;
+
+ function test() {
+ var $definer = $( '<div dir="rtl" style="font-size: 14px; width: 1px; height: 1px; position: absolute; top: -1000px; overflow: scroll">A</div>' ),
+ definer = $definer[ 0 ];
+
+ $definer.appendTo( 'body' );
+ if ( definer.scrollLeft > 0 ) {
+ // Safari, Chrome
+ rtlScrollType = 'default';
+ } else {
+ definer.scrollLeft = 1;
+ if ( definer.scrollLeft === 0 ) {
+ // Firefox, old Opera
+ rtlScrollType = 'negative';
+ } else {
+ // Internet Explorer, Edge
+ rtlScrollType = 'reverse';
+ }
+ }
+ $definer.remove();
+ }
+
+ return function getScrollLeft( el ) {
+ var isRoot = el.window === el ||
+ el === el.ownerDocument.body ||
+ el === el.ownerDocument.documentElement,
+ scrollLeft = isRoot ? $( window ).scrollLeft() : el.scrollLeft,
+ // All browsers use the correct scroll type ('negative') on the root, so don't
+ // do any fixups when looking at the root element
+ direction = isRoot ? 'ltr' : $( el ).css( 'direction' );
+
+ if ( direction === 'rtl' ) {
+ if ( rtlScrollType === null ) {
+ test();
+ }
+ if ( rtlScrollType === 'reverse' ) {
+ scrollLeft = -scrollLeft;
+ } else if ( rtlScrollType === 'default' ) {
+ scrollLeft = scrollLeft - el.scrollWidth + el.clientWidth;
+ }
+ }
+
+ return scrollLeft;
+ };
+}() );
+
/**
* Get scrollable object parent
*
*/
OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
var i, val,
- // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
+ // Browsers do not correctly return the computed value of 'overflow' when 'overflow-x' and
+ // 'overflow-y' have different values, so we need to check the separate properties.
props = [ 'overflow-x', 'overflow-y' ],
$parent = $( el ).parent();
i = props.length;
while ( i-- ) {
val = $parent.css( props[ i ] );
+ // We assume that elements with 'overflow' (in any direction) set to 'hidden' will never be
+ // scrolled in that direction, but they can actually be scrolled programatically. The user can
+ // unintentionally perform a scroll in such case even if the application doesn't scroll
+ // programatically, e.g. when jumping to an anchor, or when using built-in find functionality.
+ // This could cause funny issues...
if ( val === 'auto' || val === 'scroll' ) {
return $parent[ 0 ];
}
/* 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 ) );
+ if ( this.input.getInputId() ) {
+ this.$element.attr( 'for', this.input.getInputId() );
+ } else {
+ this.$label.on( 'click', function () {
+ this.fieldWidget.focus();
+ return false;
+ }.bind( this ) );
+ }
}
-
- // 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 will stick under a specified container, even when it is inserted elsewhere in the
- * document (for example, in a OO.ui.Window's $overlay).
+ * Element that will stick adjacent to a specified container, even when it is inserted elsewhere
+ * in the document (for example, in an 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
* @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
+ * @cfg {jQuery} [$floatableContainer] Node to position adjacent to
+ * @cfg {string} [verticalPosition='below'] Where to position $floatable vertically:
+ * 'below': Directly below $floatableContainer, aligning f's top edge with fC's bottom edge
+ * 'above': Directly above $floatableContainer, aligning f's bottom edge with fC's top edge
+ * 'top': Align the top edge with $floatableContainer's top edge
+ * 'bottom': Align the bottom edge with $floatableContainer's bottom edge
+ * 'center': Vertically align the center with $floatableContainer's center
+ * @cfg {string} [horizontalPosition='start'] Where to position $floatable horizontally:
+ * 'before': Directly before $floatableContainer, aligning f's end edge with fC's start edge
+ * 'after': Directly after $floatableContainer, algining f's start edge with fC's end edge
+ * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge
+ * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge
+ * 'center': Horizontally align the center with $floatableContainer's center
+ * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container
+ * is out of view
*/
OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
// Configuration initialization
// Initialization
this.setFloatableContainer( config.$floatableContainer );
this.setFloatableElement( config.$floatable || this.$element );
+ this.setVerticalPosition( config.verticalPosition || 'below' );
+ this.setHorizontalPosition( config.horizontalPosition || 'start' );
+ this.hideWhenOutOfView = config.hideWhenOutOfView === undefined ? true : !!config.hideWhenOutOfView;
};
/* Methods */
/**
* Set floatable container.
*
- * The element will be always positioned under the specified container.
+ * The element will be positioned relative to the specified container.
*
* @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
*/
}
};
+/**
+ * Change how the element is positioned vertically.
+ *
+ * @param {string} position 'below', 'above', 'top', 'bottom' or 'center'
+ */
+OO.ui.mixin.FloatableElement.prototype.setVerticalPosition = function ( position ) {
+ if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position ) === -1 ) {
+ throw new Error( 'Invalid value for vertical position: ' + position );
+ }
+ if ( this.verticalPosition !== position ) {
+ this.verticalPosition = position;
+ if ( this.$floatable ) {
+ this.position();
+ }
+ }
+};
+
+/**
+ * Change how the element is positioned horizontally.
+ *
+ * @param {string} position 'before', 'after', 'start', 'end' or 'center'
+ */
+OO.ui.mixin.FloatableElement.prototype.setHorizontalPosition = function ( position ) {
+ if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position ) === -1 ) {
+ throw new Error( 'Invalid value for horizontal position: ' + position );
+ }
+ if ( this.horizontalPosition !== position ) {
+ this.horizontalPosition = position;
+ if ( this.$floatable ) {
+ this.position();
+ }
+ }
+};
+
/**
* Toggle positioning.
*
positioning = positioning === undefined ? !this.positioning : !!positioning;
+ if ( positioning && !this.warnedUnattached && !this.isElementAttached() ) {
+ OO.ui.warnDeprecation( 'FloatableElement#togglePositioning: Before calling this method, the element must be attached to the DOM.' );
+ this.warnedUnattached = true;
+ }
+
if ( this.positioning !== positioning ) {
this.positioning = positioning;
+ this.needsCustomPosition =
+ this.verticalPostion !== 'below' ||
+ this.horizontalPosition !== 'start' ||
+ !OO.ui.contains( this.$floatableContainer[ 0 ], this.$floatable[ 0 ] );
+
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' ) ) {
this.$floatableClosestScrollable = null;
}
- this.$floatable.css( { left: '', top: '' } );
+ this.$floatable.css( { left: '', right: '', top: '' } );
}
}
* @return {boolean}
*/
OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element, $container ) {
- var elemRect, contRect,
- leftEdgeInBounds = false,
- bottomEdgeInBounds = false,
- rightEdgeInBounds = false;
+ var elemRect, contRect, topEdgeInBounds, bottomEdgeInBounds, leftEdgeInBounds, rightEdgeInBounds,
+ startEdgeInBounds, endEdgeInBounds,
+ direction = $element.css( 'direction' );
elemRect = $element[ 0 ].getBoundingClientRect();
if ( $container[ 0 ] === window ) {
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;
+ topEdgeInBounds = elemRect.top >= contRect.top && elemRect.top <= contRect.bottom;
+ bottomEdgeInBounds = elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom;
+ leftEdgeInBounds = elemRect.left >= contRect.left && elemRect.left <= contRect.right;
+ rightEdgeInBounds = elemRect.right >= contRect.left && elemRect.right <= contRect.right;
+ if ( direction === 'rtl' ) {
+ startEdgeInBounds = rightEdgeInBounds;
+ endEdgeInBounds = leftEdgeInBounds;
+ } else {
+ startEdgeInBounds = leftEdgeInBounds;
+ endEdgeInBounds = rightEdgeInBounds;
}
- if ( elemRect.bottom >= contRect.top && elemRect.bottom <= contRect.bottom ) {
- bottomEdgeInBounds = true;
+
+ if ( this.verticalPosition === 'below' && !bottomEdgeInBounds ) {
+ return false;
}
- if ( elemRect.right >= contRect.left && elemRect.right <= contRect.right ) {
- rightEdgeInBounds = true;
+ if ( this.verticalPosition === 'above' && !topEdgeInBounds ) {
+ return false;
+ }
+ if ( this.horizontalPosition === 'before' && !startEdgeInBounds ) {
+ return false;
+ }
+ if ( this.horizontalPosition === 'after' && !endEdgeInBounds ) {
+ return false;
}
- // We only care that any part of the bottom edge is visible
- return bottomEdgeInBounds && ( leftEdgeInBounds || rightEdgeInBounds );
+ // The other positioning values are all about being inside the container,
+ // so in those cases all we care about is that any part of the container is visible.
+ return elemRect.top <= contRect.bottom && elemRect.bottom >= contRect.top &&
+ elemRect.left <= contRect.right && elemRect.right >= contRect.left;
};
/**
* @chainable
*/
OO.ui.mixin.FloatableElement.prototype.position = function () {
- var pos;
-
if ( !this.positioning ) {
return this;
}
- if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
+ if ( this.hideWhenOutOfView && !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) {
this.$floatable.addClass( 'oo-ui-element-hidden' );
- return;
+ return this;
} else {
this.$floatable.removeClass( 'oo-ui-element-hidden' );
}
if ( !this.needsCustomPosition ) {
- return;
+ return this;
}
- pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
-
- // Position under container
- pos.top += this.$floatableContainer.height();
- this.$floatable.css( pos );
+ this.$floatable.css( this.computePosition() );
// We updated the position, so re-evaluate the clipping state.
// (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
return this;
};
+/**
+ * Compute how #$floatable should be positioned based on the position of #$floatableContainer
+ * and the positioning settings. This is a helper for #position that shouldn't be called directly,
+ * but may be overridden by subclasses if they want to change or add to the positioning logic.
+ *
+ * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'.
+ */
+OO.ui.mixin.FloatableElement.prototype.computePosition = function () {
+ var isBody, scrollableX, scrollableY, containerPos,
+ horizScrollbarHeight, vertScrollbarWidth, scrollTop, scrollLeft,
+ newPos = { top: '', left: '', bottom: '', right: '' },
+ direction = this.$floatableContainer.css( 'direction' ),
+ $offsetParent = this.$floatable.offsetParent();
+
+ if ( $offsetParent.is( 'html' ) ) {
+ // The innerHeight/Width and clientHeight/Width calculations don't work well on the
+ // <html> element, but they do work on the <body>
+ $offsetParent = $( $offsetParent[ 0 ].ownerDocument.body );
+ }
+ isBody = $offsetParent.is( 'body' );
+ scrollableX = $offsetParent.css( 'overflow-x' ) === 'scroll' || $offsetParent.css( 'overflow-x' ) === 'auto';
+ scrollableY = $offsetParent.css( 'overflow-y' ) === 'scroll' || $offsetParent.css( 'overflow-y' ) === 'auto';
+
+ vertScrollbarWidth = $offsetParent.innerWidth() - $offsetParent.prop( 'clientWidth' );
+ horizScrollbarHeight = $offsetParent.innerHeight() - $offsetParent.prop( 'clientHeight' );
+ // We don't need to compute and add scrollTop and scrollLeft if the scrollable container is the body,
+ // or if it isn't scrollable
+ scrollTop = scrollableY && !isBody ? $offsetParent.scrollTop() : 0;
+ scrollLeft = scrollableX && !isBody ? OO.ui.Element.static.getScrollLeft( $offsetParent[ 0 ] ) : 0;
+
+ // Avoid passing the <body> to getRelativePosition(), because it won't return what we expect
+ // if the <body> has a margin
+ containerPos = isBody ?
+ this.$floatableContainer.offset() :
+ OO.ui.Element.static.getRelativePosition( this.$floatableContainer, $offsetParent );
+ containerPos.bottom = containerPos.top + this.$floatableContainer.outerHeight();
+ containerPos.right = containerPos.left + this.$floatableContainer.outerWidth();
+ containerPos.start = direction === 'rtl' ? containerPos.right : containerPos.left;
+ containerPos.end = direction === 'rtl' ? containerPos.left : containerPos.right;
+
+ if ( this.verticalPosition === 'below' ) {
+ newPos.top = containerPos.bottom;
+ } else if ( this.verticalPosition === 'above' ) {
+ newPos.bottom = $offsetParent.outerHeight() - containerPos.top;
+ } else if ( this.verticalPosition === 'top' ) {
+ newPos.top = containerPos.top;
+ } else if ( this.verticalPosition === 'bottom' ) {
+ newPos.bottom = $offsetParent.outerHeight() - containerPos.bottom;
+ } else if ( this.verticalPosition === 'center' ) {
+ newPos.top = containerPos.top +
+ ( this.$floatableContainer.height() - this.$floatable.height() ) / 2;
+ }
+
+ if ( this.horizontalPosition === 'before' ) {
+ newPos.end = containerPos.start;
+ } else if ( this.horizontalPosition === 'after' ) {
+ newPos.start = containerPos.end;
+ } else if ( this.horizontalPosition === 'start' ) {
+ newPos.start = containerPos.start;
+ } else if ( this.horizontalPosition === 'end' ) {
+ newPos.end = containerPos.end;
+ } else if ( this.horizontalPosition === 'center' ) {
+ newPos.left = containerPos.left +
+ ( this.$floatableContainer.width() - this.$floatable.width() ) / 2;
+ }
+
+ if ( newPos.start !== undefined ) {
+ if ( direction === 'rtl' ) {
+ newPos.right = ( isBody ? $( $offsetParent[ 0 ].ownerDocument.documentElement ) : $offsetParent ).outerWidth() - newPos.start;
+ } else {
+ newPos.left = newPos.start;
+ }
+ delete newPos.start;
+ }
+ if ( newPos.end !== undefined ) {
+ if ( direction === 'rtl' ) {
+ newPos.left = newPos.end;
+ } else {
+ newPos.right = ( isBody ? $( $offsetParent[ 0 ].ownerDocument.documentElement ) : $offsetParent ).outerWidth() - newPos.end;
+ }
+ delete newPos.end;
+ }
+
+ // Account for scroll position
+ if ( newPos.top !== '' ) {
+ newPos.top += scrollTop;
+ }
+ if ( newPos.bottom !== '' ) {
+ newPos.bottom -= scrollTop;
+ }
+ if ( newPos.left !== '' ) {
+ newPos.left += scrollLeft;
+ }
+ if ( newPos.right !== '' ) {
+ newPos.right -= scrollLeft;
+ }
+
+ // Account for scrollbar gutter
+ if ( newPos.bottom !== '' ) {
+ newPos.bottom -= horizScrollbarHeight;
+ }
+ if ( direction === 'rtl' ) {
+ if ( newPos.left !== '' ) {
+ newPos.left -= vertScrollbarWidth;
+ }
+ } else {
+ if ( newPos.right !== '' ) {
+ newPos.right -= vertScrollbarWidth;
+ }
+ }
+
+ return newPos;
+};
+
/**
* Element that can be automatically clipped to visible boundaries.
*
OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
clipping = clipping === undefined ? !this.clipping : !!clipping;
+ if ( clipping && !this.warnedUnattached && !this.isElementAttached() ) {
+ OO.ui.warnDeprecation( 'ClippableElement#toggleClipping: Before calling this method, the element must be attached to the DOM.' );
+ this.warnedUnattached = true;
+ }
+
if ( this.clipping !== clipping ) {
this.clipping = clipping;
if ( clipping ) {
clipHeight = allotedHeight < naturalHeight;
if ( clipWidth ) {
+ // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. (T157672)
+ // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
+ this.$clippable.css( 'overflowX', 'scroll' );
+ void this.$clippable[ 0 ].offsetHeight; // Force reflow
this.$clippable.css( {
- overflowX: 'scroll',
width: Math.max( 0, allotedWidth ),
maxWidth: ''
} );
} );
}
if ( clipHeight ) {
+ // The order matters here. If overflow is not set first, Chrome displays bogus scrollbars. (T157672)
+ // Forcing a reflow is a smaller workaround than calling reconsiderScrollbars() for this case.
+ this.$clippable.css( 'overflowY', 'scroll' );
+ void this.$clippable[ 0 ].offsetHeight; // Force reflow
this.$clippable.css( {
- overflowY: 'scroll',
height: Math.max( 0, allotedHeight ),
maxHeight: ''
} );
* By default, each popup has an anchor that points toward its origin.
* Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
*
+ * Unlike most widgets, PopupWidget is initially hidden and must be shown by calling #toggle.
+ *
* @example
* // A popup widget.
* var popup = new OO.ui.PopupWidget( {
* @cfg {number} [width=320] Width of popup in pixels
* @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
* @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
- * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
- * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
- * popup is leaning towards the right of the screen.
- * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
- * in the given language, which means it will flip to the correct positioning in right-to-left languages.
- * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
- * sentence in the given language.
+ * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
+ * 'above': Put popup above $floatableContainer; anchor points down to the start edge of $floatableContainer
+ * 'below': Put popup below $floatableContainer; anchor points up to the start edge of $floatableContainer
+ * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points
+ * endwards (right/left) to the vertical center of $floatableContainer
+ * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points
+ * startwards (left/right) to the vertical center of $floatableContainer
+ * @cfg {string} [align='center'] How to align the popup to $floatableContainer
+ * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL)
+ * as possible while still keeping the anchor within the popup;
+ * if position is before/after, move the popup as far downwards as possible.
+ * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL)
+ * as possible while still keeping the anchor within the popup;
+ * if position in before/after, move the popup as far upwards as possible.
+ * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center
+ * of the popup with the center of $floatableContainer.
+ * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL
+ * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR
* @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
* See the [OOjs UI docs on MediaWiki][3] for an example.
* [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
this.autoClose = !!config.autoClose;
this.$autoCloseIgnore = config.$autoCloseIgnore;
this.transitionTimeout = null;
- this.anchor = null;
+ this.anchored = false;
this.width = config.width !== undefined ? config.width : 320;
this.height = config.height !== undefined ? config.height : null;
- this.setAlignment( config.align );
this.onMouseDownHandler = this.onMouseDown.bind( this );
this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
// Initialization
this.toggleAnchor( config.anchor === undefined || config.anchor );
+ this.setAlignment( config.align || 'center' );
+ this.setPosition( config.position || 'below' );
this.$body.addClass( 'oo-ui-popupWidget-body' );
this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
this.$popup
this.anchored = show;
}
};
+/**
+ * Change which edge the anchor appears on.
+ *
+ * @param {string} edge 'top', 'bottom', 'start' or 'end'
+ */
+OO.ui.PopupWidget.prototype.setAnchorEdge = function ( edge ) {
+ if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge ) === -1 ) {
+ throw new Error( 'Invalid value for edge: ' + edge );
+ }
+ if ( this.anchorEdge !== null ) {
+ this.$element.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge );
+ }
+ this.anchorEdge = edge;
+ this.$element.addClass( 'oo-ui-popupWidget-anchored-' + edge );
+};
/**
* Check if the anchor is visible.
* @return {boolean} Anchor is visible
*/
OO.ui.PopupWidget.prototype.hasAnchor = function () {
- return this.anchor;
+ return this.anchored;
};
/**
+ * Toggle visibility of the popup. The popup is initially hidden and must be shown by calling
+ * `.toggle( true )` after its #$element is attached to the DOM.
+ *
+ * Do not show the popup while it is not attached to the DOM. The calculations required to display
+ * it in the right place and with the right dimensions only work correctly while it is attached.
+ * Side-effects may include broken interface and exceptions being thrown. This wasn't always
+ * strictly enforced, so currently it only generates a warning in the browser console.
+ *
* @inheritdoc
*/
OO.ui.PopupWidget.prototype.toggle = function ( show ) {
change = show !== this.isVisible();
+ if ( show && !this.warnedUnattached && !this.isElementAttached() ) {
+ OO.ui.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' );
+ this.warnedUnattached = true;
+ }
+ if ( show && !this.$floatableContainer && this.isElementAttached() ) {
+ // Fall back to the parent node if the floatableContainer is not set
+ this.setFloatableContainer( this.$element.parent() );
+ }
+
// Parent method
OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
* @chainable
*/
OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
- var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
- popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
- align = this.align,
- widget = this;
-
- if ( !this.$container ) {
- // Lazy-initialize $container if not specified in constructor
- this.$container = $( this.getClosestScrollableElementContainer() );
- }
-
- // Set height and width before measuring things, since it might cause our measurements
- // to change (e.g. due to scrollbars appearing or disappearing)
- this.$popup.css( {
- width: this.width,
- height: this.height !== null ? this.height : 'auto'
- } );
-
- // If we are in RTL, we need to flip the alignment, unless it is center
- if ( align === 'forwards' || align === 'backwards' ) {
- if ( this.$container.css( 'direction' ) === 'rtl' ) {
- align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
- } else {
- align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
- }
-
- }
-
- // Compute initial popupOffset based on alignment
- popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
-
- // Figure out if this will cause the popup to go beyond the edge of the container
- originOffset = this.$element.offset().left;
- containerLeft = this.$container.offset().left;
- containerWidth = this.$container.innerWidth();
- containerRight = containerLeft + containerWidth;
- popupLeft = popupOffset - this.containerPadding;
- popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
- overlapLeft = ( originOffset + popupLeft ) - containerLeft;
- overlapRight = containerRight - ( originOffset + popupRight );
-
- // Adjust offset to make the popup not go beyond the edge, if needed
- if ( overlapRight < 0 ) {
- popupOffset += overlapRight;
- } else if ( overlapLeft < 0 ) {
- popupOffset -= overlapLeft;
- }
-
- // Adjust offset to avoid anchor being rendered too close to the edge
- // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
- // TODO: Find a measurement that works for CSS anchors and image anchors
- anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
- if ( popupOffset + this.width < anchorWidth ) {
- popupOffset = anchorWidth - this.width;
- } else if ( -popupOffset < anchorWidth ) {
- popupOffset = -anchorWidth;
- }
+ var widget = this;
// Prevent transition from being interrupted
clearTimeout( this.transitionTimeout );
this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
}
- // Position body relative to anchor
- this.$popup.css( 'margin-left', popupOffset );
+ this.position();
if ( transition ) {
// Prevent transitioning after transition is complete
// Prevent transitioning immediately
this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
}
+};
- // Reevaluate clipping state since we've relocated and resized the popup
- this.clip();
+/**
+ * @inheritdoc
+ */
+OO.ui.PopupWidget.prototype.computePosition = function () {
+ var direction, align, vertical, start, end, near, far, sizeProp, popupSize, anchorSize, anchorPos,
+ anchorOffset, anchorMargin, parentPosition, positionProp, positionAdjustment, floatablePos,
+ offsetParentPos, containerPos,
+ popupPos = {},
+ anchorCss = { left: '', right: '', top: '', bottom: '' },
+ alignMap = {
+ ltr: {
+ 'force-left': 'backwards',
+ 'force-right': 'forwards'
+ },
+ rtl: {
+ 'force-left': 'forwards',
+ 'force-right': 'backwards'
+ }
+ },
+ anchorEdgeMap = {
+ above: 'bottom',
+ below: 'top',
+ before: 'end',
+ after: 'start'
+ },
+ hPosMap = {
+ forwards: 'start',
+ center: 'center',
+ backwards: 'before'
+ },
+ vPosMap = {
+ forwards: 'top',
+ center: 'center',
+ backwards: 'bottom'
+ };
- return this;
+ if ( !this.$container ) {
+ // Lazy-initialize $container if not specified in constructor
+ this.$container = $( this.getClosestScrollableElementContainer() );
+ }
+ direction = this.$container.css( 'direction' );
+
+ // Set height and width before we do anything else, since it might cause our measurements
+ // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering
+ this.$popup.css( {
+ width: this.width,
+ height: this.height !== null ? this.height : 'auto'
+ } );
+
+ align = alignMap[ direction ][ this.align ] || this.align;
+ // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal
+ vertical = this.popupPosition === 'before' || this.popupPosition === 'after';
+ start = vertical ? 'top' : ( direction === 'rtl' ? 'right' : 'left' );
+ end = vertical ? 'bottom' : ( direction === 'rtl' ? 'left' : 'right' );
+ near = vertical ? 'top' : 'left';
+ far = vertical ? 'bottom' : 'right';
+ sizeProp = vertical ? 'Height' : 'Width';
+ popupSize = vertical ? ( this.height || this.$popup.height() ) : this.width;
+
+ this.setAnchorEdge( anchorEdgeMap[ this.popupPosition ] );
+ this.horizontalPosition = vertical ? this.popupPosition : hPosMap[ align ];
+ this.verticalPosition = vertical ? vPosMap[ align ] : this.popupPosition;
+
+ // Parent method
+ parentPosition = OO.ui.mixin.FloatableElement.prototype.computePosition.call( this );
+ // Find out which property FloatableElement used for positioning, and adjust that value
+ positionProp = vertical ?
+ ( parentPosition.top !== '' ? 'top' : 'bottom' ) :
+ ( parentPosition.left !== '' ? 'left' : 'right' );
+
+ // Figure out where the near and far edges of the popup and $floatableContainer are
+ floatablePos = this.$floatableContainer.offset();
+ floatablePos[ far ] = floatablePos[ near ] + this.$floatableContainer[ 'outer' + sizeProp ]();
+ // Measure where the offsetParent is and compute our position based on that and parentPosition
+ offsetParentPos = this.$element.offsetParent().offset();
+
+ if ( positionProp === near ) {
+ popupPos[ near ] = offsetParentPos[ near ] + parentPosition[ near ];
+ popupPos[ far ] = popupPos[ near ] + popupSize;
+ } else {
+ popupPos[ far ] = offsetParentPos[ near ] +
+ this.$element.offsetParent()[ 'inner' + sizeProp ]() - parentPosition[ far ];
+ popupPos[ near ] = popupPos[ far ] - popupSize;
+ }
+
+ // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer
+ // For popups above/below, we point to the start edge; for popups before/after, we point to the center
+ anchorPos = vertical ? ( floatablePos[ start ] + floatablePos[ end ] ) / 2 : floatablePos[ start ];
+ anchorOffset = ( start === far ? -1 : 1 ) * ( anchorPos - popupPos[ start ] );
+
+ // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space
+ // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height
+ anchorSize = this.$anchor[ 0 ][ 'scroll' + sizeProp ];
+ anchorMargin = parseFloat( this.$anchor.css( 'margin-' + start ) );
+ if ( anchorOffset + anchorMargin < 2 * anchorSize ) {
+ // Not enough space for the anchor on the start side; pull the popup startwards
+ positionAdjustment = ( positionProp === start ? -1 : 1 ) *
+ ( 2 * anchorSize - ( anchorOffset + anchorMargin ) );
+ } else if ( anchorOffset + anchorMargin > popupSize - 2 * anchorSize ) {
+ // Not enough space for the anchor on the end side; pull the popup endwards
+ positionAdjustment = ( positionProp === end ? -1 : 1 ) *
+ ( anchorOffset + anchorMargin - ( popupSize - 2 * anchorSize ) );
+ } else {
+ positionAdjustment = 0;
+ }
+
+ // Check if the popup will go beyond the edge of this.$container
+ containerPos = this.$container.offset();
+ containerPos[ far ] = containerPos[ near ] + this.$container[ 'inner' + sizeProp ]();
+ // Take into account how much the popup will move because of the adjustments we're going to make
+ popupPos[ near ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment;
+ popupPos[ far ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment;
+ if ( containerPos[ near ] + this.containerPadding > popupPos[ near ] ) {
+ // Popup goes beyond the near (left/top) edge, move it to the right/bottom
+ positionAdjustment += ( positionProp === near ? 1 : -1 ) *
+ ( containerPos[ near ] + this.containerPadding - popupPos[ near ] );
+ } else if ( containerPos[ far ] - this.containerPadding < popupPos[ far ] ) {
+ // Popup goes beyond the far (right/bottom) edge, move it to the left/top
+ positionAdjustment += ( positionProp === far ? 1 : -1 ) *
+ ( popupPos[ far ] - ( containerPos[ far ] - this.containerPadding ) );
+ }
+
+ // Adjust anchorOffset for positionAdjustment
+ anchorOffset += ( positionProp === start ? -1 : 1 ) * positionAdjustment;
+
+ // Position the anchor
+ anchorCss[ start ] = anchorOffset;
+ this.$anchor.css( anchorCss );
+ // Move the popup if needed
+ parentPosition[ positionProp ] += positionAdjustment;
+
+ return parentPosition;
};
/**
} else {
this.align = 'center';
}
+ this.position();
};
/**
* Get popup alignment
*
- * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
+ * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`,
* `backwards` or `forwards`.
*/
OO.ui.PopupWidget.prototype.getAlignment = function () {
return this.align;
};
+/**
+ * Change the positioning of the popup.
+ *
+ * @param {string} position 'above', 'below', 'before' or 'after'
+ */
+OO.ui.PopupWidget.prototype.setPosition = function ( position ) {
+ if ( [ 'above', 'below', 'before', 'after' ].indexOf( position ) === -1 ) {
+ position = 'below';
+ }
+ this.popupPosition = position;
+ this.position();
+};
+
+/**
+ * Get popup positioning.
+ *
+ * @return {string} 'above', 'below', 'before' or 'after'
+ */
+OO.ui.PopupWidget.prototype.getPosition = function () {
+ return this.popupPosition;
+};
+
/**
* PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
* A popup is a container for content. It is overlaid and positioned absolutely. By default, each
// Properties
this.popup = new OO.ui.PopupWidget( $.extend(
- { autoClose: true },
+ {
+ autoClose: true,
+ $floatableContainer: this.$element
+ },
config.popup,
- { $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore ) }
+ {
+ $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore )
+ }
) );
};
OO.ui.PopupButtonWidget.parent.call( this, config );
// Mixin constructors
- OO.ui.mixin.PopupElement.call( this, $.extend( true, {}, config, {
- popup: {
- $floatableContainer: this.$element
- }
- } ) );
+ OO.ui.mixin.PopupElement.call( this, config );
// Properties
this.$overlay = config.$overlay || this.$element;
/* 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 */
/* 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;
/**
* - Down-arrow key: highlight the next menu option
* - Esc key: hide the menu
*
+ * Unlike most widgets, MenuSelectWidget is initially hidden and must be shown by calling #toggle.
+ *
* Please see the [OOjs UI documentation on MediaWiki][1] for more information.
* [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
*
* that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
* that button, unless the button (or its parent widget) is passed in here.
* @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
+ * @cfg {boolean} [hideOnChoose=true] Hide the menu when the user chooses an option.
* @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
*/
OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
// Properties
this.autoHide = config.autoHide === undefined || !!config.autoHide;
+ this.hideOnChoose = config.hideOnChoose === undefined || !!config.hideOnChoose;
this.filterFromInput = !!config.filterFromInput;
this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
this.$widget = config.widget ? config.widget.$element : null;
* @protected
*/
OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
- var i, item, visible,
+ var i, item, visible, section, sectionEmpty,
anyVisible = false,
len = this.items.length,
showAll = !this.isVisible(),
filter = showAll ? null : this.getItemMatcher( this.$input.val() );
+ // Hide non-matching options, and also hide section headers if all options
+ // in their section are hidden.
for ( i = 0; i < len; i++ ) {
item = this.items[ i ];
- if ( item instanceof OO.ui.OptionWidget ) {
+ if ( item instanceof OO.ui.MenuSectionOptionWidget ) {
+ if ( section ) {
+ // If the previous section was empty, hide its header
+ section.toggle( showAll || !sectionEmpty );
+ }
+ section = item;
+ sectionEmpty = true;
+ } else if ( item instanceof OO.ui.OptionWidget ) {
visible = showAll || filter( item );
anyVisible = anyVisible || visible;
+ sectionEmpty = sectionEmpty && !visible;
item.toggle( visible );
}
}
+ // Process the final section
+ if ( section ) {
+ section.toggle( showAll || !sectionEmpty );
+ }
this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
/**
* Choose an item.
*
- * When a user chooses an item, the menu is closed.
+ * When a user chooses an item, the menu is closed, unless the hideOnChoose config option is set to false.
*
* Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
* or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
*/
OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
- this.toggle( false );
+ if ( this.hideOnChoose ) {
+ this.toggle( false );
+ }
return this;
};
};
/**
+ * Toggle visibility of the menu. The menu is initially hidden and must be shown by calling
+ * `.toggle( true )` after its #$element is attached to the DOM.
+ *
+ * Do not show the menu while it is not attached to the DOM. The calculations required to display
+ * it in the right place and with the right dimensions only work correctly while it is attached.
+ * Side-effects may include broken interface and exceptions being thrown. This wasn't always
+ * strictly enforced, so currently it only generates a warning in the browser console.
+ *
* @inheritdoc
*/
OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
change = visible !== this.isVisible();
+ if ( visible && !this.warnedUnattached && !this.isElementAttached() ) {
+ OO.ui.warnDeprecation( 'MenuSelectWidget#toggle: Before calling this method, the menu must be attached to the DOM.' );
+ this.warnedUnattached = true;
+ }
+
// Parent method
OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
/* 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 */
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.CheckboxMultioptionWidget.static.tagName = 'label';
/* Methods */
/* 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;
/* 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 );
* specifies minimum number of rows to display.
* @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
* Use the #maxRows config to specify a maximum number of displayed rows.
- * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
+ * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true.
* Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
* @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
* the value or placeholder text: `'before'` or `'after'`
}
}
+ // Check browser validity and reject if it is invalid
+ if (
+ this.$input[ 0 ].checkValidity !== undefined &&
+ this.$input[ 0 ].checkValidity() === false
+ ) {
+ return rejectOrResolve( false );
+ }
+
+ // Run our checks if the browser thinks the field is valid
if ( this.validate instanceof Function ) {
result = this.validate( this.getValue() );
if ( result && $.isFunction( result.promise ) ) {
* - by choosing a value from the menu. The value of the chosen option will then appear in the text
* input field.
*
+ * After the user chooses an option, its `data` will be used as a new value for the widget.
+ * A `label` also can be specified for each option: if given, it will be shown instead of the
+ * `data` in the dropdown menu.
+ *
* 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].
* @example
* // Example: A ComboBoxInputWidget.
* var comboBox = new OO.ui.ComboBoxInputWidget( {
- * label: 'ComboBoxInputWidget',
* value: 'Option 1',
- * menu: {
- * items: [
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 1',
- * label: 'Option One'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 2',
- * label: 'Option Two'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 3',
- * label: 'Option Three'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 4',
- * label: 'Option Four'
- * } ),
- * new OO.ui.MenuOptionWidget( {
- * data: 'Option 5',
- * label: 'Option Five'
- * } )
- * ]
- * }
+ * options: [
+ * { data: 'Option 1' },
+ * { data: 'Option 2' },
+ * { data: 'Option 3' }
+ * ]
+ * } );
+ * $( 'body' ).append( comboBox.$element );
+ *
+ * @example
+ * // Example: A ComboBoxInputWidget with additional option labels.
+ * var comboBox = new OO.ui.ComboBoxInputWidget( {
+ * value: 'Option 1',
+ * options: [
+ * {
+ * data: 'Option 1',
+ * label: 'Option One'
+ * },
+ * {
+ * data: 'Option 2',
+ * label: 'Option Two'
+ * },
+ * {
+ * data: 'Option 3',
+ * label: 'Option Three'
+ * }
+ * ]
* } );
* $( 'body' ).append( comboBox.$element );
*
* @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
* in the upper-right corner of the rendered field; clicking it will display the text in a popup.
* For important messages, you are advised to use `notices`, as they are always shown.
+ * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
*
* @throws {Error} An error is thrown if no widget is specified
*/
OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
- var hasInputWidget;
-
// 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.$field = $( '<div>' );
this.$messages = $( '<ul>' );
this.$header = $( '<div>' );
- this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
+ this.$body = $( '<div>' );
this.align = null;
if ( config.help ) {
this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ $overlay: config.$overlay,
popup: {
padded: true
},
}
// Events
- if ( hasInputWidget ) {
- this.$label.on( 'click', this.onLabelClick.bind( this ) );
- }
this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
// Initialization
+ if ( fieldWidget.constructor.static.supportsSimpleLabel ) {
+ if ( this.fieldWidget.getInputId() ) {
+ this.$label.attr( 'for', this.fieldWidget.getInputId() );
+ } else {
+ this.$label.on( 'click', function () {
+ this.fieldWidget.focus();
+ return false;
+ }.bind( this ) );
+ }
+ }
this.$element
.addClass( 'oo-ui-fieldLayout' )
.toggleClass( 'oo-ui-fieldLayout-disabled', this.fieldWidget.isDisabled() )
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.
*
* @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
* in the upper-right corner of the rendered field; clicking it will display the text in a popup.
* For important messages, you are advised to use `notices`, as they are always shown.
+ * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if `help` is given.
*/
OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
// Configuration initialization
this.$header = $( '<div>' );
if ( config.help ) {
this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
+ $overlay: config.$overlay,
popup: {
padded: true
},
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.FieldsetLayout.static.tagName = 'fieldset';
/**
/* Static Properties */
+/**
+ * @static
+ * @inheritdoc
+ */
OO.ui.FormLayout.static.tagName = 'form';
/* Methods */