/*!
- * OOUI v0.27.1
+ * OOUI v0.27.6
* https://www.mediawiki.org/wiki/OOUI
*
* Copyright 2011–2018 OOUI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2018-05-29T23:24:49Z
+ * Date: 2018-08-01T22:17:59Z
*/
( function ( OO ) {
*
* @param {string|HTMLElement|jQuery} idOrNode
* A DOM id (if a string) or node for the widget to infuse.
+ * @param {Object} [config] Configuration options
* @return {OO.ui.Element}
* The `OO.ui.Element` corresponding to this (infusable) document node.
*/
-OO.ui.infuse = function ( idOrNode ) {
- return OO.ui.Element.static.infuse( idOrNode );
+OO.ui.infuse = function ( idOrNode, config ) {
+ return OO.ui.Element.static.infuse( idOrNode, config );
};
( function () {
*
* @param {string|HTMLElement|jQuery} idOrNode
* A DOM id (if a string) or node for the widget to infuse.
+ * @param {Object} [config] Configuration options
* @return {OO.ui.Element}
* The `OO.ui.Element` corresponding to this (infusable) document node.
* For `Tag` objects emitted on the HTML side (used occasionally for content)
* the value returned is a newly-created Element wrapping around the existing
* DOM node.
*/
-OO.ui.Element.static.infuse = function ( idOrNode ) {
- var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
+OO.ui.Element.static.infuse = function ( idOrNode, config ) {
+ var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, config, false );
// Verify that the type matches up.
// FIXME: uncomment after T89721 is fixed, see T90929.
/*
*
* @private
* @param {string|HTMLElement|jQuery} idOrNode
- * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
+ * @param {Object} [config] Configuration options
+ * @param {jQuery.Promise} [domPromise] A promise that will be resolved
* when the top-level widget of this infusion is inserted into DOM,
- * replacing the original node; or false for top-level invocation.
+ * replacing the original node; only used internally.
* @return {OO.ui.Element}
*/
-OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
+OO.ui.Element.static.unsafeInfuse = function ( idOrNode, config, domPromise ) {
// look for a cached result of a previous infusion.
var id, $elem, error, data, cls, parts, parent, obj, top, state, infusedChildren;
if ( typeof idOrNode === 'string' ) {
}
if ( data._ === 'Tag' ) {
// Special case: this is a raw Tag; wrap existing node, don't rebuild.
- return new OO.ui.Element( { $element: $elem } );
+ return new OO.ui.Element( $.extend( {}, config, { $element: $elem } ) );
}
parts = data._.split( '.' );
cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
}
- if ( domPromise === false ) {
+ if ( !domPromise ) {
top = $.Deferred();
domPromise = top.promise();
}
var infused;
if ( OO.isPlainObject( value ) ) {
if ( value.tag ) {
- infused = OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
+ infused = OO.ui.Element.static.unsafeInfuse( value.tag, config, domPromise );
infusedChildren.push( infused );
// Flatten the structure
infusedChildren.push.apply( infusedChildren, infused.$element.data( 'ooui-infused-children' ) || [] );
state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
// rebuild widget
// eslint-disable-next-line new-cap
- obj = new cls( data );
+ obj = new cls( $.extend( {}, config, data ) );
// If anyone is holding a reference to the old DOM element,
// let's allow them to OO.ui.infuse() it and do what they expect, see T105828.
// Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
this.$icon = $icon
.addClass( 'oo-ui-iconElement-icon' )
+ .toggleClass( 'oo-ui-iconElement-noIcon', !this.icon )
.toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
if ( this.iconTitle !== null ) {
this.$icon.attr( 'title', this.iconTitle );
}
this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
+ if ( this.$icon ) {
+ this.$icon.toggleClass( 'oo-ui-iconElement-noIcon', !this.icon );
+ }
this.updateThemeClasses();
return this;
this.$indicator = $indicator
.addClass( 'oo-ui-indicatorElement-indicator' )
+ .toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator )
.toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
if ( this.indicatorTitle !== null ) {
this.$indicator.attr( 'title', this.indicatorTitle );
}
this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
+ if ( this.$indicator ) {
+ this.$indicator.toggleClass( 'oo-ui-indicatorElement-noIndicator', !this.indicator );
+ }
this.updateThemeClasses();
return this;
* var button = new OO.ui.ButtonWidget( {
* label: 'Button with Icon',
* icon: 'trash',
- * iconTitle: 'Remove'
+ * title: 'Remove'
* } );
* $( 'body' ).append( button.$element );
*
* // An icon widget with a label
* var myIcon = new OO.ui.IconWidget( {
* icon: 'help',
- * iconTitle: 'Help'
+ * title: 'Help'
* } );
* // Create a label.
* var iconLabel = new OO.ui.LabelWidget( {
*
* MessageDialog.prototype.initialize = function () {
* MessageDialog.parent.prototype.initialize.apply( this, arguments );
- * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
+ * this.content = new OO.ui.PanelLayout( { padded: true } );
* this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.</p>' );
* this.$body.append( this.content.$element );
* };
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {number} [width=320] Width of popup in pixels
- * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
+ * @cfg {number|null} [width=320] Width of popup in pixels. Pass `null` to use automatic width.
+ * @cfg {number|null} [height=null] Height of popup in pixels. Pass `null` to use automatic height.
* @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
* @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer
* 'above': Put popup above $floatableContainer; anchor points down to the horizontal center
this.$container = config.$container;
this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
this.autoClose = !!config.autoClose;
- this.$autoCloseIgnore = config.$autoCloseIgnore;
this.transitionTimeout = null;
this.anchored = false;
- this.width = config.width !== undefined ? config.width : 320;
- this.height = config.height !== undefined ? config.height : null;
this.onMouseDownHandler = this.onMouseDown.bind( this );
this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
// Initialization
+ this.setSize( config.width, config.height );
this.toggleAnchor( config.anchor === undefined || config.anchor );
this.setAlignment( config.align || 'center' );
this.setPosition( config.position || 'below' );
this.setAutoFlip( config.autoFlip === undefined || config.autoFlip );
+ this.setAutoCloseIgnore( config.$autoCloseIgnore );
this.$body.addClass( 'oo-ui-popupWidget-body' );
this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
this.$popup
OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
// Capture clicks outside popup
this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
+ // We add 'click' event because iOS safari needs to respond to this event.
+ // We can't use 'touchstart' (as is usually the equivalent to 'mousedown') because
+ // then it will trigger when scrolling. While iOS Safari has some reported behavior
+ // of occasionally not emitting 'click' properly, that event seems to be the standard
+ // that it should be emitting, so we add it to this and will operate the event handler
+ // on whichever of these events was triggered first
+ this.getElementDocument().addEventListener( 'click', this.onMouseDownHandler, true );
};
/**
*/
OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
+ this.getElementDocument().removeEventListener( 'click', this.onMouseDownHandler, true );
};
/**
*
* Changing the size may also change the popup's position depending on the alignment.
*
- * @param {number} width Width in pixels
- * @param {number} height Height in pixels
+ * @param {number|null} [width=320] Width in pixels. Pass `null` to use automatic width.
+ * @param {number|null} [height=null] Height in pixels. Pass `null` to use automatic height.
* @param {boolean} [transition=false] Use a smooth transition
* @chainable
*/
OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
- this.width = width;
+ this.width = width !== undefined ? width : 320;
this.height = height !== undefined ? height : null;
if ( this.isVisible() ) {
this.updateDimensions( transition );
// 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,
+ width: this.width !== null ? this.width : 'auto',
height: this.height !== null ? this.height : 'auto'
} );
near = vertical ? 'top' : 'left';
far = vertical ? 'bottom' : 'right';
sizeProp = vertical ? 'Height' : 'Width';
- popupSize = vertical ? ( this.height || this.$popup.height() ) : this.width;
+ popupSize = vertical ? ( this.height || this.$popup.height() ) : ( this.width || this.$popup.width() );
this.setAnchorEdge( anchorEdgeMap[ popupPosition ] );
this.horizontalPosition = vertical ? popupPosition : hPosMap[ align ];
}
};
+/**
+ * Set which elements will not close the popup when clicked.
+ *
+ * For auto-closing popups, clicks on these elements will not cause the popup to auto-close.
+ *
+ * @param {jQuery} $autoCloseIgnore Elements to ignore for auto-closing
+ */
+OO.ui.PopupWidget.prototype.setAutoCloseIgnore = function ( $autoCloseIgnore ) {
+ this.$autoCloseIgnore = $autoCloseIgnore;
+};
+
/**
* Get an ID of the body element, this can be used as the
* `aria-describedby` attribute for an input field.
// Initialization
this.$element
- .addClass( 'oo-ui-popupButtonWidget' )
- .attr( 'aria-haspopup', 'true' );
+ .addClass( 'oo-ui-popupButtonWidget' );
this.popup.$element
.addClass( 'oo-ui-popupButtonWidget-popup' )
.toggleClass( 'oo-ui-popupButtonWidget-framed-popup', this.isFramed() )
OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
OO.ui.mixin.FloatableElement.call( this, config );
+ // Initial vertical positions other than 'center' will result in
+ // the menu being flipped if there is not enough space in the container.
+ // Store the original position so we know what to reset to.
+ this.originalVerticalPosition = this.verticalPosition;
+
// Properties
this.autoHide = config.autoHide === undefined || !!config.autoHide;
this.hideOnChoose = config.hideOnChoose === undefined || !!config.hideOnChoose;
* The menu is ready: it is visible and has been positioned and clipped.
*/
+/* Static properties */
+
+/**
+ * Positions to flip to if there isn't room in the container for the
+ * menu in a specific direction.
+ *
+ * @property {Object.<string,string>}
+ */
+OO.ui.MenuSelectWidget.static.flippedPositions = {
+ below: 'above',
+ above: 'below',
+ top: 'bottom',
+ bottom: 'top'
+};
+
/* Methods */
/**
* @protected
*/
OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
- var i, item, visible, section, sectionEmpty, filter, exactFilter,
- firstItemFound = false,
+ var i, item, items, visible, section, sectionEmpty, filter, exactFilter,
anyVisible = false,
len = this.items.length,
showAll = !this.isVisible(),
if ( this.$input && this.filterFromInput ) {
filter = showAll ? null : this.getItemMatcher( this.$input.val() );
exactFilter = this.getItemMatcher( this.$input.val(), true );
-
// Hide non-matching options, and also hide section headers if all options
// in their section are hidden.
for ( i = 0; i < len; i++ ) {
anyVisible = anyVisible || visible;
sectionEmpty = sectionEmpty && !visible;
item.toggle( visible );
- if ( this.highlightOnFilter && visible && !firstItemFound ) {
- // Highlight the first item in the list
- this.highlightItem( item );
- firstItemFound = true;
- }
}
}
// Process the final section
}
this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible );
+
+ if ( this.highlightOnFilter ) {
+ // Highlight the first item on the list
+ item = null;
+ items = this.getItems();
+ for ( i = 0; i < items.length; i++ ) {
+ if ( items[ i ].isVisible() ) {
+ item = items[ i ];
+ break;
+ }
+ }
+ this.highlightItem( item );
+ }
+
}
// Reevaluate clipping
* @inheritdoc
*/
OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
- var change, belowHeight, aboveHeight;
+ var change, originalHeight, flippedHeight;
visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
change = visible !== this.isVisible();
if ( change && visible ) {
// Reset position before showing the popup again. It's possible we no longer need to flip
// (e.g. if the user scrolled).
- this.setVerticalPosition( 'below' );
+ this.setVerticalPosition( this.originalVerticalPosition );
}
// Parent method
this.bindKeyDownListener();
this.bindKeyPressListener();
- if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
- // If opening the menu downwards causes it to be clipped, flip it to open upwards instead
- belowHeight = this.$element.height();
- this.setVerticalPosition( 'above' );
+ if (
+ ( this.isClippedVertically() || this.isFloatableOutOfView() ) &&
+ this.originalVerticalPosition !== 'center'
+ ) {
+ // If opening the menu in one direction causes it to be clipped, flip it
+ originalHeight = this.$element.height();
+ this.setVerticalPosition(
+ this.constructor.static.flippedPositions[ this.originalVerticalPosition ]
+ );
if ( this.isClippedVertically() || this.isFloatableOutOfView() ) {
- // If opening upwards also causes it to be clipped, flip it to open in whichever direction
+ // If flipping also causes it to be clipped, open in whichever direction
// we have more space
- aboveHeight = this.$element.height();
- if ( aboveHeight < belowHeight ) {
- this.setVerticalPosition( 'below' );
+ flippedHeight = this.$element.height();
+ if ( originalHeight > flippedHeight ) {
+ this.setVerticalPosition( this.originalVerticalPosition );
}
}
}
if ( this.autosize ) {
this.$clone = this.$input
.clone()
+ .removeAttr( 'id' )
+ .removeAttr( 'name' )
.insertAfter( this.$input )
.attr( 'aria-hidden', 'true' )
.addClass( 'oo-ui-element-hidden' );
* - **inline**: The label is placed after the field-widget and aligned to the left.
* An inline-alignment is best used with checkboxes or radio buttons.
*
- * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
+ * Help text can either be:
+ *
+ * - accessed via a help icon that appears in the upper right corner of the rendered field layout, or
+ * - shown as a subtle explanation below the label.
+ *
+ * If the help text is brief, or is essential to always espose it, set `helpInline` to `true`. If it
+ * is long or not essential, leave `helpInline` to its default, `false`.
+ *
* Please see the [OOUI documentation on MediaWiki] [1] for examples and more information.
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Layouts/Fields_and_Fieldsets
* @constructor
* @param {OO.ui.Widget} fieldWidget Field widget
* @param {Object} [config] Configuration options
- * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
- * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
+ * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top'
+ * or 'inline'
+ * @cfg {Array} [errors] Error messages about the widget, which will be
+ * displayed below the widget.
* The array may contain strings or OO.ui.HtmlSnippet instances.
- * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
+ * @cfg {Array} [notices] Notices about the widget, which will be displayed
+ * below the widget.
* The array may contain strings or OO.ui.HtmlSnippet instances.
- * @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.
+ * These are more visible than `help` messages when `helpInline` is set, and so
+ * might be good for transient messages.
+ * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified
+ * and `helpInline` is `false`, a "help" icon will appear in the upper-right
+ * corner of the rendered field; clicking it will display the text in a popup.
+ * If `helpInline` is `true`, then a subtle description will be shown after the
+ * label.
+ * @cfg {boolean} [helpInline=false] Whether or not the help should be inline,
+ * or shown when the "help" icon is clicked.
+ * @cfg {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup, if
+ * `help` is given.
* See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
*
* @throws {Error} An error is thrown if no widget is specified
}
// Configuration initialization
- config = $.extend( { align: 'left' }, config );
+ config = $.extend( { align: 'left', helpInline: false }, config );
// Parent constructor
OO.ui.FieldLayout.parent.call( this, config );
this.$header = $( '<span>' );
this.$body = $( '<div>' );
this.align = null;
- if ( config.help ) {
- this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
- $overlay: config.$overlay,
- popup: {
- padded: true
- },
- classes: [ 'oo-ui-fieldLayout-help' ],
- framed: false,
- icon: 'info',
- label: OO.ui.msg( 'ooui-field-help' )
- } );
- if ( config.help instanceof OO.ui.HtmlSnippet ) {
- this.popupButtonWidget.getPopup().$body.html( config.help.toString() );
- } else {
- this.popupButtonWidget.getPopup().$body.text( config.help );
- }
- this.$help = this.popupButtonWidget.$element;
- } else {
- this.$help = $( [] );
- }
+ this.helpInline = config.helpInline;
// Events
this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
// Initialization
- if ( config.help ) {
- // Set the 'aria-describedby' attribute on the fieldWidget
- // Preference given to an input or a button
- (
- this.fieldWidget.$input ||
- this.fieldWidget.$button ||
- this.fieldWidget.$element
- ).attr(
- 'aria-describedby',
- this.popupButtonWidget.getPopup().getBodyId()
- );
- }
+ this.$help = config.help ?
+ this.createHelpElement( config.help, config.$overlay ) :
+ $( [] );
if ( this.fieldWidget.getInputId() ) {
this.$label.attr( 'for', this.fieldWidget.getInputId() );
+ if ( this.helpInline ) {
+ this.$help.attr( 'for', this.fieldWidget.getInputId() );
+ }
} else {
this.$label.on( 'click', function () {
this.fieldWidget.simulateLabelClick();
}.bind( this ) );
+ if ( this.helpInline ) {
+ this.$help.on( 'click', function () {
+ this.fieldWidget.simulateLabelClick();
+ }.bind( this ) );
+ }
}
this.$element
.addClass( 'oo-ui-fieldLayout' )
value = 'top';
}
// Reorder elements
- if ( value === 'top' ) {
- this.$header.append( this.$help, this.$label );
- this.$body.append( this.$header, this.$field );
- } else if ( value === 'inline' ) {
- this.$header.append( this.$help, this.$label );
- this.$body.append( this.$field, this.$header );
+
+ if ( this.helpInline ) {
+ if ( value === 'inline' ) {
+ this.$header.append( this.$label, this.$help );
+ this.$body.append( this.$field, this.$header );
+ } else {
+ this.$header.append( this.$label, this.$help );
+ this.$body.append( this.$header, this.$field );
+ }
} else {
- this.$header.append( this.$label );
- this.$body.append( this.$header, this.$help, this.$field );
+ if ( value === 'top' ) {
+ this.$header.append( this.$help, this.$label );
+ this.$body.append( this.$header, this.$field );
+ } else if ( value === 'inline' ) {
+ this.$header.append( this.$help, this.$label );
+ this.$body.append( this.$field, this.$header );
+ } else {
+ 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
return title;
};
+/**
+ * Creates and returns the help element. Also sets the `aria-describedby`
+ * attribute on the main element of the `fieldWidget`.
+ *
+ * @private
+ * @param {string|OO.ui.HtmlSnippet} [help] Help text.
+ * @param {jQuery} [$overlay] Passed to OO.ui.PopupButtonWidget for help popup.
+ * @return {jQuery} The element that should become `this.$help`.
+ */
+OO.ui.FieldLayout.prototype.createHelpElement = function ( help, $overlay ) {
+ var helpId, helpWidget;
+
+ if ( this.helpInline ) {
+ helpWidget = new OO.ui.LabelWidget( {
+ label: help,
+ classes: [ 'oo-ui-inline-help' ]
+ } );
+
+ helpId = helpWidget.getElementId();
+ } else {
+ helpWidget = new OO.ui.PopupButtonWidget( {
+ $overlay: $overlay,
+ popup: {
+ padded: true
+ },
+ classes: [ 'oo-ui-fieldLayout-help' ],
+ framed: false,
+ icon: 'info',
+ label: OO.ui.msg( 'ooui-field-help' )
+ } );
+ if ( help instanceof OO.ui.HtmlSnippet ) {
+ helpWidget.getPopup().$body.html( help.toString() );
+ } else {
+ helpWidget.getPopup().$body.text( help );
+ }
+
+ helpId = helpWidget.getPopup().getBodyId();
+ }
+
+ // Set the 'aria-describedby' attribute on the fieldWidget
+ // Preference given to an input or a button
+ (
+ this.fieldWidget.$input ||
+ this.fieldWidget.$button ||
+ this.fieldWidget.$element
+ ).attr( 'aria-describedby', helpId );
+
+ return helpWidget.$element;
+};
+
/**
* ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
* and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),