/*!
- * OOjs UI v0.12.10
+ * 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-09-29T21:20:38Z
+ * 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
* @cfg {Array} [content] An array of content elements to append (after #text).
* Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
* Instances of OO.ui.Element will have their $element appended.
- * @cfg {jQuery} [$content] Content elements to append (after #text)
+ * @cfg {jQuery} [$content] Content elements to append (after #text).
+ * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
* @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
* Data can also be specified with the #setData method.
*/
}
}
} );
- // jscs:disable requireCapitalizedConstructors
- obj = new cls( data ); // rebuild widget
+ // allow widgets to reuse parts of the DOM
+ data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data );
// pick up dynamic state, like focus, value of form inputs, scroll position, etc.
- state = obj.gatherPreInfuseState( $elem );
+ state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
+ // rebuild widget
+ // jscs:disable requireCapitalizedConstructors
+ obj = new cls( data );
+ // jscs:enable requireCapitalizedConstructors
// now replace old DOM with this new DOM.
if ( top ) {
- $elem.replaceWith( obj.$element );
- // This element is now gone from the DOM, but if anyone is holding a reference to it,
- // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
- // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
- $elem[ 0 ].oouiInfused = obj;
+ // An efficient constructor might be able to reuse the entire DOM tree of the original element,
+ // so only mutate the DOM if we need to.
+ if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
+ $elem.replaceWith( obj.$element );
+ // This element is now gone from the DOM, but if anyone is holding a reference to it,
+ // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
+ // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
+ $elem[ 0 ].oouiInfused = obj;
+ }
top.resolve();
}
obj.$element.data( 'ooui-infused', obj );
return obj;
};
+/**
+ * Pick out parts of `node`'s DOM to be reused when infusing a widget.
+ *
+ * This method **must not** make any changes to the DOM, only find interesting pieces and add them
+ * to `config` (which should then be returned). Actual DOM juggling should then be done by the
+ * constructor, which will be given the enhanced config.
+ *
+ * @protected
+ * @param {HTMLElement} node
+ * @param {Object} config
+ * @return {Object}
+ */
+OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) {
+ return config;
+};
+
+/**
+ * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
+ * (and its children) that represent an Element of the same class and the given configuration,
+ * generated by the PHP implementation.
+ *
+ * This method is called just before `node` is detached from the DOM. The return value of this
+ * function will be passed to #restorePreInfuseState after the newly created widget's #$element
+ * is inserted into DOM to replace `node`.
+ *
+ * @protected
+ * @param {HTMLElement} node
+ * @param {Object} config
+ * @return {Object}
+ */
+OO.ui.Element.static.gatherPreInfuseState = function () {
+ return {};
+};
+
/**
* Get a jQuery function within a specific document.
*
return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
};
-/**
- * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
- * (and its children) that represent an Element of the same type and configuration as the current
- * one, generated by the PHP implementation.
- *
- * This method is called just before `node` is detached from the DOM. The return value of this
- * function will be passed to #restorePreInfuseState after this widget's #$element is inserted into
- * DOM to replace `node`.
- *
- * @protected
- * @param {HTMLElement} node
- * @return {Object}
- */
-OO.ui.Element.prototype.gatherPreInfuseState = function () {
- return {};
-};
-
/**
* Restore the pre-infusion dynamic state for this widget.
*
* 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 );
* @param {OO.ui.Element} element Element for which to get classes
* @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
*/
-OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
+OO.ui.Theme.prototype.getElementClasses = function () {
return { on: [], off: [] };
};
.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
};
/**
- * Set the button to its 'active' state.
+ * Set the button's active state.
*
* The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
* a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
* for other button types.
*
- * @param {boolean} [value] Make button active
+ * @param {boolean} value Make button active
* @chainable
*/
OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
- this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
+ this.active = !!value;
+ this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
return this;
};
+/**
+ * Check if the button is active
+ *
+ * @return {boolean} The button is active
+ */
+OO.ui.mixin.ButtonElement.prototype.isActive = function () {
+ return this.active;
+};
+
/**
* Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
* {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
* @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.
* @throws {Error} An error is thrown if no widget is specified
*/
OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
- var hasInputWidget, div, i;
+ var hasInputWidget, div;
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
// Properties
this.fieldWidget = fieldWidget;
- this.errors = config.errors || [];
- this.notices = config.notices || [];
+ this.errors = [];
+ this.notices = [];
this.$field = $( '<div>' );
this.$messages = $( '<ul>' );
this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
this.$element
.addClass( 'oo-ui-fieldLayout' )
.append( this.$help, this.$body );
- if ( this.errors.length || this.notices.length ) {
- this.$element.append( this.$messages );
- }
this.$body.addClass( 'oo-ui-fieldLayout-body' );
this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
this.$field
.toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
.append( this.fieldWidget.$element );
- for ( i = 0; i < this.notices.length; i++ ) {
- this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
- }
- for ( i = 0; i < this.errors.length; i++ ) {
- this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
- }
-
+ this.setErrors( config.errors || [] );
+ this.setNotices( config.notices || [] );
this.setAlignment( config.align );
};
};
/**
+ * @protected
* @param {string} kind 'error' or 'notice'
* @param {string|OO.ui.HtmlSnippet} text
* @return {jQuery}
return this;
};
+/**
+ * Set the list of error messages.
+ *
+ * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
+ * The array may contain strings or OO.ui.HtmlSnippet instances.
+ * @chainable
+ */
+OO.ui.FieldLayout.prototype.setErrors = function ( errors ) {
+ this.errors = errors.slice();
+ this.updateMessages();
+ return this;
+};
+
+/**
+ * Set the list of notice messages.
+ *
+ * @param {Array} notices Notices about the widget, which will be displayed below the widget.
+ * The array may contain strings or OO.ui.HtmlSnippet instances.
+ * @chainable
+ */
+OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
+ this.notices = notices.slice();
+ this.updateMessages();
+ return this;
+};
+
+/**
+ * Update the rendering of error and notice messages.
+ *
+ * @private
+ */
+OO.ui.FieldLayout.prototype.updateMessages = function () {
+ var i;
+ this.$messages.empty();
+
+ if ( this.errors.length || this.notices.length ) {
+ this.$body.after( this.$messages );
+ } else {
+ this.$messages.remove();
+ return;
+ }
+
+ for ( i = 0; i < this.notices.length; i++ ) {
+ this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
+ }
+ for ( i = 0; i < this.errors.length; i++ ) {
+ this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
+ }
+};
+
/**
* ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
* and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
if ( this.outlined ) {
this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
+ this.scrolling = false;
+ this.stackLayout.connect( this, { visibleItemChange: 'onStackLayoutVisibleItemChange' } );
}
if ( this.autoFocus ) {
// Event 'focus' does not bubble, but 'focusin' does
}
};
+/**
+ * Handle visibleItemChange events from the stackLayout
+ *
+ * The next visible page is set as the current page by selecting it
+ * in the outline
+ *
+ * @param {OO.ui.PageLayout} page The next visible page in the layout
+ */
+OO.ui.BookletLayout.prototype.onStackLayoutVisibleItemChange = function ( page ) {
+ // Set a flag to so that the resulting call to #onStackLayoutSet doesn't
+ // try and scroll the item into view again.
+ this.scrolling = true;
+ this.outlineSelectWidget.selectItemByData( page.getName() );
+ this.scrolling = false;
+};
+
/**
* Handle stack layout set events.
*
*/
OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
var layout = this;
- if ( page ) {
+ if ( !this.scrolling && page ) {
page.scrollElementIntoView( { complete: function () {
if ( layout.autoFocus ) {
layout.focus();
return;
}
// Only change the focus if is not already in the current page
- if ( !page.$element.find( ':focus' ).length ) {
- OO.ui.findFocusable( page.$element ).focus();
+ if ( !OO.ui.contains( page.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
+ page.focus();
}
};
if ( !card ) {
return;
}
- // Only change the focus if is not already in the current card
- if ( !card.$element.find( ':focus' ).length ) {
- OO.ui.findFocusable( card.$element ).focus();
+ // Only change the focus if is not already in the current page
+ if ( !OO.ui.contains( card.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
+ card.focus();
}
};
OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
+/* Methods */
+
/**
- * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
+ * Focus the panel layout
+ *
+ * The default implementation just focuses the first focusable element in the panel
+ */
+OO.ui.PanelLayout.prototype.focus = function () {
+ OO.ui.findFocusable( this.$element ).focus();
+};
+
+/**
+ * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
* from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
* rather extended to include the required content and functionality.
*
this.$element.addClass( 'oo-ui-stackLayout' );
if ( this.continuous ) {
this.$element.addClass( 'oo-ui-stackLayout-continuous' );
+ this.$element.on( 'scroll', OO.ui.debounce( this.onScroll.bind( this ), 250 ) );
}
if ( Array.isArray( config.items ) ) {
this.addItems( config.items );
* @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
*/
+/**
+ * When used in continuous mode, this event is emitted when the user scrolls down
+ * far enough such that currentItem is no longer visible.
+ *
+ * @event visibleItemChange
+ * @param {OO.ui.PanelLayout} panel The next visible item in the layout
+ */
+
/* Methods */
+/**
+ * Handle scroll events from the layout element
+ *
+ * @param {jQuery.Event} e
+ * @fires visibleItemChange
+ */
+OO.ui.StackLayout.prototype.onScroll = function () {
+ var currentRect,
+ len = this.items.length,
+ currentIndex = this.items.indexOf( this.currentItem ),
+ newIndex = currentIndex,
+ containerRect = this.$element[ 0 ].getBoundingClientRect();
+
+ if ( !containerRect || ( !containerRect.top && !containerRect.bottom ) ) {
+ // Can't get bounding rect, possibly not attached.
+ return;
+ }
+
+ function getRect( item ) {
+ return item.$element[ 0 ].getBoundingClientRect();
+ }
+
+ function isVisible( item ) {
+ var rect = getRect( item );
+ return rect.bottom > containerRect.top && rect.top < containerRect.bottom;
+ }
+
+ currentRect = getRect( this.currentItem );
+
+ if ( currentRect.bottom < containerRect.top ) {
+ // Scrolled down past current item
+ while ( ++newIndex < len ) {
+ if ( isVisible( this.items[ newIndex ] ) ) {
+ break;
+ }
+ }
+ } else if ( currentRect.top > containerRect.bottom ) {
+ // Scrolled up past current item
+ while ( --newIndex >= 0 ) {
+ if ( isVisible( this.items[ newIndex ] ) ) {
+ break;
+ }
+ }
+ }
+
+ if ( newIndex !== currentIndex ) {
+ this.emit( 'visibleItemChange', this.items[ newIndex ] );
+ }
+};
+
/**
* Get the current panel.
*
* // 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].
* @param {jQuery.Event} event
*/
OO.ui.CapsuleMultiSelectWidget.prototype.onInputBlur = function () {
+ if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
+ this.addItemsFromData( [ this.$input.val() ] );
+ }
this.clearInput();
};
}
};
+/**
+ * Focus the widget.
+ *
+ * Focusses the select file button.
+ *
+ * @chainable
+ */
+OO.ui.SelectFileWidget.prototype.focus = function () {
+ this.selectButton.$button[ 0 ].focus();
+ return this;
+};
+
/**
* Update the user interface when a file is selected or unselected
*
* @param {Object} [config] Configuration options
* @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
* @cfg {string} [value=''] The value of the input.
- * @cfg {string} [accessKey=''] The access key of the input.
+ * @cfg {string} [dir] The directionality of the input (ltr/rtl).
* @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
* before it is accepted.
*/
.append( this.$input );
this.setValue( config.value );
this.setAccessKey( config.accessKey );
+ if ( config.dir ) {
+ this.setDir( config.dir );
+ }
};
/* Setup */
OO.ui.InputWidget.static.supportsSimpleLabel = true;
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.InputWidget.static.reusePreInfuseDOM = function ( node, config ) {
+ config = OO.ui.InputWidget.parent.static.reusePreInfuseDOM( node, config );
+ // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
+ config.$input = $( node ).find( '.oo-ui-inputWidget-input' );
+ return config;
+};
+
+/**
+ * @inheritdoc
+ */
+OO.ui.InputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.InputWidget.parent.static.gatherPreInfuseState( node, config );
+ state.value = config.$input.val();
+ // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
+ state.focus = config.$input.is( ':focus' );
+ return state;
+};
+
/* Events */
/**
* @param {Object} config Configuration options
* @return {jQuery} Input element
*/
-OO.ui.InputWidget.prototype.getInputElement = function () {
- return $( '<input>' );
+OO.ui.InputWidget.prototype.getInputElement = function ( config ) {
+ // See #reusePreInfuseDOM about config.$input
+ return config.$input || $( '<input>' );
};
/**
};
/**
- * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right).
+ * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
*
- * @param {boolean} isRTL
- * Direction is right-to-left
+ * @deprecated since v0.13.1, use #setDir directly
+ * @param {boolean} isRTL Directionality is right-to-left
+ * @chainable
*/
OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
- this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
+ this.setDir( isRTL ? 'rtl' : 'ltr' );
+ return this;
+};
+
+/**
+ * Set the directionality of the input.
+ *
+ * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
+ * @chainable
+ */
+OO.ui.InputWidget.prototype.setDir = function ( dir ) {
+ this.$input.prop( 'dir', dir );
+ return this;
};
/**
return this;
};
-/**
- * @inheritdoc
- */
-OO.ui.InputWidget.prototype.gatherPreInfuseState = function ( node ) {
- var
- state = OO.ui.InputWidget.parent.prototype.gatherPreInfuseState.call( this, node ),
- $input = state.$input || $( node ).find( '.oo-ui-inputWidget-input' );
- state.value = $input.val();
- // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
- state.focus = $input.is( ':focus' );
- return state;
-};
-
/**
* @inheritdoc
*/
* @protected
*/
OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
- var type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ?
- config.type :
- 'button';
+ var type;
+ // See InputWidget#reusePreInfuseDOM about config.$input
+ if ( config.$input ) {
+ return config.$input.empty();
+ }
+ type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ? config.type : 'button';
return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
};
OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.CheckboxInputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.CheckboxInputWidget.parent.static.gatherPreInfuseState( node, config );
+ state.checked = config.$input.prop( 'checked' );
+ return state;
+};
+
/* Methods */
/**
return this.selected;
};
-/**
- * @inheritdoc
- */
-OO.ui.CheckboxInputWidget.prototype.gatherPreInfuseState = function ( node ) {
- var
- state = OO.ui.CheckboxInputWidget.parent.prototype.gatherPreInfuseState.call( this, node ),
- $input = $( node ).find( '.oo-ui-inputWidget-input' );
- state.$input = $input; // shortcut for performance, used in InputWidget
- state.checked = $input.prop( 'checked' );
- return state;
-};
-
/**
* @inheritdoc
*/
* @inheritdoc
* @protected
*/
-OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
+OO.ui.DropdownInputWidget.prototype.getInputElement = function ( config ) {
+ // See InputWidget#reusePreInfuseDOM about config.$input
+ if ( config.$input ) {
+ return config.$input.addClass( 'oo-ui-element-hidden' );
+ }
return $( '<input type="hidden">' );
};
OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioInputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.RadioInputWidget.parent.static.gatherPreInfuseState( node, config );
+ state.checked = config.$input.prop( 'checked' );
+ return state;
+};
+
/* Methods */
/**
return this.$input.prop( 'checked' );
};
-/**
- * @inheritdoc
- */
-OO.ui.RadioInputWidget.prototype.gatherPreInfuseState = function ( node ) {
- var
- state = OO.ui.RadioInputWidget.parent.prototype.gatherPreInfuseState.call( this, node ),
- $input = $( node ).find( '.oo-ui-inputWidget-input' );
- state.$input = $input; // shortcut for performance, used in InputWidget
- state.checked = $input.prop( 'checked' );
- return state;
-};
-
/**
* @inheritdoc
*/
OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.RadioSelectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.RadioSelectInputWidget.parent.static.gatherPreInfuseState( node, config );
+ state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
+ return state;
+};
+
/* Methods */
/**
return this;
};
-/**
- * @inheritdoc
- */
-OO.ui.RadioSelectInputWidget.prototype.gatherPreInfuseState = function ( node ) {
- var state = OO.ui.RadioSelectInputWidget.parent.prototype.gatherPreInfuseState.call( this, node );
- state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
- return state;
-};
-
/**
* TextInputWidgets, like HTML text inputs, can be configured with options that customize the
* size of the field as well as its presentation. In addition, these widgets can be configured
this.minRows = config.rows !== undefined ? config.rows : '';
this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
this.validate = null;
+ this.styleHeight = null;
+ this.scrollWidth = null;
// Clone for resizing
if ( this.autosize ) {
}
if ( config.autocomplete === false ) {
this.$input.attr( 'autocomplete', 'off' );
+ // Turning off autocompletion also disables "form caching" when the user navigates to a
+ // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
+ $( window ).on( {
+ beforeunload: function () {
+ this.$input.removeAttr( 'autocomplete' );
+ }.bind( this ),
+ pageshow: function () {
+ // Browsers don't seem to actually fire this event on "Back", they instead just reload the
+ // whole page... it shouldn't hurt, though.
+ this.$input.attr( 'autocomplete', 'off' );
+ }.bind( this )
+ } );
}
if ( this.multiline && config.rows ) {
this.$input.attr( 'rows', config.rows );
integer: /^\d+$/
};
+/* Static Methods */
+
+/**
+ * @inheritdoc
+ */
+OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
+ var state = OO.ui.TextInputWidget.parent.static.gatherPreInfuseState( node, config );
+ if ( config.multiline ) {
+ state.scrollTop = config.$input.scrollTop();
+ }
+ return state;
+};
+
/* Events */
/**
* @event enter
*/
+/**
+ * A `resize` event is emitted when autosize is set and the widget resizes
+ *
+ * @event resize
+ */
+
/* Methods */
/**
* This only affects #multiline inputs that are {@link #autosize autosized}.
*
* @chainable
+ * @fires resize
*/
OO.ui.TextInputWidget.prototype.adjustSize = function () {
- var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
+ var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError,
+ idealHeight, newHeight, scrollWidth, property;
- if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
- this.$clone
- .val( this.$input.val() )
- .attr( 'rows', this.minRows )
- // Set inline height property to 0 to measure scroll height
- .css( 'height', 0 );
+ if ( this.multiline && this.$input.val() !== this.valCache ) {
+ if ( this.autosize ) {
+ this.$clone
+ .val( this.$input.val() )
+ .attr( 'rows', this.minRows )
+ // Set inline height property to 0 to measure scroll height
+ .css( 'height', 0 );
- this.$clone.removeClass( 'oo-ui-element-hidden' );
+ this.$clone.removeClass( 'oo-ui-element-hidden' );
- this.valCache = this.$input.val();
+ this.valCache = this.$input.val();
- scrollHeight = this.$clone[ 0 ].scrollHeight;
+ scrollHeight = this.$clone[ 0 ].scrollHeight;
- // Remove inline height property to measure natural heights
- this.$clone.css( 'height', '' );
- innerHeight = this.$clone.innerHeight();
- outerHeight = this.$clone.outerHeight();
+ // Remove inline height property to measure natural heights
+ this.$clone.css( 'height', '' );
+ innerHeight = this.$clone.innerHeight();
+ outerHeight = this.$clone.outerHeight();
- // Measure max rows height
- this.$clone
- .attr( 'rows', this.maxRows )
- .css( 'height', 'auto' )
- .val( '' );
- maxInnerHeight = this.$clone.innerHeight();
+ // Measure max rows height
+ this.$clone
+ .attr( 'rows', this.maxRows )
+ .css( 'height', 'auto' )
+ .val( '' );
+ maxInnerHeight = this.$clone.innerHeight();
- // Difference between reported innerHeight and scrollHeight with no scrollbars present
- // Equals 1 on Blink-based browsers and 0 everywhere else
- measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
- idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
+ // Difference between reported innerHeight and scrollHeight with no scrollbars present
+ // Equals 1 on Blink-based browsers and 0 everywhere else
+ measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
+ idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
- this.$clone.addClass( 'oo-ui-element-hidden' );
+ this.$clone.addClass( 'oo-ui-element-hidden' );
- // Only apply inline height when expansion beyond natural height is needed
- if ( idealHeight > innerHeight ) {
+ // Only apply inline height when expansion beyond natural height is needed
// Use the difference between the inner and outer height as a buffer
- this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
- } else {
- this.$input.css( 'height', '' );
+ newHeight = idealHeight > innerHeight ? idealHeight + ( outerHeight - innerHeight ) : '';
+ if ( newHeight !== this.styleHeight ) {
+ this.$input.css( 'height', newHeight );
+ this.styleHeight = newHeight;
+ this.emit( 'resize' );
+ }
+ }
+ scrollWidth = this.$input[ 0 ].offsetWidth - this.$input[ 0 ].clientWidth;
+ if ( scrollWidth !== this.scrollWidth ) {
+ property = this.$element.css( 'direction' ) === 'rtl' ? 'left' : 'right';
+ // Reset
+ this.$label.css( { right: '', left: '' } );
+ this.$indicator.css( { right: '', left: '' } );
+
+ if ( scrollWidth ) {
+ this.$indicator.css( property, scrollWidth );
+ if ( this.labelPosition === 'after' ) {
+ this.$label.css( property, scrollWidth );
+ }
+ }
+
+ this.scrollWidth = scrollWidth;
+ this.positionLabel();
}
}
return this;
};
/**
- * Select the entire text of the input.
+ * Focus the input and select a specified range within the text.
*
+ * @param {number} from Select from offset
+ * @param {number} [to] Select to offset, defaults to from
* @chainable
*/
-OO.ui.TextInputWidget.prototype.select = function () {
- this.$input.select();
+OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
+ var textRange, isBackwards, start, end,
+ input = this.$input[ 0 ];
+
+ to = to || from;
+
+ isBackwards = to < from;
+ start = isBackwards ? to : from;
+ end = isBackwards ? from : to;
+
+ this.focus();
+
+ if ( input.setSelectionRange ) {
+ input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
+ } else if ( input.createTextRange ) {
+ // IE 8 and below
+ textRange = input.createTextRange();
+ textRange.collapse( true );
+ textRange.moveStart( 'character', start );
+ textRange.moveEnd( 'character', end - start );
+ textRange.select();
+ }
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.
+ *
+ * This could differ from the length of #getValue if the
+ * value gets filtered
+ *
+ * @return {number} Input length
+ */
+OO.ui.TextInputWidget.prototype.getInputLength = function () {
+ return this.$input[ 0 ].value.length;
+};
+
+/**
+ * Focus the input and select the entire text.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.select = function () {
+ return this.selectRange( 0, this.getInputLength() );
+};
+
+/**
+ * Focus the input and move the cursor to the start.
+ *
+ * @chainable
+ */
+OO.ui.TextInputWidget.prototype.moveCursorToStart = function () {
+ return this.selectRange( 0 );
+};
+
/**
* Focus the input and move the cursor to the end.
+ *
+ * @chainable
*/
OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
- var textRange,
- element = this.$input[ 0 ];
- this.focus();
- if ( element.selectionStart !== undefined ) {
- element.selectionStart = element.selectionEnd = element.value.length;
- } else if ( element.createTextRange ) {
- // IE 8 and below
- textRange = element.createTextRange();
- textRange.collapse( false );
- textRange.select();
- }
+ return this.selectRange( this.getInputLength() );
};
/**
.toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
.toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
+ this.valCache = null;
+ this.scrollWidth = null;
+ this.adjustSize();
this.positionLabel();
return this;
rtl = this.$element.css( 'direction' ) === 'rtl';
property = after === rtl ? 'padding-left' : 'padding-right';
- this.$input.css( property, this.$label.outerWidth( true ) );
+ this.$input.css( property, this.$label.outerWidth( true ) + ( after ? this.scrollWidth : 0 ) );
return this;
};
-/**
- * @inheritdoc
- */
-OO.ui.TextInputWidget.prototype.gatherPreInfuseState = function ( node ) {
- var
- state = OO.ui.TextInputWidget.parent.prototype.gatherPreInfuseState.call( this, node ),
- $input = $( node ).find( '.oo-ui-inputWidget-input' );
- state.$input = $input; // shortcut for performance, used in InputWidget
- if ( this.multiline ) {
- state.scrollTop = $input.scrollTop();
- }
- return state;
-};
-
/**
* @inheritdoc
*/
};
/**
- * 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
}
};
-/*!
- * Deprecated aliases for classes in the `OO.ui.mixin` namespace.
- */
-
-/**
- * @inheritdoc OO.ui.mixin.ButtonElement
- * @deprecated Use {@link OO.ui.mixin.ButtonElement} instead.
- */
-OO.ui.ButtonElement = OO.ui.mixin.ButtonElement;
-
-/**
- * @inheritdoc OO.ui.mixin.ClippableElement
- * @deprecated Use {@link OO.ui.mixin.ClippableElement} instead.
- */
-OO.ui.ClippableElement = OO.ui.mixin.ClippableElement;
-
-/**
- * @inheritdoc OO.ui.mixin.DraggableElement
- * @deprecated Use {@link OO.ui.mixin.DraggableElement} instead.
- */
-OO.ui.DraggableElement = OO.ui.mixin.DraggableElement;
-
-/**
- * @inheritdoc OO.ui.mixin.DraggableGroupElement
- * @deprecated Use {@link OO.ui.mixin.DraggableGroupElement} instead.
- */
-OO.ui.DraggableGroupElement = OO.ui.mixin.DraggableGroupElement;
-
-/**
- * @inheritdoc OO.ui.mixin.FlaggedElement
- * @deprecated Use {@link OO.ui.mixin.FlaggedElement} instead.
- */
-OO.ui.FlaggedElement = OO.ui.mixin.FlaggedElement;
-
-/**
- * @inheritdoc OO.ui.mixin.GroupElement
- * @deprecated Use {@link OO.ui.mixin.GroupElement} instead.
- */
-OO.ui.GroupElement = OO.ui.mixin.GroupElement;
-
-/**
- * @inheritdoc OO.ui.mixin.GroupWidget
- * @deprecated Use {@link OO.ui.mixin.GroupWidget} instead.
- */
-OO.ui.GroupWidget = OO.ui.mixin.GroupWidget;
-
-/**
- * @inheritdoc OO.ui.mixin.IconElement
- * @deprecated Use {@link OO.ui.mixin.IconElement} instead.
- */
-OO.ui.IconElement = OO.ui.mixin.IconElement;
-
-/**
- * @inheritdoc OO.ui.mixin.IndicatorElement
- * @deprecated Use {@link OO.ui.mixin.IndicatorElement} instead.
- */
-OO.ui.IndicatorElement = OO.ui.mixin.IndicatorElement;
-
-/**
- * @inheritdoc OO.ui.mixin.ItemWidget
- * @deprecated Use {@link OO.ui.mixin.ItemWidget} instead.
- */
-OO.ui.ItemWidget = OO.ui.mixin.ItemWidget;
-
-/**
- * @inheritdoc OO.ui.mixin.LabelElement
- * @deprecated Use {@link OO.ui.mixin.LabelElement} instead.
- */
-OO.ui.LabelElement = OO.ui.mixin.LabelElement;
-
-/**
- * @inheritdoc OO.ui.mixin.LookupElement
- * @deprecated Use {@link OO.ui.mixin.LookupElement} instead.
- */
-OO.ui.LookupElement = OO.ui.mixin.LookupElement;
-
-/**
- * @inheritdoc OO.ui.mixin.PendingElement
- * @deprecated Use {@link OO.ui.mixin.PendingElement} instead.
- */
-OO.ui.PendingElement = OO.ui.mixin.PendingElement;
-
-/**
- * @inheritdoc OO.ui.mixin.PopupElement
- * @deprecated Use {@link OO.ui.mixin.PopupElement} instead.
- */
-OO.ui.PopupElement = OO.ui.mixin.PopupElement;
-
-/**
- * @inheritdoc OO.ui.mixin.TabIndexedElement
- * @deprecated Use {@link OO.ui.mixin.TabIndexedElement} instead.
- */
-OO.ui.TabIndexedElement = OO.ui.mixin.TabIndexedElement;
-
-/**
- * @inheritdoc OO.ui.mixin.TitledElement
- * @deprecated Use {@link OO.ui.mixin.TitledElement} instead.
- */
-OO.ui.TitledElement = OO.ui.mixin.TitledElement;
-
}( OO ) );