/*!
- * OOjs UI v0.13.1
+ * OOjs UI v0.13.3
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2015 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2015-11-03T21:42:20Z
+ * Date: 2015-11-18T01:09:23Z
*/
( function ( OO ) {
* Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
* they support unnamed, ordered message parameters.
*
- * @abstract
* @param {string} key Message key
* @param {Mixed...} [params] Message parameters
* @return {string} Translated message with parameters substituted
* To add window content that persists between openings, you may wish to use the #initialize method
* instead.
*
- * @abstract
* @param {Object} [data] Window opening data
* @return {OO.ui.Process} Setup process
*/
* provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
* methods of OO.ui.Process.
*
- * @abstract
* @param {Object} [data] Window opening data
* @return {OO.ui.Process} Ready process
*/
* using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
* of OO.ui.Process.
*
- * @abstract
* @param {Object} [data] Window closing data
* @return {OO.ui.Process} Hold process
*/
* using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
* of OO.ui.Process.
*
- * @abstract
* @param {Object} [data] Window closing data
* @return {OO.ui.Process} Teardown process
*/
* @return {jQuery.Promise} Promise resolved when window is setup
*/
OO.ui.Window.prototype.setup = function ( data ) {
- var win = this,
- deferred = $.Deferred();
+ var win = this;
this.toggle( true );
this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
this.$focusTraps.on( 'focus', this.focusTrapHandler );
- this.getSetupProcess( data ).execute().done( function () {
+ return this.getSetupProcess( data ).execute().then( function () {
// Force redraw by asking the browser to measure the elements' widths
win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
win.$content.addClass( 'oo-ui-window-content-setup' ).width();
- deferred.resolve();
} );
-
- return deferred.promise();
};
/**
* @return {jQuery.Promise} Promise resolved when window is ready
*/
OO.ui.Window.prototype.ready = function ( data ) {
- var win = this,
- deferred = $.Deferred();
+ var win = this;
this.$content.focus();
- this.getReadyProcess( data ).execute().done( function () {
+ return this.getReadyProcess( data ).execute().then( function () {
// Force redraw by asking the browser to measure the elements' widths
win.$element.addClass( 'oo-ui-window-ready' ).width();
win.$content.addClass( 'oo-ui-window-content-ready' ).width();
- deferred.resolve();
} );
-
- return deferred.promise();
};
/**
* @return {jQuery.Promise} Promise resolved when window is held
*/
OO.ui.Window.prototype.hold = function ( data ) {
- var win = this,
- deferred = $.Deferred();
+ var win = this;
- this.getHoldProcess( data ).execute().done( function () {
+ return this.getHoldProcess( data ).execute().then( function () {
// Get the focused element within the window's content
var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
// Force redraw by asking the browser to measure the elements' widths
win.$element.removeClass( 'oo-ui-window-ready' ).width();
win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
- deferred.resolve();
} );
-
- return deferred.promise();
};
/**
OO.ui.Window.prototype.teardown = function ( data ) {
var win = this;
- return this.getTeardownProcess( data ).execute()
- .done( function () {
- // Force redraw by asking the browser to measure the elements' widths
- win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
- win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
- win.$focusTraps.off( 'focus', win.focusTrapHandler );
- win.toggle( false );
- } );
+ return this.getTeardownProcess( data ).execute().then( function () {
+ // Force redraw by asking the browser to measure the elements' widths
+ win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
+ win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
+ win.$focusTraps.off( 'focus', win.focusTrapHandler );
+ win.toggle( false );
+ } );
};
/**
* accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
* and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
*
- * @abstract
* @param {string} [action] Symbolic name of action
* @return {OO.ui.Process} Action process
*/
manager.opening = null;
manager.opened = $.Deferred();
opening.resolve( manager.opened.promise(), data );
+ }, function () {
+ manager.opening = null;
+ manager.opened = $.Deferred();
+ opening.reject();
+ manager.closeWindow( win );
} );
}, manager.getReadyDelay() );
+ }, function () {
+ manager.opening = null;
+ manager.opened = $.Deferred();
+ opening.reject();
+ manager.closeWindow( win );
} );
}, manager.getSetupDelay() );
} );
// If the window is currently opening, close it when it's done
this.preparingToClose = $.when( this.opening );
// Ensure handlers get called after preparingToClose is set
- this.preparingToClose.done( function () {
+ this.preparingToClose.always( function () {
manager.closing = closing;
manager.preparingToClose = null;
manager.emit( 'closing', win, closing, data );
.addClass( classes.on.join( ' ' ) );
};
+/**
+ * RequestManager is a mixin that manages the lifecycle of a promise-backed request for a widget, such as
+ * the {@link OO.ui.mixin.LookupElement}.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ */
+OO.ui.mixin.RequestManager = function OoUiMixinRequestManager() {
+ this.requestCache = {};
+ this.requestQuery = null;
+ this.requestRequest = null;
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.mixin.RequestManager );
+
+/**
+ * Get request results for the current query.
+ *
+ * @return {jQuery.Promise} Promise object which will be passed response data as the first argument of
+ * the done event. If the request was aborted to make way for a subsequent request, this promise
+ * may not be rejected, depending on what jQuery feels like doing.
+ */
+OO.ui.mixin.RequestManager.prototype.getRequestData = function () {
+ var widget = this,
+ value = this.getRequestQuery(),
+ deferred = $.Deferred(),
+ ourRequest;
+
+ this.abortRequest();
+ if ( Object.prototype.hasOwnProperty.call( this.requestCache, value ) ) {
+ deferred.resolve( this.requestCache[ value ] );
+ } else {
+ if ( this.pushPending ) {
+ this.pushPending();
+ }
+ this.requestQuery = value;
+ ourRequest = this.requestRequest = this.getRequest();
+ ourRequest
+ .always( function () {
+ // We need to pop pending even if this is an old request, otherwise
+ // the widget will remain pending forever.
+ // TODO: this assumes that an aborted request will fail or succeed soon after
+ // being aborted, or at least eventually. It would be nice if we could popPending()
+ // at abort time, but only if we knew that we hadn't already called popPending()
+ // for that request.
+ if ( widget.popPending ) {
+ widget.popPending();
+ }
+ } )
+ .done( function ( response ) {
+ // If this is an old request (and aborting it somehow caused it to still succeed),
+ // ignore its success completely
+ if ( ourRequest === widget.requestRequest ) {
+ widget.requestQuery = null;
+ widget.requestRequest = null;
+ widget.requestCache[ value ] = widget.getRequestCacheDataFromResponse( response );
+ deferred.resolve( widget.requestCache[ value ] );
+ }
+ } )
+ .fail( function () {
+ // If this is an old request (or a request failing because it's being aborted),
+ // ignore its failure completely
+ if ( ourRequest === widget.requestRequest ) {
+ widget.requestQuery = null;
+ widget.requestRequest = null;
+ deferred.reject();
+ }
+ } );
+ }
+ return deferred.promise();
+};
+
+/**
+ * Abort the currently pending request, if any.
+ *
+ * @private
+ */
+OO.ui.mixin.RequestManager.prototype.abortRequest = function () {
+ var oldRequest = this.requestRequest;
+ if ( oldRequest ) {
+ // First unset this.requestRequest to the fail handler will notice
+ // that the request is no longer current
+ this.requestRequest = null;
+ this.requestQuery = null;
+ oldRequest.abort();
+ }
+};
+
+/**
+ * Get the query to be made.
+ *
+ * @protected
+ * @method
+ * @abstract
+ * @return {string} query to be used
+ */
+OO.ui.mixin.RequestManager.prototype.getRequestQuery = null;
+
+/**
+ * Get a new request object of the current query value.
+ *
+ * @protected
+ * @method
+ * @abstract
+ * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
+ */
+OO.ui.mixin.RequestManager.prototype.getRequest = null;
+
+/**
+ * Pre-process data returned by the request from #getRequest.
+ *
+ * The return value of this function will be cached, and any further queries for the given value
+ * will use the cache rather than doing API requests.
+ *
+ * @protected
+ * @method
+ * @abstract
+ * @param {Mixed} response Response from server
+ * @return {Mixed} Cached result data
+ */
+OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;
+
/**
* The TabIndexedElement class is an attribute mixin used to add additional functionality to an
* element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
* @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
* @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
* By default, the lookup menu is not generated and displayed until the user begins to type.
+ * @cfg {boolean} [highlightFirst=true] Whether the first lookup result should be highlighted (so, that the user can
+ * take it over into the input with simply pressing return) automatically or not.
*/
OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
// Configuration initialization
- config = config || {};
+ config = $.extend( { highlightFirst: true }, config );
+
+ // Mixin constructors
+ OO.ui.mixin.RequestManager.call( this, config );
// Properties
this.$overlay = config.$overlay || this.$element;
this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
- this.lookupCache = {};
- this.lookupQuery = null;
- this.lookupRequest = null;
this.lookupsDisabled = false;
this.lookupInputFocused = false;
+ this.lookupHighlightFirstItem = config.highlightFirst;
// Events
this.$input.on( {
this.$overlay.append( this.lookupMenu.$element );
};
+/* Setup */
+
+OO.mixinClass( OO.ui.mixin.LookupElement, OO.ui.mixin.RequestManager );
+
/* Methods */
/**
};
/**
- * Highlight the first selectable item in the menu.
+ * Highlight the first selectable item in the menu, if configured.
*
* @private
* @chainable
*/
OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
- if ( !this.lookupMenu.getSelectedItem() ) {
+ if ( this.lookupHighlightFirstItem && !this.lookupMenu.getSelectedItem() ) {
this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
}
};
* will not be rejected: it will remain pending forever.
*/
OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
- var widget = this,
- value = this.getValue(),
- deferred = $.Deferred(),
- ourRequest;
-
- this.abortLookupRequest();
- if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
- deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
- } else {
- this.pushPending();
- this.lookupQuery = value;
- ourRequest = this.lookupRequest = this.getLookupRequest();
- ourRequest
- .always( function () {
- // We need to pop pending even if this is an old request, otherwise
- // the widget will remain pending forever.
- // TODO: this assumes that an aborted request will fail or succeed soon after
- // being aborted, or at least eventually. It would be nice if we could popPending()
- // at abort time, but only if we knew that we hadn't already called popPending()
- // for that request.
- widget.popPending();
- } )
- .done( function ( response ) {
- // If this is an old request (and aborting it somehow caused it to still succeed),
- // ignore its success completely
- if ( ourRequest === widget.lookupRequest ) {
- widget.lookupQuery = null;
- widget.lookupRequest = null;
- widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( response );
- deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
- }
- } )
- .fail( function () {
- // If this is an old request (or a request failing because it's being aborted),
- // ignore its failure completely
- if ( ourRequest === widget.lookupRequest ) {
- widget.lookupQuery = null;
- widget.lookupRequest = null;
- deferred.reject();
- }
- } );
- }
- return deferred.promise();
+ return this.getRequestData().then( function ( data ) {
+ return this.getLookupMenuOptionsFromData( data );
+ }.bind( this ) );
};
/**
* @private
*/
OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
- var oldRequest = this.lookupRequest;
- if ( oldRequest ) {
- // First unset this.lookupRequest to the fail handler will notice
- // that the request is no longer current
- this.lookupRequest = null;
- this.lookupQuery = null;
- oldRequest.abort();
- }
+ this.abortRequest();
};
/**
* Get a new request object of the current lookup query value.
*
* @protected
+ * @method
* @abstract
* @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
*/
-OO.ui.mixin.LookupElement.prototype.getLookupRequest = function () {
- // Stub, implemented in subclass
- return null;
-};
+OO.ui.mixin.LookupElement.prototype.getLookupRequest = null;
/**
* Pre-process data returned by the request from #getLookupRequest.
* will use the cache rather than doing API requests.
*
* @protected
+ * @method
* @abstract
* @param {Mixed} response Response from server
* @return {Mixed} Cached result data
*/
-OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
- // Stub, implemented in subclass
- return [];
-};
+OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = null;
/**
* Get a list of menu option widgets from the (possibly cached) data returned by
* #getLookupCacheDataFromResponse.
*
* @protected
+ * @method
* @abstract
* @param {Mixed} data Cached result data, usually an array
* @return {OO.ui.MenuOptionWidget[]} Menu items
*/
-OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
- // Stub, implemented in subclass
- return [];
-};
+OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = null;
/**
* Set the read-only state of the widget.
return this;
};
+/**
+ * @inheritdoc OO.ui.mixin.RequestManager
+ */
+OO.ui.mixin.LookupElement.prototype.getRequestQuery = function () {
+ return this.getValue();
+};
+
+/**
+ * @inheritdoc OO.ui.mixin.RequestManager
+ */
+OO.ui.mixin.LookupElement.prototype.getRequest = function () {
+ return this.getLookupRequest();
+};
+
+/**
+ * @inheritdoc OO.ui.mixin.RequestManager
+ */
+OO.ui.mixin.LookupElement.prototype.getRequestCacheDataFromResponse = function ( response ) {
+ return this.getLookupCacheDataFromResponse( response );
+};
+
/**
* 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
* @chainable
*/
OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
- title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
+ title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
+ title = ( typeof title === 'string' && title.length ) ? title : null;
if ( this.title !== title ) {
if ( this.$titled ) {
*
* This is an abstract method that must be overridden in a concrete subclass.
*
+ * @method
* @protected
* @abstract
*/
-OO.ui.Tool.prototype.onUpdateState = function () {
- throw new Error(
- 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
- );
-};
+OO.ui.Tool.prototype.onUpdateState = null;
/**
* Handle the tool being selected.
*
* This is an abstract method that must be overridden in a concrete subclass.
*
+ * @method
* @protected
* @abstract
*/
-OO.ui.Tool.prototype.onSelect = function () {
- throw new Error(
- 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
- );
-};
+OO.ui.Tool.prototype.onSelect = null;
/**
* Check if the tool is active.
*
* Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
* the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
- * picture’), and an icon.
+ * image’), and an icon.
*
* Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
* of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
* // Define the tools that we're going to place in our toolbar
*
* // Create a class inheriting from OO.ui.Tool
- * function PictureTool() {
- * PictureTool.parent.apply( this, arguments );
+ * function ImageTool() {
+ * ImageTool.parent.apply( this, arguments );
* }
- * OO.inheritClass( PictureTool, OO.ui.Tool );
+ * OO.inheritClass( ImageTool, OO.ui.Tool );
* // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
* // of 'icon' and 'title' (displayed icon and text).
- * PictureTool.static.name = 'picture';
- * PictureTool.static.icon = 'picture';
- * PictureTool.static.title = 'Insert picture';
+ * ImageTool.static.name = 'image';
+ * ImageTool.static.icon = 'image';
+ * ImageTool.static.title = 'Insert image';
* // Defines the action that will happen when this tool is selected (clicked).
- * PictureTool.prototype.onSelect = function () {
- * $area.text( 'Picture tool clicked!' );
+ * ImageTool.prototype.onSelect = function () {
+ * $area.text( 'Image tool clicked!' );
* // Never display this tool as "active" (selected).
* this.setActive( false );
* };
* // Make this tool available in our toolFactory and thus our toolbar
- * toolFactory.register( PictureTool );
+ * toolFactory.register( ImageTool );
*
* // Register two more tools, nothing interesting here
* function SettingsTool() {
* {
* // 'bar' tool groups display tools' icons only, side-by-side.
* type: 'bar',
- * include: [ 'picture', 'help' ]
+ * include: [ 'image', 'help' ]
* },
* {
* // 'list' tool groups display both the titles and icons, in a dropdown list.
* // Define the tools that we're going to place in our toolbar
*
* // Create a class inheriting from OO.ui.Tool
- * function PictureTool() {
- * PictureTool.parent.apply( this, arguments );
+ * function ImageTool() {
+ * ImageTool.parent.apply( this, arguments );
* }
- * OO.inheritClass( PictureTool, OO.ui.Tool );
+ * OO.inheritClass( ImageTool, OO.ui.Tool );
* // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
* // of 'icon' and 'title' (displayed icon and text).
- * PictureTool.static.name = 'picture';
- * PictureTool.static.icon = 'picture';
- * PictureTool.static.title = 'Insert picture';
+ * ImageTool.static.name = 'image';
+ * ImageTool.static.icon = 'image';
+ * ImageTool.static.title = 'Insert image';
* // Defines the action that will happen when this tool is selected (clicked).
- * PictureTool.prototype.onSelect = function () {
- * $area.text( 'Picture tool clicked!' );
+ * ImageTool.prototype.onSelect = function () {
+ * $area.text( 'Image tool clicked!' );
* // Never display this tool as "active" (selected).
* this.setActive( false );
* };
* // The toolbar can be synchronized with the state of some external stuff, like a text
* // editor's editing area, highlighting the tools (e.g. a 'bold' tool would be shown as active
* // when the text cursor was inside bolded text). Here we simply disable this feature.
- * PictureTool.prototype.onUpdateState = function () {
+ * ImageTool.prototype.onUpdateState = function () {
* };
* // Make this tool available in our toolFactory and thus our toolbar
- * toolFactory.register( PictureTool );
+ * toolFactory.register( ImageTool );
*
* // Register two more tools, nothing interesting here
* function SettingsTool() {
* {
* // 'bar' tool groups display tools' icons only, side-by-side.
* type: 'bar',
- * include: [ 'picture', 'help' ]
+ * include: [ 'image', 'help' ]
* },
* {
* // 'menu' tool groups display both the titles and icons, in a dropdown menu.
* // Define the tools that we're going to place in our toolbar
*
* // Create a class inheriting from OO.ui.Tool
- * function PictureTool() {
- * PictureTool.parent.apply( this, arguments );
+ * function ImageTool() {
+ * ImageTool.parent.apply( this, arguments );
* }
- * OO.inheritClass( PictureTool, OO.ui.Tool );
+ * OO.inheritClass( ImageTool, OO.ui.Tool );
* // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
* // of 'icon' and 'title' (displayed icon and text).
- * PictureTool.static.name = 'picture';
- * PictureTool.static.icon = 'picture';
- * PictureTool.static.title = 'Insert picture';
+ * ImageTool.static.name = 'image';
+ * ImageTool.static.icon = 'image';
+ * ImageTool.static.title = 'Insert image';
* // Defines the action that will happen when this tool is selected (clicked).
- * PictureTool.prototype.onSelect = function () {
- * $area.text( 'Picture tool clicked!' );
+ * ImageTool.prototype.onSelect = function () {
+ * $area.text( 'Image tool clicked!' );
* // Never display this tool as "active" (selected).
* this.setActive( false );
* };
* // Make this tool available in our toolFactory and thus our toolbar
- * toolFactory.register( PictureTool );
+ * toolFactory.register( ImageTool );
*
* // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
* // little popup window (a PopupWidget).
* {
* // 'bar' tool groups display tools by icon only
* type: 'bar',
- * include: [ 'picture', 'help' ]
+ * include: [ 'image', 'help' ]
* }
* ] );
*
* type: 'list',
* label: 'ListToolGroup',
* indicator: 'down',
- * icon: 'picture',
+ * icon: 'image',
* title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
* header: 'This is the header',
* include: [ 'settings', 'stuff' ],
};
/**
- * CapsuleMultiSelectWidgets are something like a {@link OO.ui.ComboBoxWidget combo box widget}
+ * CapsuleMultiSelectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
* that allows for selecting multiple values.
*
* For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
*/
OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
var textRange, isBackwards, start, end,
- element = this.$input[ 0 ];
+ input = this.$input[ 0 ];
to = to || from;
this.focus();
- if ( element.setSelectionRange ) {
- element.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
- } else if ( element.createTextRange ) {
+ if ( input.setSelectionRange ) {
+ input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
+ } else if ( input.createTextRange ) {
// IE 8 and below
- textRange = element.createTextRange();
+ textRange = input.createTextRange();
textRange.collapse( true );
textRange.moveStart( 'character', start );
textRange.moveEnd( 'character', end - start );
return this;
};
+/**
+ * Get an object describing the current selection range in a directional manner
+ *
+ * @return {Object} Object containing 'from' and 'to' offsets
+ */
+OO.ui.TextInputWidget.prototype.getRange = function () {
+ var input = this.$input[ 0 ],
+ start = input.selectionStart,
+ end = input.selectionEnd,
+ isBackwards = input.selectionDirection === 'backward';
+
+ return {
+ from: isBackwards ? end : start,
+ to: isBackwards ? start : end
+ };
+};
+
/**
* Get the length of the text input value.
*
};
/**
- * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
+ * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
* can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
* a value can be chosen instead). Users can choose options from the combo box in one of two ways:
*
* - by choosing a value from the menu. The value of the chosen option will then appear in the text
* input field.
*
+ * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
+ *
* For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
*
* @example
- * // Example: A ComboBoxWidget.
- * var comboBox = new OO.ui.ComboBoxWidget( {
- * label: 'ComboBoxWidget',
- * input: { value: 'Option One' },
+ * // Example: A ComboBoxInputWidget.
+ * var comboBox = new OO.ui.ComboBoxInputWidget( {
+ * label: 'ComboBoxInputWidget',
+ * value: 'Option 1',
* menu: {
* items: [
* new OO.ui.MenuOptionWidget( {
* [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
*
* @class
- * @extends OO.ui.Widget
- * @mixins OO.ui.mixin.TabIndexedElement
+ * @extends OO.ui.TextInputWidget
*
* @constructor
* @param {Object} [config] Configuration options
+ * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
* @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
- * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
* @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
* the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
* containing `<div>` and has a larger area. By default, the menu uses relative positioning.
*/
-OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
+OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
// Configuration initialization
- config = config || {};
+ config = $.extend( {
+ indicator: 'down'
+ }, config );
+ // For backwards-compatibility with ComboBoxWidget config
+ $.extend( config, config.input );
// Parent constructor
- OO.ui.ComboBoxWidget.parent.call( this, config );
-
- // Properties (must be set before TabIndexedElement constructor call)
- this.$indicator = this.$( '<span>' );
-
- // Mixin constructors
- OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
+ OO.ui.ComboBoxInputWidget.parent.call( this, config );
// Properties
this.$overlay = config.$overlay || this.$element;
- this.input = new OO.ui.TextInputWidget( $.extend(
- {
- indicator: 'down',
- $indicator: this.$indicator,
- disabled: this.isDisabled()
- },
- config.input
- ) );
- this.input.$input.eq( 0 ).attr( {
- role: 'combobox',
- 'aria-autocomplete': 'list'
- } );
this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
{
widget: this,
- input: this.input,
- $container: this.input.$element,
+ input: this,
+ $container: this.$element,
disabled: this.isDisabled()
},
config.menu
) );
+ // For backwards-compatibility with ComboBoxWidget
+ this.input = this;
// Events
this.$indicator.on( {
- click: this.onClick.bind( this ),
- keypress: this.onKeyPress.bind( this )
+ click: this.onIndicatorClick.bind( this ),
+ keypress: this.onIndicatorKeyPress.bind( this )
} );
- this.input.connect( this, {
+ this.connect( this, {
change: 'onInputChange',
enter: 'onInputEnter'
} );
} );
// Initialization
- this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
+ this.$input.attr( {
+ role: 'combobox',
+ 'aria-autocomplete': 'list'
+ } );
+ // Do not override options set via config.menu.items
+ if ( config.options !== undefined ) {
+ this.setOptions( config.options );
+ }
+ // Extra class for backwards-compatibility with ComboBoxWidget
+ this.$element.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
this.$overlay.append( this.menu.$element );
this.onMenuItemsChange();
};
/* Setup */
-OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
-OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.mixin.TabIndexedElement );
+OO.inheritClass( OO.ui.ComboBoxInputWidget, OO.ui.TextInputWidget );
/* Methods */
* Get the combobox's menu.
* @return {OO.ui.FloatingMenuSelectWidget} Menu widget
*/
-OO.ui.ComboBoxWidget.prototype.getMenu = function () {
+OO.ui.ComboBoxInputWidget.prototype.getMenu = function () {
return this.menu;
};
* Get the combobox's text input widget.
* @return {OO.ui.TextInputWidget} Text input widget
*/
-OO.ui.ComboBoxWidget.prototype.getInput = function () {
- return this.input;
+OO.ui.ComboBoxInputWidget.prototype.getInput = function () {
+ return this;
};
/**
* @private
* @param {string} value New value
*/
-OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
+OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
var match = this.menu.getItemFromData( value );
this.menu.selectItem( match );
* @private
* @param {jQuery.Event} e Mouse click event
*/
-OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
+OO.ui.ComboBoxInputWidget.prototype.onIndicatorClick = function ( e ) {
if ( !this.isDisabled() && e.which === 1 ) {
this.menu.toggle();
- this.input.$input[ 0 ].focus();
+ this.$input[ 0 ].focus();
}
return false;
};
* @private
* @param {jQuery.Event} e Key press event
*/
-OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
+OO.ui.ComboBoxInputWidget.prototype.onIndicatorKeyPress = function ( e ) {
if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
this.menu.toggle();
- this.input.$input[ 0 ].focus();
+ this.$input[ 0 ].focus();
return false;
}
};
*
* @private
*/
-OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
+OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
if ( !this.isDisabled() ) {
this.menu.toggle( false );
}
* @private
* @param {OO.ui.OptionWidget} item Chosen item
*/
-OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
- this.input.setValue( item.getData() );
+OO.ui.ComboBoxInputWidget.prototype.onMenuChoose = function ( item ) {
+ this.setValue( item.getData() );
};
/**
*
* @private
*/
-OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
- var match = this.menu.getItemFromData( this.input.getValue() );
+OO.ui.ComboBoxInputWidget.prototype.onMenuItemsChange = function () {
+ var match = this.menu.getItemFromData( this.getValue() );
this.menu.selectItem( match );
if ( this.menu.getHighlightedItem() ) {
this.menu.highlightItem( match );
}
- this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
+ this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu.isEmpty() );
};
/**
* @inheritdoc
*/
-OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
+OO.ui.ComboBoxInputWidget.prototype.setDisabled = function ( disabled ) {
// Parent method
- OO.ui.ComboBoxWidget.parent.prototype.setDisabled.call( this, disabled );
+ OO.ui.ComboBoxInputWidget.parent.prototype.setDisabled.call( this, disabled );
- if ( this.input ) {
- this.input.setDisabled( this.isDisabled() );
- }
if ( this.menu ) {
this.menu.setDisabled( this.isDisabled() );
}
return this;
};
+/**
+ * Set the options available for this input.
+ *
+ * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
+ * @chainable
+ */
+OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
+ this.getMenu()
+ .clearItems()
+ .addItems( options.map( function ( opt ) {
+ return new OO.ui.MenuOptionWidget( {
+ data: opt.data,
+ label: opt.label !== undefined ? opt.label : opt.data
+ } );
+ } ) );
+
+ return this;
+};
+
+/**
+ * @class
+ * @deprecated Use OO.ui.ComboBoxInputWidget instead.
+ */
+OO.ui.ComboBoxWidget = OO.ui.ComboBoxInputWidget;
+
/**
* LabelWidgets help identify the function of interface elements. Each LabelWidget can
* be configured with a `label` option that is set to a string, a label node, or a function:
/**
* MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
* is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
- * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxWidget ComboBoxWidget},
+ * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
* and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
* MenuSelectWidgets themselves are not instantiated directly, rather subclassed
* and customized to be opened, closed, and displayed as needed.
* @constructor
* @param {Object} [config] Configuration options
* @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
- * the text the user types. This config is used by {@link OO.ui.ComboBoxWidget ComboBoxWidget}
+ * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
* and {@link OO.ui.mixin.LookupElement LookupElement}
* @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
* the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
* The menu's position is automatically calculated and maintained when the menu
* is toggled or the window is resized.
*
- * See OO.ui.ComboBoxWidget for an example of a widget that uses this class.
+ * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
*
* @class
* @extends OO.ui.MenuSelectWidget