X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=resources%2Flib%2Fooui%2Foojs-ui-core.js;h=4eea3bdfa8ca6b2bd06b98d0f5abfd97e5481f1c;hb=8414e3c4d0c2a647f794e1e535e70f3a2abe6eb4;hp=3ca6632b7907fc95c4b8553e6c084ac9d81745c5;hpb=8d9bc43448b34b3d0e465fc893df94301e75f4e9;p=lhc%2Fweb%2Fwiklou.git diff --git a/resources/lib/ooui/oojs-ui-core.js b/resources/lib/ooui/oojs-ui-core.js index 3ca6632b79..4eea3bdfa8 100644 --- a/resources/lib/ooui/oojs-ui-core.js +++ b/resources/lib/ooui/oojs-ui-core.js @@ -1,12 +1,12 @@ /*! - * OOUI v0.31.0 + * OOUI v0.31.4 * 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-20T23:07:02Z + * Date: 2019-04-16T23:14:51Z */ ( function ( OO ) { @@ -295,7 +295,7 @@ OO.ui.throttle = function ( func, wait ) { previous = 0, run = function () { timeout = null; - previous = OO.ui.now(); + previous = Date.now(); func.apply( context, args ); }; return function () { @@ -304,7 +304,7 @@ OO.ui.throttle = function ( func, wait ) { // period. If it's less, run the function immediately. If it's more, // set a timeout for the remaining time -- but don't replace an // existing timeout, since that'd indefinitely prolong the wait. - var remaining = wait - ( OO.ui.now() - previous ); + var remaining = wait - ( Date.now() - previous ); context = this; args = arguments; if ( remaining <= 0 ) { @@ -323,10 +323,12 @@ OO.ui.throttle = function ( func, wait ) { /** * A (possibly faster) way to get the current timestamp as an integer. * + * @deprecated Since 0.31.1; use `Date.now()` instead. * @return {number} Current timestamp, in milliseconds since the Unix epoch */ -OO.ui.now = Date.now || function () { - return new Date().getTime(); +OO.ui.now = function () { + OO.ui.warnDeprecation( 'OO.ui.now() is deprecated, use Date.now() instead' ); + return Date.now(); }; /** @@ -345,124 +347,74 @@ OO.ui.infuse = function ( idOrNode, config ) { 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. @@ -581,6 +533,36 @@ OO.ui.getDefaultOverlay = function () { 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. */ @@ -1301,75 +1283,107 @@ OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) * 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(); } @@ -2118,7 +2132,7 @@ OO.ui.mixin.TabIndexedElement.prototype.getInputId = function () { OO.ui.mixin.TabIndexedElement.prototype.isLabelableNode = function ( $node ) { var labelableTags = [ 'button', 'meter', 'output', 'progress', 'select', 'textarea' ], - tagName = $node.prop( 'tagName' ).toLowerCase(); + tagName = ( $node.prop( 'tagName' ) || '' ).toLowerCase(); if ( tagName === 'input' && $node.attr( 'type' ) !== 'hidden' ) { return true; @@ -3250,10 +3264,14 @@ OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) { 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 */ /** @@ -3266,6 +3284,17 @@ OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) { * 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.} + */ +OO.ui.mixin.FlaggedElement.static.flags = null; + /* Methods */ /** @@ -3456,6 +3485,9 @@ OO.initClass( OO.ui.mixin.TitledElement ); * 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. ``), `null` will allow that title to be + * shown. Use empty string to suppress it. + * * @static * @inheritable * @property {string|Function|null} @@ -3480,9 +3512,7 @@ OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) { } this.$titled = $titled; - if ( this.title ) { - this.updateTitle(); - } + this.updateTitle(); }; /** @@ -3495,7 +3525,7 @@ OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) { */ 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; @@ -6420,6 +6450,7 @@ OO.ui.OptionWidget.prototype.getMatchText = function () { * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See * the [OOUI documentation on MediaWiki] [2] for examples. * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Selects_and_Options + * @cfg {boolean} [multiselect] Allow for multiple selections */ OO.ui.SelectWidget = function OoUiSelectWidget( config ) { // Configuration initialization @@ -6436,6 +6467,7 @@ OO.ui.SelectWidget = function OoUiSelectWidget( config ) { // Properties this.pressed = false; this.selecting = null; + this.multiselect = !!config.multiselect; this.onDocumentMouseUpHandler = this.onDocumentMouseUp.bind( this ); this.onDocumentMouseMoveHandler = this.onDocumentMouseMove.bind( this ); this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this ); @@ -6496,13 +6528,16 @@ OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget ); * A `select` event is emitted when the selection is modified programmatically with the #selectItem * method. * - * @param {OO.ui.OptionWidget|null} item Selected item + * @param {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} items Currently selected items */ /** * @event choose + * * A `choose` event is emitted when an item is chosen with the #chooseItem method. + * * @param {OO.ui.OptionWidget} item Chosen item + * @param {boolean} selected Item is selected */ /** @@ -6699,12 +6734,13 @@ OO.ui.SelectWidget.prototype.onMouseLeave = function () { OO.ui.SelectWidget.prototype.onDocumentKeyDown = function ( e ) { var nextItem, handled = false, - currentItem = this.findHighlightedItem() || this.findSelectedItem(); + currentItem = this.findHighlightedItem(), + firstItem = this.getItems()[ 0 ]; if ( !this.isDisabled() && this.isVisible() ) { switch ( e.keyCode ) { case OO.ui.Keys.ENTER: - if ( currentItem && currentItem.constructor.static.highlightable ) { + if ( currentItem ) { // Was only highlighted, now let's select it. No-op if already selected. this.chooseItem( currentItem ); handled = true; @@ -6713,18 +6749,20 @@ OO.ui.SelectWidget.prototype.onDocumentKeyDown = function ( e ) { case OO.ui.Keys.UP: case OO.ui.Keys.LEFT: this.clearKeyPressBuffer(); - nextItem = this.findRelativeSelectableItem( currentItem, -1 ); + nextItem = currentItem ? + this.findRelativeSelectableItem( currentItem, -1 ) : firstItem; handled = true; break; case OO.ui.Keys.DOWN: case OO.ui.Keys.RIGHT: this.clearKeyPressBuffer(); - nextItem = this.findRelativeSelectableItem( currentItem, 1 ); + nextItem = currentItem ? + this.findRelativeSelectableItem( currentItem, 1 ) : firstItem; handled = true; break; case OO.ui.Keys.ESCAPE: case OO.ui.Keys.TAB: - if ( currentItem && currentItem.constructor.static.highlightable ) { + if ( currentItem ) { currentItem.setHighlighted( false ); } this.unbindDocumentKeyDownListener(); @@ -6944,20 +6982,36 @@ OO.ui.SelectWidget.prototype.findTargetItem = function ( e ) { return $option.data( 'oo-ui-optionWidget' ) || null; }; +/** + * Find all selected items, if there are any. If the widget allows for multiselect + * it will return an array of selected options. If the widget doesn't allow for + * multiselect, it will return the selected option or null if no item is selected. + * + * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect + * then return an array of selected items (or empty array), + * if the widget is not multiselect, return a single selected item, or `null` + * if no item is selected + */ +OO.ui.SelectWidget.prototype.findSelectedItems = function () { + var selected = this.items.filter( function ( item ) { + return item.isSelected(); + } ); + + return this.multiselect ? + selected : + selected[ 0 ] || null; +}; + /** * Find selected item. * - * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected + * @return {OO.ui.OptionWidget[]|OO.ui.OptionWidget|null} If the widget is multiselect + * then return an array of selected items (or empty array), + * if the widget is not multiselect, return a single selected item, or `null` + * if no item is selected */ OO.ui.SelectWidget.prototype.findSelectedItem = function () { - var i, len; - - for ( i = 0, len = this.items.length; i < len; i++ ) { - if ( this.items[ i ].isSelected() ) { - return this.items[ i ]; - } - } - return null; + return this.findSelectedItems(); }; /** @@ -7104,6 +7158,30 @@ OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) { return this.selectItem( itemFromData ); }; +/** + * Programmatically unselect an option by its reference. If the widget + * allows for multiple selections, there may be other items still selected; + * otherwise, no items will be selected. + * If no item is given, all selected items will be unselected. + * + * @param {OO.ui.OptionWidget} [item] Item to unselect + * @fires select + * @chainable + * @return {OO.ui.Widget} The widget, for chaining + */ +OO.ui.SelectWidget.prototype.unselectItem = function ( item ) { + if ( item ) { + item.setSelected( false ); + } else { + this.items.forEach( function ( item ) { + item.setSelected( false ); + } ); + } + + this.emit( 'select', this.findSelectedItems() ); + return this; +}; + /** * Programmatically select an option by its reference. If the `item` parameter is omitted, * all options will be deselected. @@ -7117,14 +7195,20 @@ OO.ui.SelectWidget.prototype.selectItem = function ( item ) { var i, len, selected, changed = false; - for ( i = 0, len = this.items.length; i < len; i++ ) { - selected = this.items[ i ] === item; - if ( this.items[ i ].isSelected() !== selected ) { - this.items[ i ].setSelected( selected ); - changed = true; + if ( this.multiselect && item ) { + // Select the item directly + item.setSelected( true ); + } else { + for ( i = 0, len = this.items.length; i < len; i++ ) { + selected = this.items[ i ] === item; + if ( this.items[ i ].isSelected() !== selected ) { + this.items[ i ].setSelected( selected ); + changed = true; + } } } if ( changed ) { + // TODO: When should a non-highlightable element be selected? if ( item && !item.constructor.static.highlightable ) { if ( item ) { this.$focusOwner.attr( 'aria-activedescendant', item.getElementId() ); @@ -7132,7 +7216,7 @@ OO.ui.SelectWidget.prototype.selectItem = function ( item ) { this.$focusOwner.removeAttr( 'aria-activedescendant' ); } } - this.emit( 'select', item ); + this.emit( 'select', this.findSelectedItems() ); } return this; @@ -7185,8 +7269,13 @@ OO.ui.SelectWidget.prototype.pressItem = function ( item ) { */ OO.ui.SelectWidget.prototype.chooseItem = function ( item ) { if ( item ) { - this.selectItem( item ); - this.emit( 'choose', item ); + if ( this.multiselect && item.isSelected() ) { + this.unselectItem( item ); + } else { + this.selectItem( item ); + } + + this.emit( 'choose', item, item.isSelected() ); } return this; @@ -7655,7 +7744,7 @@ OO.ui.MenuSelectWidget.prototype.onDocumentKeyDown = function ( e ) { break; case OO.ui.Keys.ESCAPE: case OO.ui.Keys.TAB: - if ( currentItem ) { + if ( currentItem && !this.multiselect ) { currentItem.setHighlighted( false ); } this.toggle( false ); @@ -7712,10 +7801,6 @@ OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () { section.toggle( showAll || !sectionEmpty ); } - if ( anyVisible && this.items.length && !exactMatch ) { - this.scrollItemIntoView( this.items[ 0 ] ); - } - if ( !anyVisible ) { this.highlightItem( null ); } @@ -7872,7 +7957,7 @@ OO.ui.MenuSelectWidget.prototype.clearItems = function () { * @inheritdoc */ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) { - var change, originalHeight, flippedHeight; + var change, originalHeight, flippedHeight, selectedItem; visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length; change = visible !== this.isVisible(); @@ -7937,9 +8022,13 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) { this.$focusOwner.attr( 'aria-expanded', 'true' ); - if ( this.findSelectedItem() ) { - this.$focusOwner.attr( 'aria-activedescendant', this.findSelectedItem().getElementId() ); - this.findSelectedItem().scrollElementIntoView( { duration: 0 } ); + 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. + this.$focusOwner.attr( 'aria-activedescendant', selectedItem.getElementId() ); + selectedItem.scrollElementIntoView( { duration: 0 } ); } // Auto-hide @@ -7962,6 +8051,13 @@ OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) { return this; }; +/** + * Scroll to the top of the menu + */ +OO.ui.MenuSelectWidget.prototype.scrollToTop = function () { + this.$element.scrollTop( 0 ); +}; + /** * DropdownWidgets are not menus themselves, rather they contain a menu of options created with * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that @@ -9401,6 +9497,7 @@ OO.ui.ButtonInputWidget.prototype.getInputId = function () { * @param {Object} [config] Configuration options * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is * not selected. + * @cfg {boolean} [indeterminate=false] Whether the checkbox is in the indeterminate state. */ OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) { // Configuration initialization @@ -9421,12 +9518,24 @@ OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) { // Required for pretty styling in WikimediaUI theme .append( this.checkIcon.$element ); this.setSelected( config.selected !== undefined ? config.selected : false ); + this.setIndeterminate( config.indeterminate !== undefined ? config.indeterminate : false ); }; /* Setup */ OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget ); +/* Events */ + +/** + * @event change + * + * A change event is emitted when the state of the input changes. + * + * @param {boolean} selected + * @param {boolean} indeterminate + */ + /* Static Properties */ /** @@ -9465,6 +9574,7 @@ OO.ui.CheckboxInputWidget.prototype.onEdit = function () { // Allow the stack to clear so the value will be updated setTimeout( function () { widget.setSelected( widget.$input.prop( 'checked' ) ); + widget.setIndeterminate( widget.$input.prop( 'indeterminate' ) ); } ); } }; @@ -9472,16 +9582,20 @@ OO.ui.CheckboxInputWidget.prototype.onEdit = function () { /** * Set selection state of this checkbox. * - * @param {boolean} state `true` for selected + * @param {boolean} state Selected state + * @param {boolean} internal Used for internal calls to suppress events * @chainable - * @return {OO.ui.Widget} The widget, for chaining + * @return {OO.ui.CheckboxInputWidget} The widget, for chaining */ -OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) { +OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state, internal ) { state = !!state; if ( this.selected !== state ) { this.selected = state; this.$input.prop( 'checked', this.selected ); - this.emit( 'change', this.selected ); + if ( !internal ) { + this.setIndeterminate( false, true ); + this.emit( 'change', this.selected, this.indeterminate ); + } } // The first time that the selection state is set (probably while constructing the widget), // remember it in defaultSelected. This property can be later used to check whether @@ -9508,6 +9622,42 @@ OO.ui.CheckboxInputWidget.prototype.isSelected = function () { return this.selected; }; +/** + * Set indeterminate state of this checkbox. + * + * @param {boolean} state Indeterminate state + * @param {boolean} internal Used for internal calls to suppress events + * @chainable + * @return {OO.ui.CheckboxInputWidget} The widget, for chaining + */ +OO.ui.CheckboxInputWidget.prototype.setIndeterminate = function ( state, internal ) { + state = !!state; + if ( this.indeterminate !== state ) { + this.indeterminate = state; + this.$input.prop( 'indeterminate', this.indeterminate ); + if ( !internal ) { + this.setSelected( false, true ); + this.emit( 'change', this.selected, this.indeterminate ); + } + } + return this; +}; + +/** + * Check if this checkbox is selected. + * + * @return {boolean} Checkbox is selected + */ +OO.ui.CheckboxInputWidget.prototype.isIndeterminate = function () { + // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify + // it, and we won't know unless they're kind enough to trigger a 'change' event. + var indeterminate = this.$input.prop( 'indeterminate' ); + if ( this.indeterminate !== indeterminate ) { + this.setIndeterminate( indeterminate ); + } + return this.indeterminate; +}; + /** * @inheritdoc */ @@ -10397,7 +10547,7 @@ OO.ui.CheckboxMultiselectInputWidget.prototype.focus = function () { * // A TextInputWidget. * var textInput = new OO.ui.TextInputWidget( { * value: 'Text input' - * } ) + * } ); * $( document.body ).append( textInput.$element ); * * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs @@ -11117,6 +11267,7 @@ OO.ui.SearchInputWidget = function OoUiSearchInputWidget( config ) { this.connect( this, { change: 'onChange' } ); + this.$indicator.on( 'click', this.onIndicatorClick.bind( this ) ); // Initialization this.updateSearchIndicator(); @@ -11140,9 +11291,12 @@ OO.ui.SearchInputWidget.prototype.getSaneType = function () { }; /** - * @inheritdoc + * Handle click events on the indicator + * + * @param {jQuery.Event} e Click event + * @return {boolean} */ -OO.ui.SearchInputWidget.prototype.onIndicatorMouseDown = function ( e ) { +OO.ui.SearchInputWidget.prototype.onIndicatorClick = function ( e ) { if ( e.which === OO.ui.MouseButtons.LEFT ) { // Clear the text field this.setValue( '' ); @@ -11205,8 +11359,8 @@ OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) { * // A MultilineTextInputWidget. * var multilineTextInput = new OO.ui.MultilineTextInputWidget( { * value: 'Text input on multiple lines' - * } ) - * $( 'body' ).append( multilineTextInput.$element ); + * } ); + * $( document.body ).append( multilineTextInput.$element ); * * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs#MultilineTextInputWidget * @@ -12360,21 +12514,21 @@ OO.ui.FieldsetLayout.static.tagName = 'fieldset'; * [2]: https://www.mediawiki.org/wiki/OOUI/Widgets/Inputs * * @example - * // Example of a form layout that wraps a fieldset layout + * // Example of a form layout that wraps a fieldset layout. * var input1 = new OO.ui.TextInputWidget( { - * placeholder: 'Username' - * } ); - * var input2 = new OO.ui.TextInputWidget( { - * placeholder: 'Password', - * type: 'password' - * } ); - * var submit = new OO.ui.ButtonInputWidget( { - * label: 'Submit' - * } ); + * placeholder: 'Username' + * } ), + * input2 = new OO.ui.TextInputWidget( { + * placeholder: 'Password', + * type: 'password' + * } ), + * submit = new OO.ui.ButtonInputWidget( { + * label: 'Submit' + * } ), + * fieldset = new OO.ui.FieldsetLayout( { + * label: 'A form layout' + * } ); * - * var fieldset = new OO.ui.FieldsetLayout( { - * label: 'A form layout' - * } ); * fieldset.addItems( [ * new OO.ui.FieldLayout( input1, { * label: 'Username', @@ -12565,7 +12719,7 @@ OO.ui.PanelLayout.prototype.focus = function () { * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper. * * @example - * // HorizontalLayout with a text input and a label + * // HorizontalLayout with a text input and a label. * var layout = new OO.ui.HorizontalLayout( { * items: [ * new OO.ui.LabelWidget( { label: 'Label' } ), @@ -12961,19 +13115,303 @@ OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) { } }; +/** + * Update the disabled state of the controls + * + * @chainable + * @protected + * @return {OO.ui.NumberInputWidget} The widget, for chaining + */ +OO.ui.NumberInputWidget.prototype.updateControlsDisabled = function () { + var disabled = this.isDisabled() || this.isReadOnly(); + if ( this.minusButton ) { + this.minusButton.setDisabled( disabled ); + } + if ( this.plusButton ) { + 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; +}; - if ( this.minusButton ) { - this.minusButton.setDisabled( this.isDisabled() ); +/** + * @inheritdoc + */ +OO.ui.NumberInputWidget.prototype.setReadOnly = function () { + // Parent method + OO.ui.NumberInputWidget.parent.prototype.setReadOnly.apply( this, arguments ); + this.updateControlsDisabled(); + return this; +}; + +/** + * SelectFileInputWidgets allow for selecting files, using . These + * widgets can be configured with {@link OO.ui.mixin.IconElement icons}, {@link + * OO.ui.mixin.IndicatorElement indicators} and {@link OO.ui.mixin.TitledElement titles}. + * Please see the [OOUI documentation on MediaWiki] [1] for more information and examples. + * + * SelectFileInputWidgets must be used in HTML forms, as getValue only returns the filename. + * + * @example + * // A file select input widget. + * var selectFile = new OO.ui.SelectFileInputWidget(); + * $( document.body ).append( selectFile.$element ); + * + * [1]: https://www.mediawiki.org/wiki/OOUI/Widgets + * + * @class + * @extends OO.ui.InputWidget + * + * @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) + this.selectButton = new OO.ui.ButtonWidget( $.extend( { + $element: $( '