/*!
- * OOUI v0.31.2
+ * OOUI v0.31.6
* https://www.mediawiki.org/wiki/OOUI
*
* Copyright 2011–2019 OOUI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2019-03-26T23:00:40Z
+ * Date: 2019-05-08T10:08:36Z
*/
( function ( OO ) {
return OO.ui.Element.static.infuse( idOrNode, config );
};
-( function () {
- /**
- * Message store for the default implementation of OO.ui.msg.
- *
- * Environments that provide a localization system should not use this, but should override
- * OO.ui.msg altogether.
- *
- * @private
- */
- var messages = {
- // Tool tip for a button that moves items in a list down one place
- 'ooui-outline-control-move-down': 'Move item down',
- // Tool tip for a button that moves items in a list up one place
- 'ooui-outline-control-move-up': 'Move item up',
- // Tool tip for a button that removes items from a list
- 'ooui-outline-control-remove': 'Remove item',
- // Label for the toolbar group that contains a list of all other available tools
- 'ooui-toolbar-more': 'More',
- // Label for the fake tool that expands the full list of tools in a toolbar group
- 'ooui-toolgroup-expand': 'More',
- // Label for the fake tool that collapses the full list of tools in a toolbar group
- 'ooui-toolgroup-collapse': 'Fewer',
- // Default label for the tooltip for the button that removes a tag item
- 'ooui-item-remove': 'Remove',
- // Default label for the accept button of a confirmation dialog
- 'ooui-dialog-message-accept': 'OK',
- // Default label for the reject button of a confirmation dialog
- 'ooui-dialog-message-reject': 'Cancel',
- // Title for process dialog error description
- 'ooui-dialog-process-error': 'Something went wrong',
- // Label for process dialog dismiss error button, visible when describing errors
- 'ooui-dialog-process-dismiss': 'Dismiss',
- // Label for process dialog retry action button, visible when describing only recoverable
- // errors
- 'ooui-dialog-process-retry': 'Try again',
- // Label for process dialog retry action button, visible when describing only warnings
- 'ooui-dialog-process-continue': 'Continue',
- // Label for button in combobox input that triggers its dropdown
- 'ooui-combobox-button-label': 'Dropdown for combobox',
- // Label for the file selection widget's select file button
- 'ooui-selectfile-button-select': 'Select a file',
- // Label for the file selection widget if file selection is not supported
- 'ooui-selectfile-not-supported': 'File selection is not supported',
- // Label for the file selection widget when no file is currently selected
- 'ooui-selectfile-placeholder': 'No file is selected',
- // Label for the file selection widget's drop target
- 'ooui-selectfile-dragdrop-placeholder': 'Drop file here',
- // Label for the help icon attached to a form field
- 'ooui-field-help': 'Help'
- };
-
- /**
- * Get a localized message.
- *
- * 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.
- * // OOUI 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'
- * } );
- * $( document.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'
- * } );
- * $( document.body ).append( button.$element );
- * } );
- *
- * @param {string} key Message key
- * @param {...Mixed} [params] Message parameters
- * @return {string} Translated message with parameters substituted
- */
- OO.ui.msg = function ( key ) {
- var message = messages[ key ],
- params = Array.prototype.slice.call( arguments, 1 );
- if ( typeof message === 'string' ) {
- // Perform $1 substitution
- message = message.replace( /\$(\d+)/g, function ( unused, n ) {
- var i = parseInt( n, 10 );
- return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
- } );
- } else {
- // Return placeholder if message not found
- message = '[' + key + ']';
- }
- return message;
- };
-}() );
+/**
+ * Get a localized message.
+ *
+ * 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.
+ * // OOUI 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'
+ * } );
+ * $( document.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'
+ * } );
+ * $( document.body ).append( button.$element );
+ * } );
+ *
+ * @param {string} key Message key
+ * @param {...Mixed} [params] Message parameters
+ * @return {string} Translated message with parameters substituted
+ */
+OO.ui.msg = function ( key ) {
+ // `OO.ui.msg.messages` is defined in code generated during the build process
+ var messages = OO.ui.msg.messages,
+ message = messages[ key ],
+ params = Array.prototype.slice.call( arguments, 1 );
+ if ( typeof message === 'string' ) {
+ // Perform $1 substitution
+ message = message.replace( /\$(\d+)/g, function ( unused, n ) {
+ var i = parseInt( n, 10 );
+ return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
+ } );
+ } else {
+ // Return placeholder if message not found
+ message = '[' + key + ']';
+ }
+ return message;
+};
/**
* Package a message and arguments for deferred resolution.
return OO.ui.$defaultOverlay;
};
+/**
+ * Message store for the default implementation of OO.ui.msg.
+ *
+ * Environments that provide a localization system should not use this, but should override
+ * OO.ui.msg altogether.
+ *
+ * @private
+ */
+OO.ui.msg.messages = {
+ "ooui-outline-control-move-down": "Move item down",
+ "ooui-outline-control-move-up": "Move item up",
+ "ooui-outline-control-remove": "Remove item",
+ "ooui-toolbar-more": "More",
+ "ooui-toolgroup-expand": "More",
+ "ooui-toolgroup-collapse": "Fewer",
+ "ooui-item-remove": "Remove",
+ "ooui-dialog-message-accept": "OK",
+ "ooui-dialog-message-reject": "Cancel",
+ "ooui-dialog-process-error": "Something went wrong",
+ "ooui-dialog-process-dismiss": "Dismiss",
+ "ooui-dialog-process-retry": "Try again",
+ "ooui-dialog-process-continue": "Continue",
+ "ooui-combobox-button-label": "Dropdown for combobox",
+ "ooui-selectfile-button-select": "Select a file",
+ "ooui-selectfile-not-supported": "File selection is not supported",
+ "ooui-selectfile-placeholder": "No file is selected",
+ "ooui-selectfile-dragdrop-placeholder": "Drop file here",
+ "ooui-field-help": "Help"
+};
+
/*!
* Mixin namespace.
*/
* Scroll element into view.
*
* @static
- * @param {HTMLElement} el Element to scroll into view
+ * @param {HTMLElement|Object} elOrPosition Element to scroll into view
* @param {Object} [config] Configuration options
+ * @param {string} [config.animate=true] Animate to the new scroll offset.
* @param {string} [config.duration='fast'] jQuery animation duration value
* @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
* to scroll in both directions
+ * @param {Object} [config.padding] Additional padding on the container to scroll past.
+ * Object containing any of 'top', 'bottom', 'left', or 'right' as numbers.
+ * @param {Object} [config.scrollContainer] Scroll container. Defaults to
+ * getClosestScrollableContainer of the element.
* @return {jQuery.Promise} Promise which resolves when the scroll is complete
*/
-OO.ui.Element.static.scrollIntoView = function ( el, config ) {
- var position, animations, container, $container, elementDimensions, containerDimensions,
- $window,
+OO.ui.Element.static.scrollIntoView = function ( elOrPosition, config ) {
+ var position, animations, container, $container, elementPosition, containerDimensions,
+ $window, padding, animate, method,
deferred = $.Deferred();
// Configuration initialization
config = config || {};
+ padding = $.extend( {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0
+ }, config.padding );
+
+ animate = config.animate !== false;
+
animations = {};
- container = this.getClosestScrollableContainer( el, config.direction );
+ elementPosition = elOrPosition instanceof HTMLElement ?
+ this.getDimensions( elOrPosition ).rect :
+ elOrPosition;
+ container = config.scrollContainer || (
+ elOrPosition instanceof HTMLElement ?
+ this.getClosestScrollableContainer( elOrPosition, config.direction ) :
+ // No scrollContainer or element
+ this.getClosestScrollableContainer( document.body )
+ );
$container = $( container );
- elementDimensions = this.getDimensions( el );
containerDimensions = this.getDimensions( container );
- $window = $( this.getWindow( el ) );
+ $window = $( this.getWindow( container ) );
// Compute the element's position relative to the container
if ( $container.is( 'html, body' ) ) {
// If the scrollable container is the root, this is easy
position = {
- top: elementDimensions.rect.top,
- bottom: $window.innerHeight() - elementDimensions.rect.bottom,
- left: elementDimensions.rect.left,
- right: $window.innerWidth() - elementDimensions.rect.right
+ top: elementPosition.top,
+ bottom: $window.innerHeight() - elementPosition.bottom,
+ left: elementPosition.left,
+ right: $window.innerWidth() - elementPosition.right
};
} else {
// Otherwise, we have to subtract el's coordinates from container's coordinates
position = {
- top: elementDimensions.rect.top -
+ top: elementPosition.top -
( containerDimensions.rect.top + containerDimensions.borders.top ),
bottom: containerDimensions.rect.bottom - containerDimensions.borders.bottom -
- containerDimensions.scrollbar.bottom - elementDimensions.rect.bottom,
- left: elementDimensions.rect.left -
+ containerDimensions.scrollbar.bottom - elementPosition.bottom,
+ left: elementPosition.left -
( containerDimensions.rect.left + containerDimensions.borders.left ),
right: containerDimensions.rect.right - containerDimensions.borders.right -
- containerDimensions.scrollbar.right - elementDimensions.rect.right
+ containerDimensions.scrollbar.right - elementPosition.right
};
}
if ( !config.direction || config.direction === 'y' ) {
- if ( position.top < 0 ) {
- animations.scrollTop = containerDimensions.scroll.top + position.top;
- } else if ( position.top > 0 && position.bottom < 0 ) {
+ if ( position.top < padding.top ) {
+ animations.scrollTop = containerDimensions.scroll.top + position.top - padding.top;
+ } else if ( position.bottom < padding.bottom ) {
animations.scrollTop = containerDimensions.scroll.top +
- Math.min( position.top, -position.bottom );
+ // Scroll the bottom into view, but not at the expense
+ // of scrolling the top out of view
+ Math.min( position.top - padding.top, -position.bottom + padding.bottom );
}
}
if ( !config.direction || config.direction === 'x' ) {
- if ( position.left < 0 ) {
- animations.scrollLeft = containerDimensions.scroll.left + position.left;
- } else if ( position.left > 0 && position.right < 0 ) {
+ if ( position.left < padding.left ) {
+ animations.scrollLeft = containerDimensions.scroll.left + position.left - padding.left;
+ } else if ( position.right < padding.right ) {
animations.scrollLeft = containerDimensions.scroll.left +
- Math.min( position.left, -position.right );
+ // Scroll the right into view, but not at the expense
+ // of scrolling the left out of view
+ Math.min( position.left - padding.left, -position.right + padding.right );
}
}
if ( !$.isEmptyObject( animations ) ) {
- // eslint-disable-next-line no-jquery/no-animate
- $container.stop( true ).animate( animations, config.duration === undefined ?
- 'fast' : config.duration );
- $container.queue( function ( next ) {
+ if ( animate ) {
+ // eslint-disable-next-line no-jquery/no-animate
+ $container.stop( true ).animate( animations, config.duration === undefined ? 'fast' : config.duration );
+ $container.queue( function ( next ) {
+ deferred.resolve();
+ next();
+ } );
+ } else {
+ $container.stop( true );
+ for ( method in animations ) {
+ $container[ method ]( animations[ method ] );
+ }
deferred.resolve();
- next();
- } );
+ }
} else {
deferred.resolve();
}
*
* @protected
* @param {jQuery.Event} e Mouse down event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
* @protected
* @param {jQuery.Event} e Mouse click event
* @fires click
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
* @protected
* @param {jQuery.Event} e Key press event
* @fires click
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
this.$flagged = null;
// Initialization
- this.setFlags( config.flags );
+ this.setFlags( config.flags || this.constructor.static.flags );
this.setFlaggedElement( config.$flagged || this.$element );
};
+/* Setup */
+
+OO.initClass( OO.ui.mixin.FlaggedElement );
+
/* Events */
/**
* that the flag was added, `false` that the flag was removed.
*/
+/* Static Properties */
+
+/**
+ * Initial value to pass to setFlags if no value is provided in config.
+ *
+ * @static
+ * @inheritable
+ * @property {string|string[]|Object.<string, boolean>}
+ */
+OO.ui.mixin.FlaggedElement.static.flags = null;
+
/* Methods */
/**
* The title text, a function that returns text, or `null` for no title. The value of the static
* property is overridden if the #title config option is used.
*
+ * If the element has a default title (e.g. `<input type=file>`), `null` will allow that title to be
+ * shown. Use empty string to suppress it.
+ *
* @static
* @inheritable
* @property {string|Function|null}
}
this.$titled = $titled;
- if ( this.title ) {
- this.updateTitle();
- }
+ this.updateTitle();
};
/**
*/
OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
- title = ( typeof title === 'string' && title.length ) ? title : null;
+ title = typeof title === 'string' ? title : null;
if ( this.title !== title ) {
this.title = title;
*
* @private
* @param {jQuery.Event} e Mouse down event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
var item;
*
* @private
* @param {MouseEvent} e Mouse up event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.SelectWidget.prototype.onDocumentMouseUp = function ( e ) {
var item;
*
* @private
* @param {jQuery.Event} e Mouse over event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
var item;
*
* @private
* @param {jQuery.Event} e Mouse over event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.SelectWidget.prototype.onMouseLeave = function () {
if ( !this.isDisabled() ) {
case OO.ui.Keys.UP:
case OO.ui.Keys.LEFT:
this.clearKeyPressBuffer();
- nextItem = currentItem ? this.findRelativeSelectableItem( currentItem, -1 ) : firstItem;
+ nextItem = currentItem ?
+ this.findRelativeSelectableItem( currentItem, -1 ) : firstItem;
handled = true;
break;
case OO.ui.Keys.DOWN:
case OO.ui.Keys.RIGHT:
this.clearKeyPressBuffer();
- nextItem = currentItem ? this.findRelativeSelectableItem( currentItem, 1 ) : firstItem;
+ nextItem = currentItem ?
+ this.findRelativeSelectableItem( currentItem, 1 ) : firstItem;
handled = true;
break;
case OO.ui.Keys.ESCAPE:
*
* @protected
* @param {KeyboardEvent} e Key press event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.SelectWidget.prototype.onDocumentKeyPress = function ( e ) {
var c, filter, item;
selectedItem = this.findSelectedItem();
if ( !this.multiselect && selectedItem ) {
// TODO: Verify if this is even needed; This is already done on highlight changes
- // in SelectWidget#highlightItem, so we should just need to highlight the item we need to
- // highlight here and not bother with attr or checking selections.
+ // in SelectWidget#highlightItem, so we should just need to highlight the item
+ // we need to highlight here and not bother with attr or checking selections.
this.$focusOwner.attr( 'aria-activedescendant', selectedItem.getElementId() );
selectedItem.scrollElementIntoView( { duration: 0 } );
}
this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
this.togglePositioning( false );
this.toggleClipping( false );
+ this.lastHighlightedItem = null;
}
}
*
* @private
* @param {jQuery.Event} e Mouse click event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
*
* @private
* @param {jQuery.Event} e Key down event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.DropdownWidget.prototype.onKeyDown = function ( e ) {
if (
*
* @private
* @param {jQuery.Event} e Mouse down event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
if ( e.which === OO.ui.MouseButtons.LEFT ) {
*
* @private
* @param {jQuery.Event} e Mouse down event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
if ( e.which === OO.ui.MouseButtons.LEFT ) {
var $listItem, $icon, message;
$listItem = $( '<li>' );
if ( kind === 'error' ) {
- $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'error' ] } ).$element;
+ $icon = new OO.ui.IconWidget( { icon: 'error', flags: [ 'error' ] } ).$element;
$listItem.attr( 'role', 'alert' );
} else if ( kind === 'warning' ) {
$icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
*
* @private
* @param {jQuery.Event} event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
var delta = 0;
- if ( !this.isDisabled() && this.$input.is( ':focus' ) ) {
+ if ( this.isDisabled() || this.isReadOnly() ) {
+ return;
+ }
+
+ if ( this.$input.is( ':focus' ) ) {
// Standard 'wheel' event
if ( event.originalEvent.deltaMode !== undefined ) {
this.sawWheelEvent = true;
*
* @private
* @param {jQuery.Event} e Key down event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
- if ( !this.isDisabled() ) {
- switch ( e.which ) {
- case OO.ui.Keys.UP:
- this.adjustValue( this.buttonStep );
- return false;
- case OO.ui.Keys.DOWN:
- this.adjustValue( -this.buttonStep );
- return false;
- case OO.ui.Keys.PAGEUP:
- this.adjustValue( this.pageStep );
- return false;
- case OO.ui.Keys.PAGEDOWN:
- this.adjustValue( -this.pageStep );
- return false;
- }
+ if ( this.isDisabled() || this.isReadOnly() ) {
+ return;
+ }
+
+ switch ( e.which ) {
+ case OO.ui.Keys.UP:
+ this.adjustValue( this.buttonStep );
+ return false;
+ case OO.ui.Keys.DOWN:
+ this.adjustValue( -this.buttonStep );
+ return false;
+ case OO.ui.Keys.PAGEUP:
+ this.adjustValue( this.pageStep );
+ return false;
+ case OO.ui.Keys.PAGEDOWN:
+ this.adjustValue( -this.pageStep );
+ return false;
}
};
/**
- * @inheritdoc
+ * Update the disabled state of the controls
+ *
+ * @chainable
+ * @protected
+ * @return {OO.ui.NumberInputWidget} The widget, for chaining
*/
-OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
- // Parent method
- OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
-
+OO.ui.NumberInputWidget.prototype.updateControlsDisabled = function () {
+ var disabled = this.isDisabled() || this.isReadOnly();
if ( this.minusButton ) {
- this.minusButton.setDisabled( this.isDisabled() );
+ this.minusButton.setDisabled( disabled );
}
if ( this.plusButton ) {
- this.plusButton.setDisabled( this.isDisabled() );
+ this.plusButton.setDisabled( disabled );
}
+ return this;
+};
+/**
+ * @inheritdoc
+ */
+OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
+ // Parent method
+ OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
+ this.updateControlsDisabled();
+ return this;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.NumberInputWidget.prototype.setReadOnly = function () {
+ // Parent method
+ OO.ui.NumberInputWidget.parent.prototype.setReadOnly.apply( this, arguments );
+ this.updateControlsDisabled();
return this;
};
* @constructor
* @param {Object} [config] Configuration options
* @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
+ * @cfg {boolean} [multiple=false] Allow multiple files to be selected.
* @cfg {string} [placeholder] Text to display when no file is selected.
* @cfg {Object} [button] Config to pass to select file button.
* @cfg {string} [icon] Icon to show next to file info
*/
OO.ui.SelectFileInputWidget = function OoUiSelectFileInputWidget( config ) {
+ var widget = this;
+
config = config || {};
// Construct buttons before parent method is called (calling setDisabled)
OO.ui.SelectFileInputWidget.parent.call( this, config );
// Properties
- this.currentFile = null;
+ this.currentFiles = this.filterFiles( this.$input[ 0 ].files || [] );
if ( Array.isArray( config.accept ) ) {
this.accept = config.accept;
} else {
this.accept = null;
}
- this.onFileSelectedHandler = this.onFileSelected.bind( this );
+ this.multiple = !!config.multiple;
// Events
this.info.connect( this, { change: 'onInfoChange' } );
this.selectButton.$button.on( {
keypress: this.onKeyPress.bind( this )
} );
+ this.$input.on( {
+ change: this.onFileSelected.bind( this ),
+ // Support: IE11
+ // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
+ // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
+ // Since this messes with our custom styling (the file input has large dimensions and this
+ // causes the label to scroll out of view), scroll the button back to top. (T192131)
+ focus: function () {
+ widget.$input.parent().prop( 'scrollTop', 0 );
+ }
+ } );
this.connect( this, { change: 'updateUI' } );
- // Initialization
- this.setupInput();
-
this.fieldLayout = new OO.ui.ActionFieldLayout( this.info, this.selectButton, { align: 'top' } );
+ this.$input
+ .attr( {
+ type: 'file',
+ // this.selectButton is tabindexed
+ tabindex: -1,
+ // Infused input may have previously by
+ // TabIndexed, so remove aria-disabled attr.
+ 'aria-disabled': null
+ } );
+
+ if ( this.accept ) {
+ this.$input.attr( 'accept', this.accept.join( ', ' ) );
+ }
+ if ( this.multiple ) {
+ this.$input.attr( 'multiple', '' );
+ }
+ this.selectButton.$button.append( this.$input );
+
this.$element
.addClass( 'oo-ui-selectFileInputWidget' )
.append( this.fieldLayout.$element );
OO.inheritClass( OO.ui.SelectFileInputWidget, OO.ui.InputWidget );
+/* Static properties */
+
+// Set empty title so that browser default tooltips like "No file chosen" don't appear.
+// On SelectFileWidget this tooltip will often be incorrect, so create a consistent
+// experience on SelectFileInputWidget.
+OO.ui.SelectFileInputWidget.static.title = '';
+
/* Methods */
/**
* @return {string} Filename
*/
OO.ui.SelectFileInputWidget.prototype.getFilename = function () {
- if ( this.currentFile ) {
- return this.currentFile.name;
+ if ( this.currentFiles.length ) {
+ return this.currentFiles.map( function ( file ) {
+ return file.name;
+ } ).join( ', ' );
} else {
// Try to strip leading fakepath.
return this.getValue().split( '\\' ).pop();
this.emit( 'change', this.value );
}
} else {
- this.currentFile = null;
+ this.currentFiles = [];
// Parent method
OO.ui.SelectFileInputWidget.super.prototype.setValue.call( this, '' );
}
* @param {jQuery.Event} e
*/
OO.ui.SelectFileInputWidget.prototype.onFileSelected = function ( e ) {
- var file = OO.getProp( e.target, 'files', 0 ) || null;
-
- if ( file && !this.isAllowedType( file.type ) ) {
- file = null;
- }
-
- this.currentFile = file;
+ this.currentFiles = this.filterFiles( e.target.files || [] );
};
/**
this.info.setValue( this.getFilename() );
};
-/**
- * Setup the input element.
- *
- * @protected
- */
-OO.ui.SelectFileInputWidget.prototype.setupInput = function () {
- var widget = this;
- this.$input
- .attr( {
- type: 'file',
- // this.selectButton is tabindexed
- tabindex: -1,
- // Infused input may have previously by
- // TabIndexed, so remove aria-disabled attr.
- 'aria-disabled': null
- } )
- .on( 'change', this.onFileSelectedHandler )
- // Support: IE11
- // In IE 11, focussing a file input (by clicking on it) displays a text cursor and scrolls
- // the cursor into view (in this case, it scrolls the button, which has 'overflow: hidden').
- // Since this messes with our custom styling (the file input has large dimensions and this
- // causes the label to scroll out of view), scroll the button back to top. (T192131)
- .on( 'focus', function () {
- widget.$input.parent().prop( 'scrollTop', 0 );
- } );
-
- if ( this.accept ) {
- this.$input.attr( 'accept', this.accept.join( ', ' ) );
- }
- this.selectButton.$button.append( this.$input );
-};
-
/**
* Determine if we should accept this file.
*
* @private
- * @param {string} mimeType File MIME type
- * @return {boolean}
+ * @param {FileList|File[]} files Files to filter
+ * @return {File[]} Filter files
*/
-OO.ui.SelectFileInputWidget.prototype.isAllowedType = function ( mimeType ) {
- var i, mimeTest;
+OO.ui.SelectFileInputWidget.prototype.filterFiles = function ( files ) {
+ var accept = this.accept;
- if ( !this.accept || !mimeType ) {
- return true;
- }
+ function mimeAllowed( file ) {
+ var i, mimeTest,
+ mimeType = file.type;
- for ( i = 0; i < this.accept.length; i++ ) {
- mimeTest = this.accept[ i ];
- if ( mimeTest === mimeType ) {
+ if ( !accept || !mimeType ) {
return true;
- } else if ( mimeTest.substr( -2 ) === '/*' ) {
- mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
- if ( mimeType.substr( 0, mimeTest.length ) === mimeTest ) {
+ }
+
+ for ( i = 0; i < accept.length; i++ ) {
+ mimeTest = accept[ i ];
+ if ( mimeTest === mimeType ) {
return true;
+ } else if ( mimeTest.substr( -2 ) === '/*' ) {
+ mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
+ if ( mimeType.substr( 0, mimeTest.length ) === mimeTest ) {
+ return true;
+ }
}
}
+ return false;
}
- return false;
+ return Array.prototype.filter.call( files, mimeAllowed );
};
/**
*
* @private
* @param {jQuery.Event} e Key press event
- * @return {undefined/boolean} False to prevent default if event is handled
+ * @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.SelectFileInputWidget.prototype.onKeyPress = function ( e ) {
if ( !this.isDisabled() && this.$input &&