X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=resources%2Flib%2Foojs-ui%2Foojs-ui.js;h=d42139e8921e742c2818603160012b9d740a38d6;hb=0b8f48c5353bb12faef3546c80a76aea6d3956ea;hp=94ae50e0cd6c2243292e1f13e9306adb4e20d0bd;hpb=eae1f41e72a9a0b67bfda9c7c6f58e6887e023bb;p=lhc%2Fweb%2Fwiklou.git diff --git a/resources/lib/oojs-ui/oojs-ui.js b/resources/lib/oojs-ui/oojs-ui.js index 94ae50e0cd..d42139e892 100644 --- a/resources/lib/oojs-ui/oojs-ui.js +++ b/resources/lib/oojs-ui/oojs-ui.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.1.0-pre (1829141230) + * OOjs UI v0.2.3 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2014 OOjs Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2014-10-15T22:49:59Z + * Date: 2014-11-26T23:37:00Z */ ( function ( OO ) { @@ -93,6 +93,30 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { return undefined; }; +/** + * Check if a node is contained within another node + * + * Similar to jQuery#contains except a list of containers can be supplied + * and a boolean argument allows you to include the container in the match list + * + * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in + * @param {HTMLElement} contained Node to find + * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants + * @return {boolean} The node is in the list of target nodes + */ +OO.ui.contains = function ( containers, contained, matchContainers ) { + var i; + if ( !Array.isArray( containers ) ) { + containers = [ containers ]; + } + for ( i = containers.length - 1; i >= 0; i-- ) { + if ( ( matchContainers && contained === containers[i] ) || $.contains( containers[i], contained ) ) { + return true; + } + } + return false; +}; + ( function () { /** * Message store for the default implementation of OO.ui.msg @@ -111,6 +135,10 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { 'ooui-outline-control-remove': 'Remove item', // Label for the toolbar group that contains a list of all other available tools 'ooui-toolbar-more': 'More', + // Label for the fake tool that expands the full list of tools in a toolbar group + 'ooui-toolgroup-expand': 'More', + // Label for the fake tool that collapses the full list of tools in a toolbar group + 'ooui-toolgroup-collapse': 'Fewer', // Default label for the accept button of a confirmation dialog 'ooui-dialog-message-accept': 'OK', // Default label for the reject button of a confirmation dialog @@ -119,8 +147,10 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { 'ooui-dialog-process-error': 'Something went wrong', // Label for process dialog dismiss error button, visible when describing errors 'ooui-dialog-process-dismiss': 'Dismiss', - // Label for process dialog retry action button, visible when describing recoverable errors - 'ooui-dialog-process-retry': 'Try again' + // Label for process dialog retry action button, visible when describing only recoverable errors + 'ooui-dialog-process-retry': 'Try again', + // Label for process dialog retry action button, visible when describing only warnings + 'ooui-dialog-process-continue': 'Continue' }; /** @@ -196,9 +226,10 @@ OO.ui.getLocalValue = function ( obj, lang, fallback ) { * * @constructor * @param {Object} [config] Configuration options + * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element */ OO.ui.PendingElement = function OoUiPendingElement( config ) { - // Config initialisation + // Configuration initialization config = config || {}; // Properties @@ -283,7 +314,7 @@ OO.ui.PendingElement.prototype.popPending = function () { * @param {Object} [config] Configuration options */ OO.ui.ActionSet = function OoUiActionSet( config ) { - // Configuration intialization + // Configuration initialization config = config || {}; // Mixin constructors @@ -625,7 +656,7 @@ OO.ui.ActionSet.prototype.clear = function () { /** * Organize actions. * - * This is called whenver organized information is requested. It will only reorganize the actions + * This is called whenever organized information is requested. It will only reorganize the actions * if something has changed since the last time it ran. * * @private @@ -642,7 +673,7 @@ OO.ui.ActionSet.prototype.organize = function () { for ( i = 0, iLen = this.list.length; i < iLen; i++ ) { action = this.list[i]; if ( action.isVisible() ) { - // Populate catgeories + // Populate categories for ( category in this.categories ) { if ( !this.categorized[category] ) { this.categorized[category] = {}; @@ -689,9 +720,10 @@ OO.ui.ActionSet.prototype.organize = function () { * @constructor * @param {Object} [config] Configuration options * @cfg {Function} [$] jQuery for the frame the widget is in - * @cfg {string[]} [classes] CSS class names + * @cfg {string[]} [classes] CSS class names to add * @cfg {string} [text] Text to insert * @cfg {jQuery} [$content] Content elements to append (after text) + * @cfg {Mixed} [data] Element data */ OO.ui.Element = function OoUiElement( config ) { // Configuration initialization @@ -699,11 +731,10 @@ OO.ui.Element = function OoUiElement( config ) { // Properties this.$ = config.$ || OO.ui.Element.getJQuery( document ); + this.data = config.data; this.$element = this.$( this.$.context.createElement( this.getTagName() ) ); this.elementGroup = null; - this.debouncedUpdateThemeClassesHandler = OO.ui.bind( - this.debouncedUpdateThemeClasses, this - ); + this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this ); this.updateThemeClassesPending = false; // Initialization @@ -727,7 +758,7 @@ OO.initClass( OO.ui.Element ); /** * HTML tag name. * - * This may be ignored if getTagName is overridden. + * This may be ignored if #getTagName is overridden. * * @static * @inheritable @@ -798,7 +829,7 @@ OO.ui.Element.getWindow = function ( obj ) { * * @static * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for - * @return {string} Text direction, either `ltr` or `rtl` + * @return {string} Text direction, either 'ltr' or 'rtl' */ OO.ui.Element.getDir = function ( obj ) { var isDoc, isWin; @@ -865,15 +896,35 @@ OO.ui.Element.getFrameOffset = function ( from, to, offset ) { /** * Get the offset between two elements. * + * The two elements may be in a different frame, but in that case the frame $element is in must + * be contained in the frame $anchor is in. + * * @static - * @param {jQuery} $from - * @param {jQuery} $to + * @param {jQuery} $element Element whose position to get + * @param {jQuery} $anchor Element to get $element's position relative to * @return {Object} Translated position coordinates, containing top and left properties */ -OO.ui.Element.getRelativePosition = function ( $from, $to ) { - var from = $from.offset(), - to = $to.offset(); - return { top: Math.round( from.top - to.top ), left: Math.round( from.left - to.left ) }; +OO.ui.Element.getRelativePosition = function ( $element, $anchor ) { + var iframe, iframePos, + pos = $element.offset(), + anchorPos = $anchor.offset(), + elementDocument = this.getDocument( $element ), + anchorDocument = this.getDocument( $anchor ); + + // If $element isn't in the same document as $anchor, traverse up + while ( elementDocument !== anchorDocument ) { + iframe = elementDocument.defaultView.frameElement; + if ( !iframe ) { + throw new Error( '$element frame is not contained in $anchor frame' ); + } + iframePos = $( iframe ).offset(); + pos.left += iframePos.left; + pos.top += iframePos.top; + elementDocument = iframe.ownerDocument; + } + pos.left -= anchorPos.left; + pos.top -= anchorPos.top; + return pos; }; /** @@ -989,7 +1040,7 @@ OO.ui.Element.getClosestScrollableContainer = function ( el, dimension ) { * * @static * @param {HTMLElement} el Element to scroll into view - * @param {Object} [config={}] Configuration config + * @param {Object} [config] Configuration options * @param {string} [config.duration] jQuery animation duration value * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit * to scroll in both directions @@ -1086,10 +1137,50 @@ OO.ui.Element.offDOMEvent = function ( el, event, callback ) { /* Methods */ +/** + * Get element data. + * + * @return {Mixed} Element data + */ +OO.ui.Element.prototype.getData = function () { + return this.data; +}; + +/** + * Set element data. + * + * @param {Mixed} Element data + * @chainable + */ +OO.ui.Element.prototype.setData = function ( data ) { + this.data = data; + return this; +}; + +/** + * Check if element supports one or more methods. + * + * @param {string|string[]} methods Method or list of methods to check + * @return {boolean} All methods are supported + */ +OO.ui.Element.prototype.supports = function ( methods ) { + var i, len, + support = 0; + + methods = $.isArray( methods ) ? methods : [ methods ]; + for ( i = 0, len = methods.length; i < len; i++ ) { + if ( $.isFunction( this[methods[i]] ) ) { + support++; + } + } + + return methods.length === support; +}; + /** * Update the theme-provided classes. * - * @localdoc This is called in element mixins and widget classes anytime state changes. + * @localdoc This is called in element mixins and widget classes any time state changes. * Updating is debounced, minimizing overhead of changing multiple attributes and * guaranteeing that theme updates do not occur within an element's constructor */ @@ -1175,7 +1266,7 @@ OO.ui.Element.prototype.setElementGroup = function ( group ) { /** * Scroll element into view. * - * @param {Object} [config={}] + * @param {Object} [config] Configuration options */ OO.ui.Element.prototype.scrollElementIntoView = function ( config ) { return OO.ui.Element.scrollIntoView( this.$element[0], config ); @@ -1215,7 +1306,7 @@ OO.ui.Element.prototype.offDOMEvent = function ( event, callback ) { * @param {Object} [config] Configuration options */ OO.ui.Layout = function OoUiLayout( config ) { - // Initialize config + // Configuration initialization config = config || {}; // Parent constructor @@ -1287,7 +1378,7 @@ OO.mixinClass( OO.ui.Widget, OO.EventEmitter ); /** * Check if the widget is disabled. * - * @param {boolean} Button is disabled + * @return {boolean} Button is disabled */ OO.ui.Widget.prototype.isDisabled = function () { return this.disabled; @@ -1382,8 +1473,8 @@ OO.ui.Widget.prototype.updateDisabled = function () { * * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and - * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchonous - * processing can complete. Always assume window processes are executed asychronously. See + * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchronous + * processing can complete. Always assume window processes are executed asynchronously. See * OO.ui.Process for more details about how to work with processes. Some events, as well as the * #open and #close methods, provide promises which are resolved when the window enters a new state. * @@ -1394,7 +1485,6 @@ OO.ui.Widget.prototype.updateDisabled = function () { * @param {Object} [config] Configuration options * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to * use #static-size - * @fires initialize */ OO.ui.Window = function OoUiWindow( config ) { // Configuration initialization @@ -1426,7 +1516,7 @@ OO.ui.Window = function OoUiWindow( config ) { this.$frame.addClass( 'oo-ui-window-frame' ); this.$overlay.addClass( 'oo-ui-window-overlay' ); - // NOTE: Additional intitialization will occur when #setManager is called + // NOTE: Additional initialization will occur when #setManager is called }; /* Setup */ @@ -1666,11 +1756,20 @@ OO.ui.Window.prototype.getSize = function () { * @return {number} Content height */ OO.ui.Window.prototype.getContentHeight = function () { - // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements - var bodyHeight, oldHeight = this.$frame[0].style.height; - this.$frame[0].style.height = '1px'; + // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements. + // Disable transitions first, otherwise we'll get values from when the window was animating. + var bodyHeight, oldHeight, oldTransition, + styleObj = this.$frame[0].style; + oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition || + styleObj.MozTransition || styleObj.WebkitTransition; + styleObj.transition = styleObj.OTransition = styleObj.MsTransition = + styleObj.MozTransition = styleObj.WebkitTransition = 'none'; + oldHeight = styleObj.height; + styleObj.height = '1px'; bodyHeight = this.getBodyHeight(); - this.$frame[0].style.height = oldHeight; + styleObj.height = oldHeight; + styleObj.transition = styleObj.OTransition = styleObj.MsTransition = + styleObj.MozTransition = styleObj.WebkitTransition = oldTransition; return Math.round( // Add buffer for border @@ -1772,11 +1871,11 @@ OO.ui.Window.prototype.getTeardownProcess = function () { /** * Toggle visibility of window. * - * If the window is isolated and hasn't fully loaded yet, the visiblity property will be used + * If the window is isolated and hasn't fully loaded yet, the visibility property will be used * instead of display. * * @param {boolean} [show] Make window visible, omit to toggle visibility - * @fires visible + * @fires toggle * @chainable */ OO.ui.Window.prototype.toggle = function ( show ) { @@ -1903,7 +2002,7 @@ OO.ui.Window.prototype.initialize = function () { this.$innerOverlay = this.$( '
' ); // Events - this.$element.on( 'mousedown', OO.ui.bind( this.onMouseDown, this ) ); + this.$element.on( 'mousedown', this.onMouseDown.bind( this ) ); // Initialization this.$head.addClass( 'oo-ui-window-head' ); @@ -1945,7 +2044,7 @@ OO.ui.Window.prototype.close = function ( data ) { /** * Setup window. * - * This is called by OO.ui.WindowManager durring window opening, and should not be called directly + * This is called by OO.ui.WindowManager during window opening, and should not be called directly * by other systems. * * @param {Object} [data] Window opening data @@ -1970,7 +2069,7 @@ OO.ui.Window.prototype.setup = function ( data ) { /** * Ready window. * - * This is called by OO.ui.WindowManager durring window opening, and should not be called directly + * This is called by OO.ui.WindowManager during window opening, and should not be called directly * by other systems. * * @param {Object} [data] Window opening data @@ -1994,7 +2093,7 @@ OO.ui.Window.prototype.ready = function ( data ) { /** * Hold window. * - * This is called by OO.ui.WindowManager durring window closing, and should not be called directly + * This is called by OO.ui.WindowManager during window closing, and should not be called directly * by other systems. * * @param {Object} [data] Window closing data @@ -2025,7 +2124,7 @@ OO.ui.Window.prototype.hold = function ( data ) { /** * Teardown window. * - * This is called by OO.ui.WindowManager durring window closing, and should not be called directly + * This is called by OO.ui.WindowManager during window closing, and should not be called directly * by other systems. * * @param {Object} [data] Window closing data @@ -2037,7 +2136,7 @@ OO.ui.Window.prototype.teardown = function ( data ) { this.getTeardownProcess( data ).execute().done( function () { // Force redraw by asking the browser to measure the elements' widths - win.$element.removeClass( 'oo-ui-window-setup' ).width(); + win.$element.removeClass( 'oo-ui-window-load oo-ui-window-setup' ).width(); win.$content.removeClass( 'oo-ui-window-content-setup' ).width(); win.$element.hide(); win.visible = false; @@ -2050,10 +2149,9 @@ OO.ui.Window.prototype.teardown = function ( data ) { /** * Load the frame contents. * - * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned - * promise will be resolved. Calling while loading will return a promise but not trigger a new - * loading cycle. Calling after loading is complete will return a promise that's already been - * resolved. + * Once the iframe's stylesheets are loaded the returned promise will be resolved. Calling while + * loading will return a promise but not trigger a new loading cycle. Calling after loading is + * complete will return a promise that's already been resolved. * * Sounds simple right? Read on... * @@ -2082,12 +2180,13 @@ OO.ui.Window.prototype.teardown = function ( data ) { * All this stylesheet injection and polling magic is in #transplantStyles. * * @return {jQuery.Promise} Promise resolved when loading is complete - * @fires load */ OO.ui.Window.prototype.load = function () { var sub, doc, loading, win = this; + this.$element.addClass( 'oo-ui-window-load' ); + // Non-isolated windows are already "loaded" if ( !this.loading && !this.isolated ) { this.loading = $.Deferred().resolve(); @@ -2185,6 +2284,7 @@ OO.ui.Dialog = function OoUiDialog( config ) { this.actions = new OO.ui.ActionSet(); this.attachedActions = []; this.currentAction = null; + this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this ); // Events this.actions.connect( this, { @@ -2348,6 +2448,10 @@ OO.ui.Dialog.prototype.getSetupProcess = function ( data ) { ); } this.actions.add( items ); + + if ( this.constructor.static.escapable ) { + this.$document.on( 'keydown', this.onDocumentKeyDownHandler ); + } }, this ); }; @@ -2358,6 +2462,10 @@ OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) { // Parent method return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data ) .first( function () { + if ( this.constructor.static.escapable ) { + this.$document.off( 'keydown', this.onDocumentKeyDownHandler ); + } + this.actions.clear(); this.currentAction = null; }, this ); @@ -2373,11 +2481,6 @@ OO.ui.Dialog.prototype.initialize = function () { // Properties this.title = new OO.ui.LabelWidget( { $: this.$ } ); - // Events - if ( this.constructor.static.escapable ) { - this.$document.on( 'keydown', OO.ui.bind( this.onDocumentKeyDown, this ) ); - } - // Initialization this.$content.addClass( 'oo-ui-dialog-content' ); this.setPendingElement( this.$head ); @@ -2415,7 +2518,7 @@ OO.ui.Dialog.prototype.detachActions = function () { OO.ui.Dialog.prototype.executeAction = function ( action ) { this.pushPending(); return this.getActionProcess( action ).execute() - .always( OO.ui.bind( this.popPending, this ) ); + .always( this.popPending.bind( this ) ); }; /** @@ -2487,10 +2590,10 @@ OO.ui.WindowManager = function OoUiWindowManager( config ) { this.$ariaHidden = null; this.requestedSize = null; this.onWindowResizeTimeout = null; - this.onWindowResizeHandler = OO.ui.bind( this.onWindowResize, this ); - this.afterWindowResizeHandler = OO.ui.bind( this.afterWindowResize, this ); - this.onWindowMouseWheelHandler = OO.ui.bind( this.onWindowMouseWheel, this ); - this.onDocumentKeyDownHandler = OO.ui.bind( this.onDocumentKeyDown, this ); + this.onWindowResizeHandler = this.onWindowResize.bind( this ); + this.afterWindowResizeHandler = this.afterWindowResize.bind( this ); + this.onWindowMouseWheelHandler = this.onWindowMouseWheel.bind( this ); + this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this ); // Initialization this.$element @@ -2604,8 +2707,10 @@ OO.ui.WindowManager.prototype.afterWindowResize = function () { * * @param {jQuery.Event} e Mouse wheel event */ -OO.ui.WindowManager.prototype.onWindowMouseWheel = function () { - return false; +OO.ui.WindowManager.prototype.onWindowMouseWheel = function ( e ) { + // Kill all events in the parent window if the child window is isolated, + // or if the event didn't come from the child window + return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame[0], e.target ) ); }; /** @@ -2623,8 +2728,9 @@ OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) { case OO.ui.Keys.UP: case OO.ui.Keys.RIGHT: case OO.ui.Keys.DOWN: - // Prevent any key events that might cause scrolling - return false; + // Kill all events in the parent window if the child window is isolated, + // or if the event didn't come from the child window + return !( this.shouldIsolate() || !$.contains( this.getCurrentWindow().$frame[0], e.target ) ); } }; @@ -2808,13 +2914,10 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) { // Window opening if ( opening.state() !== 'rejected' ) { - // Begin loading the window if it's not loading or loaded already - may take noticable time - // and we want to do this in paralell with any other preparatory actions - if ( !win.isLoading() && !win.isLoaded() ) { - // Finish initializing the window (must be done after manager is attached to DOM) + if ( !win.getManager() ) { win.setManager( this ); - preparing.push( win.load() ); } + preparing.push( win.load() ); if ( this.closing ) { // If a window is currently closing, wait for it to complete @@ -2866,7 +2969,7 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { var manager = this, preparing = [], closing = $.Deferred(), - opened = this.opened; + opened; // Argument handling if ( typeof win === 'string' ) { @@ -2903,6 +3006,7 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { manager.closing = closing; manager.preparingToClose = null; manager.emit( 'closing', win, closing, data ); + opened = manager.opened; manager.opened = null; opened.resolve( closing.promise(), data ); setTimeout( function () { @@ -2984,7 +3088,7 @@ OO.ui.WindowManager.prototype.removeWindows = function ( names ) { if ( !win ) { throw new Error( 'Cannot remove window' ); } - promises.push( this.closeWindow( name ).then( OO.ui.bind( cleanup, null, name, win ) ) ); + promises.push( this.closeWindow( name ).then( cleanup.bind( null, name, win ) ) ); } return $.when.apply( $, promises ); @@ -3093,7 +3197,7 @@ OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) { .attr( 'aria-hidden', '' ); } } else if ( this.$ariaHidden ) { - // Restore screen reader visiblity + // Restore screen reader visibility this.$ariaHidden.removeAttr( 'aria-hidden' ); this.$ariaHidden = null; } @@ -3120,6 +3224,7 @@ OO.ui.WindowManager.prototype.destroy = function () { * @param {string|jQuery} message Description of error * @param {Object} [config] Configuration options * @cfg {boolean} [recoverable=true] Error is recoverable + * @cfg {boolean} [warning=false] Whether this error is a warning or not. */ OO.ui.Error = function OoUiElement( message, config ) { // Configuration initialization @@ -3128,6 +3233,7 @@ OO.ui.Error = function OoUiElement( message, config ) { // Properties this.message = message instanceof jQuery ? message : String( message ); this.recoverable = config.recoverable === undefined || !!config.recoverable; + this.warning = !!config.warning; }; /* Setup */ @@ -3145,6 +3251,15 @@ OO.ui.Error.prototype.isRecoverable = function () { return this.recoverable; }; +/** + * Check if the error is a warning + * + * @return {boolean} Error is warning + */ +OO.ui.Error.prototype.isWarning = function () { + return this.warning; +}; + /** * Get error message as DOM nodes. * @@ -3342,7 +3457,15 @@ OO.inheritClass( OO.ui.ToolFactory, OO.Factory ); /* Methods */ -/** */ +/** + * Get tools from the factory + * + * @param {Array} include Included tools + * @param {Array} exclude Excluded tools + * @param {Array} promote Promoted tools + * @param {Array} demote Demoted tools + * @return {string[]} List of tools + */ OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) { var i, len, included, promoted, demoted, auto = [], @@ -3486,7 +3609,7 @@ OO.ui.ToolGroupFactory.static.getDefaultClasses = function () { * @param {Object} [config] Configuration options */ OO.ui.Theme = function OoUiTheme( config ) { - // Initialize config + // Configuration initialization config = config || {}; }; @@ -3499,8 +3622,8 @@ OO.initClass( OO.ui.Theme ); /** * Get a list of classes to be applied to a widget. * - * @localdoc The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or - * removes, otherwise state transitions will not work properly. + * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes, + * otherwise state transitions will not work properly. * * @param {OO.ui.Element} element Element for which to get classes * @return {Object.} Categorized class names with `on` and `off` lists @@ -3512,9 +3635,9 @@ OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) { /** * Update CSS classes provided by the theme. * - * For elements with theme logic hooks, this should be called anytime there's a state change. + * For elements with theme logic hooks, this should be called any time there's a state change. * - * @param {OO.ui.Element} Element for which to update classes + * @param {OO.ui.Element} element Element for which to update classes * @return {Object.} Categorized class names with `on` and `off` lists */ OO.ui.Theme.prototype.updateElementClasses = function ( element ) { @@ -3538,7 +3661,8 @@ OO.ui.Theme.prototype.updateElementClasses = function ( element ) { * @param {Object} [config] Configuration options * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `` * @cfg {boolean} [framed=true] Render button with a frame - * @cfg {number} [tabIndex=0] Button's tab index, use null to have no tabIndex + * @cfg {number} [tabIndex=0] Button's tab index. Use 0 to use default ordering, use -1 to prevent + * tab focusing. * @cfg {string} [accessKey] Button's access key */ OO.ui.ButtonElement = function OoUiButtonElement( config ) { @@ -3551,8 +3675,8 @@ OO.ui.ButtonElement = function OoUiButtonElement( config ) { this.tabIndex = null; this.accessKey = null; this.active = false; - this.onMouseUpHandler = OO.ui.bind( this.onMouseUp, this ); - this.onMouseDownHandler = OO.ui.bind( this.onMouseDown, this ); + this.onMouseUpHandler = this.onMouseUp.bind( this ); + this.onMouseDownHandler = this.onMouseDown.bind( this ); // Initialization this.$element.addClass( 'oo-ui-buttonElement' ); @@ -3630,7 +3754,7 @@ OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) { if ( this.isDisabled() || e.which !== 1 ) { return false; } - // Restore the tab-index after the button is up to restore the button's accesssibility + // Restore the tab-index after the button is up to restore the button's accessibility this.$button.attr( 'tabindex', this.tabIndex ); this.$element.removeClass( 'oo-ui-buttonElement-pressed' ); // Stop listening for mouseup, since we only needed this once @@ -3733,7 +3857,7 @@ OO.ui.ButtonElement.prototype.setActive = function ( value ) { * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `
` */ OO.ui.GroupElement = function OoUiGroupElement( config ) { - // Configuration + // Configuration initialization config = config || {}; // Properties @@ -3781,6 +3905,51 @@ OO.ui.GroupElement.prototype.getItems = function () { return this.items.slice( 0 ); }; +/** + * Get an item by its data. + * + * Data is compared by a hash of its value. Only the first item with matching data will be returned. + * + * @param {Object} data Item data to search for + * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists + */ +OO.ui.GroupElement.prototype.getItemFromData = function ( data ) { + var i, len, item, + hash = OO.getHash( data ); + + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[i]; + if ( hash === OO.getHash( item.getData() ) ) { + return item; + } + } + + return null; +}; + +/** + * Get items by their data. + * + * Data is compared by a hash of its value. All items with matching data will be returned. + * + * @param {Object} data Item data to search for + * @return {OO.ui.Element[]} Items with equivalent data + */ +OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) { + var i, len, item, + hash = OO.getHash( data ), + items = []; + + for ( i = 0, len = this.items.length; i < len; i++ ) { + item = this.items[i]; + if ( hash === OO.getHash( item.getData() ) ) { + items.push( item ); + } + } + + return items; +}; + /** * Add an aggregate item event. * @@ -3799,7 +3968,7 @@ OO.ui.GroupElement.prototype.aggregate = function ( events ) { groupEvent = events[itemEvent]; // Remove existing aggregated event - if ( itemEvent in this.aggregateItemEvents ) { + if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) { // Don't allow duplicate aggregations if ( groupEvent ) { throw new Error( 'Duplicate item event aggregation for ' + itemEvent ); @@ -3837,9 +4006,9 @@ OO.ui.GroupElement.prototype.aggregate = function ( events ) { /** * Add items. * - * Adding an existing item (by value) will move it. + * Adding an existing item will move it. * - * @param {OO.ui.Element[]} items Item + * @param {OO.ui.Element[]} items Items * @param {number} [index] Index to insert items at * @chainable */ @@ -3906,7 +4075,7 @@ OO.ui.GroupElement.prototype.removeItems = function ( items ) { !$.isEmptyObject( this.aggregateItemEvents ) ) { remove = {}; - if ( itemEvent in this.aggregateItemEvents ) { + if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) { remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ]; } item.disconnect( this, remove ); @@ -3938,7 +4107,7 @@ OO.ui.GroupElement.prototype.clearItems = function () { !$.isEmptyObject( this.aggregateItemEvents ) ) { remove = {}; - if ( itemEvent in this.aggregateItemEvents ) { + if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) { remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ]; } item.disconnect( this, remove ); @@ -3971,7 +4140,7 @@ OO.ui.GroupElement.prototype.clearItems = function () { * @cfg {string} [iconTitle] Icon title text or a function that returns text */ OO.ui.IconElement = function OoUiIconElement( config ) { - // Config intialization + // Configuration initialization config = config || {}; // Properties @@ -4045,7 +4214,7 @@ OO.ui.IconElement.prototype.setIconElement = function ( $icon ) { }; /** - * Set icon. + * Set icon name. * * @param {Object|string|null} icon Symbolic icon name, or map of icon names keyed by language ID; * use the 'default' key to specify the icon to be used when there is no icon in the user's @@ -4101,9 +4270,9 @@ OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) { }; /** - * Get icon. + * Get icon name. * - * @return {string} Icon + * @return {string} Icon name */ OO.ui.IconElement.prototype.getIcon = function () { return this.icon; @@ -4128,7 +4297,7 @@ OO.ui.IconElement.prototype.getIcon = function () { * @cfg {string} [indicatorTitle] Indicator title text or a function that returns text */ OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) { - // Config intialization + // Configuration initialization config = config || {}; // Properties @@ -4153,7 +4322,7 @@ OO.initClass( OO.ui.IndicatorElement ); * * @static * @inheritable - * @property {string|null} Symbolic indicator name or null for no indicator + * @property {string|null} Symbolic indicator name */ OO.ui.IndicatorElement.static.indicator = null; @@ -4192,7 +4361,7 @@ OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) { }; /** - * Set indicator. + * Set indicator name. * * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator * @chainable @@ -4245,9 +4414,9 @@ OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) }; /** - * Get indicator. + * Get indicator name. * - * @return {string} title Symbolic name of indicator + * @return {string} Symbolic name of indicator */ OO.ui.IndicatorElement.prototype.getIndicator = function () { return this.indicator; @@ -4275,7 +4444,7 @@ OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () { * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not. */ OO.ui.LabelElement = function OoUiLabelElement( config ) { - // Config intialization + // Configuration initialization config = config || {}; // Properties @@ -4326,7 +4495,7 @@ OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) { * Set the label. * * An empty string will result in the label being hidden. A string containing only whitespace will - * be converted to a single   + * be converted to a single ` `. * * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or * text; or null for no label @@ -4351,7 +4520,7 @@ OO.ui.LabelElement.prototype.setLabel = function ( label ) { /** * Get the label. * - * @return {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or + * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or * text; or null for no label */ OO.ui.LabelElement.prototype.getLabel = function () { @@ -4393,7 +4562,6 @@ OO.ui.LabelElement.prototype.setLabelContent = function ( label ) { } else { this.$label.empty(); } - this.$label.css( 'display', !label ? 'none' : '' ); }; /** @@ -4441,11 +4609,11 @@ OO.ui.PopupElement.prototype.getPopup = function () { * * @constructor * @param {Object} [config] Configuration options - * @cfg {string[]} [flags=[]] Styling flags, e.g. 'primary', 'destructive' or 'constructive' + * @cfg {string|string[]} [flags] Styling flags, e.g. 'primary', 'destructive' or 'constructive' * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element */ OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) { - // Config initialization + // Configuration initialization config = config || {}; // Properties @@ -4499,7 +4667,7 @@ OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) { /** * Get the names of all flags set. * - * @return {string[]} flags Flag names + * @return {string[]} Flag names */ OO.ui.FlaggedElement.prototype.getFlags = function () { return Object.keys( this.flags ); @@ -4612,10 +4780,11 @@ OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) { * @constructor * @param {Object} [config] Configuration options * @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element - * @cfg {string|Function} [title] Title text or a function that returns text + * @cfg {string|Function} [title] Title text or a function that returns text. If not provided, the + * static property 'title' is used. */ OO.ui.TitledElement = function OoUiTitledElement( config ) { - // Config intialization + // Configuration initialization config = config || {}; // Properties @@ -4721,8 +4890,8 @@ OO.ui.ClippableElement = function OoUiClippableElement( config ) { this.$clippableWindow = null; this.idealWidth = null; this.idealHeight = null; - this.onClippableContainerScrollHandler = OO.ui.bind( this.clip, this ); - this.onClippableWindowResizeHandler = OO.ui.bind( this.clip, this ); + this.onClippableContainerScrollHandler = this.clip.bind( this ); + this.onClippableWindowResizeHandler = this.clip.bind( this ); // Initialization this.setClippableElement( config.$clippable || this.$element ); @@ -4910,7 +5079,7 @@ OO.ui.ClippableElement.prototype.clip = function () { * @cfg {string|Function} [title] Title text or a function that returns text */ OO.ui.Tool = function OoUiTool( toolGroup, config ) { - // Config intialization + // Configuration initialization config = config || {}; // Parent constructor @@ -4925,6 +5094,8 @@ OO.ui.Tool = function OoUiTool( toolGroup, config ) { this.toolbar = this.toolGroup.getToolbar(); this.active = false; this.$title = this.$( '' ); + this.$titleText = this.$( '' ); + this.$accel = this.$( '' ); this.$link = this.$( '' ); this.title = null; @@ -4932,7 +5103,18 @@ OO.ui.Tool = function OoUiTool( toolGroup, config ) { this.toolbar.connect( this, { updateState: 'onUpdateState' } ); // Initialization - this.$title.addClass( 'oo-ui-tool-title' ); + this.$titleText.addClass( 'oo-ui-tool-title-text' ); + this.$accel + .addClass( 'oo-ui-tool-accel' ) + .prop( { + // This may need to be changed if the key names are ever localized, + // but for now they are essentially written in English + dir: 'ltr', + lang: 'en' + } ); + this.$title + .addClass( 'oo-ui-tool-title' ) + .append( this.$titleText, this.$accel ); this.$link .addClass( 'oo-ui-tool-link' ) .append( this.$icon, this.$title ) @@ -5064,7 +5246,7 @@ OO.ui.Tool.prototype.onSelect = function () { /** * Check if the button is active. * - * @param {boolean} Button is active + * @return {boolean} Button is active */ OO.ui.Tool.prototype.isActive = function () { return this.active; @@ -5123,13 +5305,8 @@ OO.ui.Tool.prototype.updateTitle = function () { accel = this.toolbar.getToolAccelerator( this.constructor.static.name ), tooltipParts = []; - this.$title.empty() - .text( this.title ) - .append( - this.$( '' ) - .addClass( 'oo-ui-tool-accel' ) - .text( accel ) - ); + this.$titleText.text( this.title ); + this.$accel.text( accel ); if ( titleTooltips && typeof this.title === 'string' && this.title.length ) { tooltipParts.push( this.title ); @@ -5190,16 +5367,16 @@ OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) { // Events this.$element .add( this.$bar ).add( this.$group ).add( this.$actions ) - .on( 'mousedown touchstart', OO.ui.bind( this.onPointerDown, this ) ); + .on( 'mousedown touchstart', this.onPointerDown.bind( this ) ); // Initialization this.$group.addClass( 'oo-ui-toolbar-tools' ); - this.$bar.addClass( 'oo-ui-toolbar-bar' ).append( this.$group ); if ( config.actions ) { - this.$actions.addClass( 'oo-ui-toolbar-actions' ); - this.$bar.append( this.$actions ); + this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) ); } - this.$bar.append( '
' ); + this.$bar + .addClass( 'oo-ui-toolbar-bar' ) + .append( this.$group, '
' ); if ( config.shadow ) { this.$bar.append( '
' ); } @@ -5402,14 +5579,14 @@ OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) { this.exclude = config.exclude || []; this.promote = config.promote || []; this.demote = config.demote || []; - this.onCapturedMouseUpHandler = OO.ui.bind( this.onCapturedMouseUp, this ); + this.onCapturedMouseUpHandler = this.onCapturedMouseUp.bind( this ); // Events this.$element.on( { - 'mousedown touchstart': OO.ui.bind( this.onPointerDown, this ), - 'mouseup touchend': OO.ui.bind( this.onPointerUp, this ), - mouseover: OO.ui.bind( this.onMouseOver, this ), - mouseout: OO.ui.bind( this.onMouseOut, this ) + 'mousedown touchstart': this.onPointerDown.bind( this ), + 'mouseup touchend': this.onPointerUp.bind( this ), + mouseover: this.onMouseOver.bind( this ), + mouseout: this.onMouseOut.bind( this ) } ); this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } ); this.aggregate( { disable: 'itemDisable' } ); @@ -5722,7 +5899,7 @@ OO.ui.MessageDialog.static.verbose = false; * Dialog title. * * A confirmation dialog's title should describe what the progressive action will do. An alert - * dialog's title should describe what event occured. + * dialog's title should describe what event occurred. * * @static * inheritable @@ -5732,7 +5909,7 @@ OO.ui.MessageDialog.static.title = null; /** * A confirmation dialog's message should describe the consequences of the progressive action. An - * alert dialog's message should describe why the event occured. + * alert dialog's message should describe why the event occurred. * * @static * inheritable @@ -5752,7 +5929,7 @@ OO.ui.MessageDialog.static.actions = [ */ OO.ui.MessageDialog.prototype.onActionResize = function ( action ) { this.fitActions(); - return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action ); + return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action ); }; /** @@ -5819,7 +5996,45 @@ OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) { * @inheritdoc */ OO.ui.MessageDialog.prototype.getBodyHeight = function () { - return Math.round( this.text.$element.outerHeight( true ) ); + var bodyHeight, oldOverflow, + $scrollable = this.container.$element; + + oldOverflow = $scrollable[0].style.overflow; + $scrollable[0].style.overflow = 'hidden'; + + // Force… ugh… something to happen + $scrollable.contents().hide(); + $scrollable.height(); + $scrollable.contents().show(); + + bodyHeight = Math.round( this.text.$element.outerHeight( true ) ); + $scrollable[0].style.overflow = oldOverflow; + + return bodyHeight; +}; + +/** + * @inheritdoc + */ +OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) { + var $scrollable = this.container.$element; + OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim ); + + // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced. + // Need to do it after transition completes (250ms), add 50ms just in case. + setTimeout( function () { + var oldOverflow = $scrollable[0].style.overflow; + $scrollable[0].style.overflow = 'hidden'; + + // Force… ugh… something to happen + $scrollable.contents().hide(); + $scrollable.height(); + $scrollable.contents().show(); + + $scrollable[0].style.overflow = oldOverflow; + }, 300 ); + + return this; }; /** @@ -5878,10 +6093,9 @@ OO.ui.MessageDialog.prototype.attachActions = function () { special.primary.toggleFramed( false ); } + this.manager.updateWindowSize( this ); this.fitActions(); - if ( !this.isOpening() ) { - this.manager.updateWindowSize( this ); - } + this.$body.css( 'bottom', this.$foot.outerHeight( true ) ); }; @@ -5991,10 +6205,7 @@ OO.ui.ProcessDialog.prototype.initialize = function () { $: this.$, label: OO.ui.msg( 'ooui-dialog-process-dismiss' ) } ); - this.retryButton = new OO.ui.ButtonWidget( { - $: this.$, - label: OO.ui.msg( 'ooui-dialog-process-retry' ) - } ); + this.retryButton = new OO.ui.ButtonWidget( { $: this.$ } ); this.$errors = this.$( '
' ); this.$errorsTitle = this.$( '
' ); @@ -6062,7 +6273,7 @@ OO.ui.ProcessDialog.prototype.attachActions = function () { */ OO.ui.ProcessDialog.prototype.executeAction = function ( action ) { OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action ) - .fail( OO.ui.bind( this.showErrors, this ) ); + .fail( this.showErrors.bind( this ) ); }; /** @@ -6081,19 +6292,23 @@ OO.ui.ProcessDialog.prototype.fitLabel = function () { }; /** - * Handle errors that occured durring accept or reject processes. + * Handle errors that occurred during accept or reject processes. * * @param {OO.ui.Error[]} errors Errors to be handled */ OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) { var i, len, $item, items = [], - recoverable = true; + recoverable = true, + warning = false; for ( i = 0, len = errors.length; i < len; i++ ) { if ( !errors[i].isRecoverable() ) { recoverable = false; } + if ( errors[i].isWarning() ) { + warning = true; + } $item = this.$( '
' ) .addClass( 'oo-ui-processDialog-error' ) .append( errors[i].getMessage() ); @@ -6105,6 +6320,11 @@ OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) { } else { this.currentAction.setDisabled( true ); } + if ( warning ) { + this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) ); + } else { + this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) ); + } this.retryButton.toggle( recoverable ); this.$errorsTitle.after( this.$errorItems ); this.$errors.show().scrollTop( 0 ); @@ -6133,7 +6353,7 @@ OO.ui.ProcessDialog.prototype.hideErrors = function () { * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages */ OO.ui.BookletLayout = function OoUiBookletLayout( config ) { - // Initialize configuration + // Configuration initialization config = config || {}; // Parent constructor @@ -6150,7 +6370,7 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) { if ( this.outlined ) { this.editable = !!config.editable; this.outlineControlsWidget = null; - this.outlineWidget = new OO.ui.OutlineWidget( { $: this.$ } ); + this.outlineSelectWidget = new OO.ui.OutlineSelectWidget( { $: this.$ } ); this.outlinePanel = new OO.ui.PanelLayout( { $: this.$, scrollable: true } ); this.gridLayout = new OO.ui.GridLayout( [ this.outlinePanel, this.stackLayout ], @@ -6159,7 +6379,7 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) { this.outlineVisible = true; if ( this.editable ) { this.outlineControlsWidget = new OO.ui.OutlineControlsWidget( - this.outlineWidget, { $: this.$ } + this.outlineSelectWidget, { $: this.$ } ); } } @@ -6167,11 +6387,11 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) { // Events this.stackLayout.connect( this, { set: 'onStackLayoutSet' } ); if ( this.outlined ) { - this.outlineWidget.connect( this, { select: 'onOutlineWidgetSelect' } ); + this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } ); } if ( this.autoFocus ) { // Event 'focus' does not bubble, but 'focusin' does - this.stackLayout.onDOMEvent( 'focusin', OO.ui.bind( this.onStackLayoutFocus, this ) ); + this.stackLayout.onDOMEvent( 'focusin', this.onStackLayoutFocus.bind( this ) ); } // Initialization @@ -6180,7 +6400,7 @@ OO.ui.BookletLayout = function OoUiBookletLayout( config ) { if ( this.outlined ) { this.outlinePanel.$element .addClass( 'oo-ui-bookletLayout-outlinePanel' ) - .append( this.outlineWidget.$element ); + .append( this.outlineSelectWidget.$element ); if ( this.editable ) { this.outlinePanel.$element .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' ) @@ -6241,28 +6461,46 @@ OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) { * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel */ OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) { - var $input, layout = this; + var layout = this; if ( page ) { page.scrollElementIntoView( { complete: function () { if ( layout.autoFocus ) { - // Set focus to the first input if nothing on the page is focused yet - if ( !page.$element.find( ':focus' ).length ) { - $input = page.$element.find( ':input:first' ); - if ( $input.length ) { - $input[0].focus(); - } - } + layout.focus(); } } } ); } }; +/** + * Focus the first input in the current page. + * + * If no page is selected, the first selectable page will be selected. + * If the focus is already in an element on the current page, nothing will happen. + */ +OO.ui.BookletLayout.prototype.focus = function () { + var $input, page = this.stackLayout.getCurrentItem(); + if ( !page && this.outlined ) { + this.selectFirstSelectablePage(); + page = this.stackLayout.getCurrentItem(); + if ( !page ) { + return; + } + } + // Only change the focus if is not already in the current page + if ( !page.$element.find( ':focus' ).length ) { + $input = page.$element.find( ':input:first' ); + if ( $input.length ) { + $input[0].focus(); + } + } +}; + /** * Handle outline widget select events. * * @param {OO.ui.OptionWidget|null} item Selected item */ -OO.ui.BookletLayout.prototype.onOutlineWidgetSelect = function ( item ) { +OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) { if ( item ) { this.setPage( item.getData() ); } @@ -6327,16 +6565,16 @@ OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) { prev = pages[index - 1]; // Prefer adjacent pages at the same level if ( this.outlined ) { - level = this.outlineWidget.getItemFromData( page.getName() ).getLevel(); + level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel(); if ( prev && - level === this.outlineWidget.getItemFromData( prev.getName() ).getLevel() + level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel() ) { return prev; } if ( next && - level === this.outlineWidget.getItemFromData( next.getName() ).getLevel() + level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel() ) { return next; } @@ -6348,10 +6586,10 @@ OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) { /** * Get the outline widget. * - * @return {OO.ui.OutlineWidget|null} Outline widget, or null if boolet has no outline + * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline */ OO.ui.BookletLayout.prototype.getOutline = function () { - return this.outlineWidget; + return this.outlineSelectWidget; }; /** @@ -6423,15 +6661,15 @@ OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) { name = page.getName(); this.pages[page.getName()] = page; if ( this.outlined ) { - item = new OO.ui.OutlineItemWidget( name, page, { $: this.$ } ); + item = new OO.ui.OutlineOptionWidget( { $: this.$, data: name } ); page.setOutlineItem( item ); items.push( item ); } } if ( this.outlined && items.length ) { - this.outlineWidget.addItems( items, index ); - this.updateOutlineWidget(); + this.outlineSelectWidget.addItems( items, index ); + this.selectFirstSelectablePage(); } this.stackLayout.addItems( pages, index ); this.emit( 'add', pages, index ); @@ -6454,13 +6692,13 @@ OO.ui.BookletLayout.prototype.removePages = function ( pages ) { name = page.getName(); delete this.pages[name]; if ( this.outlined ) { - items.push( this.outlineWidget.getItemFromData( name ) ); + items.push( this.outlineSelectWidget.getItemFromData( name ) ); page.setOutlineItem( null ); } } if ( this.outlined && items.length ) { - this.outlineWidget.removeItems( items ); - this.updateOutlineWidget(); + this.outlineSelectWidget.removeItems( items ); + this.selectFirstSelectablePage(); } this.stackLayout.removeItems( pages ); this.emit( 'remove', pages ); @@ -6481,7 +6719,7 @@ OO.ui.BookletLayout.prototype.clearPages = function () { this.pages = {}; this.currentPageName = null; if ( this.outlined ) { - this.outlineWidget.clearItems(); + this.outlineSelectWidget.clearItems(); for ( i = 0, len = pages.length; i < len; i++ ) { pages[i].setOutlineItem( null ); } @@ -6506,9 +6744,9 @@ OO.ui.BookletLayout.prototype.setPage = function ( name ) { if ( name !== this.currentPageName ) { if ( this.outlined ) { - selectedItem = this.outlineWidget.getSelectedItem(); + selectedItem = this.outlineSelectWidget.getSelectedItem(); if ( selectedItem && selectedItem.getData() !== name ) { - this.outlineWidget.selectItem( this.outlineWidget.getItemFromData( name ) ); + this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) ); } } if ( page ) { @@ -6533,14 +6771,13 @@ OO.ui.BookletLayout.prototype.setPage = function ( name ) { }; /** - * Call this after adding or removing items from the OutlineWidget. + * Select the first selectable page. * * @chainable */ -OO.ui.BookletLayout.prototype.updateOutlineWidget = function () { - // Auto-select first item when nothing is selected anymore - if ( !this.outlineWidget.getSelectedItem() ) { - this.outlineWidget.selectItem( this.outlineWidget.getFirstSelectableItem() ); +OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () { + if ( !this.outlineSelectWidget.getSelectedItem() ) { + this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() ); } return this; @@ -6549,30 +6786,33 @@ OO.ui.BookletLayout.prototype.updateOutlineWidget = function () { /** * Layout made of a field and optional label. * - * @class - * @extends OO.ui.Layout - * @mixins OO.ui.LabelElement - * * Available label alignment modes include: * - left: Label is before the field and aligned away from it, best for when the user will be * scanning for a specific label in a form with many fields * - right: Label is before the field and aligned toward it, best for forms the user is very * familiar with and will tab through field checking quickly to verify which field they are in - * - top: Label is before the field and above it, best for when the use will need to fill out all + * - top: Label is before the field and above it, best for when the user will need to fill out all * fields from top to bottom in a form with few fields * - inline: Label is after the field and aligned toward it, best for small boolean fields like * checkboxes or radio buttons * + * @class + * @extends OO.ui.Layout + * @mixins OO.ui.LabelElement + * * @constructor - * @param {OO.ui.Widget} field Field widget + * @param {OO.ui.Widget} fieldWidget Field widget * @param {Object} [config] Configuration options * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline' * @cfg {string} [help] Explanatory text shown as a '?' icon. */ -OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) { - // Config initialization +OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) { + // Configuration initialization config = $.extend( { align: 'left' }, config ); + // Properties (must be set before parent constructor, which calls #getTagName) + this.fieldWidget = fieldWidget; + // Parent constructor OO.ui.FieldLayout.super.call( this, config ); @@ -6581,7 +6821,6 @@ OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) { // Properties this.$field = this.$( '
' ); - this.field = field; this.align = null; if ( config.help ) { this.popupButtonWidget = new OO.ui.PopupButtonWidget( { @@ -6602,17 +6841,17 @@ OO.ui.FieldLayout = function OoUiFieldLayout( field, config ) { } // Events - if ( this.field instanceof OO.ui.InputWidget ) { - this.$label.on( 'click', OO.ui.bind( this.onLabelClick, this ) ); + if ( this.fieldWidget instanceof OO.ui.InputWidget ) { + this.$label.on( 'click', this.onLabelClick.bind( this ) ); } - this.field.connect( this, { disable: 'onFieldDisable' } ); + this.fieldWidget.connect( this, { disable: 'onFieldDisable' } ); // Initialization this.$element.addClass( 'oo-ui-fieldLayout' ); this.$field .addClass( 'oo-ui-fieldLayout-field' ) - .toggleClass( 'oo-ui-fieldLayout-disable', this.field.isDisabled() ) - .append( this.field.$element ); + .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() ) + .append( this.fieldWidget.$element ); this.setAlignment( config.align ); }; @@ -6623,6 +6862,17 @@ OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement ); /* Methods */ +/** + * @inheritdoc + */ +OO.ui.FieldLayout.prototype.getTagName = function () { + if ( this.fieldWidget instanceof OO.ui.InputWidget ) { + return 'label'; + } else { + return 'div'; + } +}; + /** * Handle field disable events. * @@ -6638,7 +6888,7 @@ OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) { * @param {jQuery.Event} e Mouse click event */ OO.ui.FieldLayout.prototype.onLabelClick = function () { - this.field.simulateLabelClick(); + this.fieldWidget.simulateLabelClick(); return false; }; @@ -6648,12 +6898,13 @@ OO.ui.FieldLayout.prototype.onLabelClick = function () { * @return {OO.ui.Widget} Field widget */ OO.ui.FieldLayout.prototype.getField = function () { - return this.field; + return this.fieldWidget; }; /** * Set the field alignment mode. * + * @private * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline' * @chainable */ @@ -6669,17 +6920,16 @@ OO.ui.FieldLayout.prototype.setAlignment = function ( value ) { } else { this.$element.append( this.$help, this.$label, this.$field ); } - // Set classes + // Set classes. The following classes can be used here: + // * oo-ui-fieldLayout-align-left + // * oo-ui-fieldLayout-align-right + // * oo-ui-fieldLayout-align-top + // * oo-ui-fieldLayout-align-inline if ( this.align ) { this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align ); } + this.$element.addClass( 'oo-ui-fieldLayout-align-' + value ); this.align = value; - // The following classes can be used here: - // oo-ui-fieldLayout-align-left - // oo-ui-fieldLayout-align-right - // oo-ui-fieldLayout-align-top - // oo-ui-fieldLayout-align-inline - this.$element.addClass( 'oo-ui-fieldLayout-align-' + this.align ); } return this; @@ -6692,17 +6942,16 @@ OO.ui.FieldLayout.prototype.setAlignment = function ( value ) { * * @class * @extends OO.ui.Layout - * @mixins OO.ui.LabelElement * @mixins OO.ui.IconElement + * @mixins OO.ui.LabelElement * @mixins OO.ui.GroupElement * * @constructor * @param {Object} [config] Configuration options - * @cfg {string} [icon] Symbolic icon name * @cfg {OO.ui.FieldLayout[]} [items] Items to add */ OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) { - // Config initialization + // Configuration initialization config = config || {}; // Parent constructor @@ -6729,10 +6978,6 @@ OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement ); OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement ); OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement ); -/* Static Properties */ - -OO.ui.FieldsetLayout.static.tagName = 'div'; - /** * Layout with an HTML form. * @@ -6741,6 +6986,9 @@ OO.ui.FieldsetLayout.static.tagName = 'div'; * * @constructor * @param {Object} [config] Configuration options + * @cfg {string} [method] HTML form `method` attribute + * @cfg {string} [action] HTML form `action` attribute + * @cfg {string} [enctype] HTML form `enctype` attribute */ OO.ui.FormLayout = function OoUiFormLayout( config ) { // Configuration initialization @@ -6750,10 +6998,16 @@ OO.ui.FormLayout = function OoUiFormLayout( config ) { OO.ui.FormLayout.super.call( this, config ); // Events - this.$element.on( 'submit', OO.ui.bind( this.onFormSubmit, this ) ); + this.$element.on( 'submit', this.onFormSubmit.bind( this ) ); // Initialization - this.$element.addClass( 'oo-ui-formLayout' ); + this.$element + .addClass( 'oo-ui-formLayout' ) + .attr( { + method: config.method, + action: config.action, + enctype: config.enctype + } ); }; /* Setup */ @@ -6793,12 +7047,12 @@ OO.ui.FormLayout.prototype.onFormSubmit = function () { * @param {OO.ui.PanelLayout[]} panels Panels in the grid * @param {Object} [config] Configuration options * @cfg {number[]} [widths] Widths of columns as ratios - * @cfg {number[]} [heights] Heights of columns as ratios + * @cfg {number[]} [heights] Heights of rows as ratios */ OO.ui.GridLayout = function OoUiGridLayout( panels, config ) { var i, len, widths; - // Config initialization + // Configuration initialization config = config || {}; // Parent constructor @@ -6819,10 +7073,7 @@ OO.ui.GridLayout = function OoUiGridLayout( panels, config ) { this.layout( config.widths || [ 1 ], config.heights || [ 1 ] ); } else { // Arrange in columns by default - widths = []; - for ( i = 0, len = this.panels.length; i < len; i++ ) { - widths[i] = 1; - } + widths = this.panels.map( function () { return 1; } ); this.layout( widths, [ 1 ] ); } }; @@ -6841,10 +7092,6 @@ OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout ); * @event update */ -/* Static Properties */ - -OO.ui.GridLayout.static.tagName = 'div'; - /* Methods */ /** @@ -6894,27 +7141,22 @@ OO.ui.GridLayout.prototype.layout = function ( widths, heights ) { * @fires update */ OO.ui.GridLayout.prototype.update = function () { - var x, y, panel, + var x, y, panel, width, height, dimensions, i = 0, - left = 0, top = 0, - dimensions, - width = 0, - height = 0, + left = 0, cols = this.widths.length, rows = this.heights.length; for ( y = 0; y < rows; y++ ) { height = this.heights[y]; for ( x = 0; x < cols; x++ ) { - panel = this.panels[i]; width = this.widths[x]; + panel = this.panels[i]; dimensions = { width: Math.round( width * 100 ) + '%', height: Math.round( height * 100 ) + '%', - top: Math.round( top * 100 ) + '%', - // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero - visibility: width === 0 || height === 0 ? 'hidden' : '' + top: Math.round( top * 100 ) + '%' }; // If RTL, reverse: if ( OO.ui.Element.getDir( this.$.context ) === 'rtl' ) { @@ -6922,6 +7164,12 @@ OO.ui.GridLayout.prototype.update = function () { } else { dimensions.left = Math.round( left * 100 ) + '%'; } + // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero + if ( width === 0 || height === 0 ) { + dimensions.visibility = 'hidden'; + } else { + dimensions.visibility = ''; + } panel.$element.css( dimensions ); i++; left += width; @@ -6940,10 +7188,10 @@ OO.ui.GridLayout.prototype.update = function () { * * @param {number} x Horizontal position * @param {number} y Vertical position - * @return {OO.ui.PanelLayout} The panel at the given postion + * @return {OO.ui.PanelLayout} The panel at the given position */ OO.ui.GridLayout.prototype.getPanel = function ( x, y ) { - return this.panels[( x * this.widths.length ) + y]; + return this.panels[ ( x * this.widths.length ) + y ]; }; /** @@ -6959,8 +7207,12 @@ OO.ui.GridLayout.prototype.getPanel = function ( x, y ) { * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element */ OO.ui.PanelLayout = function OoUiPanelLayout( config ) { - // Config initialization - config = config || {}; + // Configuration initialization + config = $.extend( { + scrollable: false, + padded: false, + expanded: true + }, config ); // Parent constructor OO.ui.PanelLayout.super.call( this, config ); @@ -6970,12 +7222,10 @@ OO.ui.PanelLayout = function OoUiPanelLayout( config ) { if ( config.scrollable ) { this.$element.addClass( 'oo-ui-panelLayout-scrollable' ); } - if ( config.padded ) { this.$element.addClass( 'oo-ui-panelLayout-padded' ); } - - if ( config.expanded === undefined || config.expanded ) { + if ( config.expanded ) { this.$element.addClass( 'oo-ui-panelLayout-expanded' ); } }; @@ -7045,7 +7295,7 @@ OO.ui.PageLayout.prototype.isActive = function () { /** * Get outline item. * - * @return {OO.ui.OutlineItemWidget|null} Outline item widget + * @return {OO.ui.OutlineOptionWidget|null} Outline item widget */ OO.ui.PageLayout.prototype.getOutlineItem = function () { return this.outlineItem; @@ -7057,9 +7307,9 @@ OO.ui.PageLayout.prototype.getOutlineItem = function () { * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the * outline item as desired; this method is called for setting (with an object) and unsetting * (with null) and overriding methods would have to check the value of `outlineItem` to avoid - * operating on null instead of an OO.ui.OutlineItemWidget object. + * operating on null instead of an OO.ui.OutlineOptionWidget object. * - * @param {OO.ui.OutlineItemWidget|null} outlineItem Outline item widget, null to clear + * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear * @chainable */ OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) { @@ -7075,7 +7325,7 @@ OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) { * * @localdoc Subclasses should override this method to adjust the outline item as desired. * - * @param {OO.ui.OutlineItemWidget} outlineItem Outline item widget to setup + * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup * @chainable */ OO.ui.PageLayout.prototype.setupOutlineItem = function () { @@ -7112,7 +7362,7 @@ OO.ui.PageLayout.prototype.setActive = function ( active ) { * @cfg {OO.ui.Layout[]} [items] Layouts to add */ OO.ui.StackLayout = function OoUiStackLayout( config ) { - // Config initialization + // Configuration initialization config = $.extend( { scrollable: true }, config ); // Parent constructor @@ -7333,13 +7583,13 @@ OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) { // Properties this.active = false; this.dragging = false; - this.onBlurHandler = OO.ui.bind( this.onBlur, this ); + this.onBlurHandler = this.onBlur.bind( this ); this.$handle = this.$( '' ); // Events this.$handle.on( { - 'mousedown touchstart': OO.ui.bind( this.onHandlePointerDown, this ), - 'mouseup touchend': OO.ui.bind( this.onHandlePointerUp, this ) + 'mousedown touchstart': this.onHandlePointerDown.bind( this ), + 'mouseup touchend': this.onHandlePointerUp.bind( this ) } ); // Initialization @@ -7405,7 +7655,9 @@ OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) { */ OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) { // e.which is 0 for touch events, 1 for left mouse button - if ( !this.isDisabled() && e.which <= 1 ) { + // Only close toolgroup when a tool was actually selected + // FIXME: this duplicates logic from the parent class + if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === this.getTargetTool( e ) ) { this.setActive( false ); } return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e ); @@ -7469,14 +7721,32 @@ OO.ui.PopupToolGroup.prototype.setActive = function ( value ) { /** * Drop down list layout of tools as labeled icon buttons. * + * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the + * bottom of the main list. These are not automatically positioned at the bottom of the list; you + * may want to use the 'promote' and 'demote' configuration options to achieve this. + * * @class * @extends OO.ui.PopupToolGroup * * @constructor * @param {OO.ui.Toolbar} toolbar * @param {Object} [config] Configuration options + * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always + * shown. + * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be + * allowed to be collapsed. + * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default */ OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) { + // Configuration initialization + config = config || {}; + + // Properties (must be set before parent constructor, which calls #populate) + this.allowCollapse = config.allowCollapse; + this.forceExpand = config.forceExpand; + this.expanded = config.expanded !== undefined ? config.expanded : false; + this.collapsibleTools = []; + // Parent constructor OO.ui.ListToolGroup.super.call( this, toolbar, config ); @@ -7494,6 +7764,96 @@ OO.ui.ListToolGroup.static.accelTooltips = true; OO.ui.ListToolGroup.static.name = 'list'; +/* Methods */ + +/** + * @inheritdoc + */ +OO.ui.ListToolGroup.prototype.populate = function () { + var i, len, allowCollapse = []; + + OO.ui.ListToolGroup.super.prototype.populate.call( this ); + + // Update the list of collapsible tools + if ( this.allowCollapse !== undefined ) { + allowCollapse = this.allowCollapse; + } else if ( this.forceExpand !== undefined ) { + allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand ); + } + + this.collapsibleTools = []; + for ( i = 0, len = allowCollapse.length; i < len; i++ ) { + if ( this.tools[ allowCollapse[i] ] !== undefined ) { + this.collapsibleTools.push( this.tools[ allowCollapse[i] ] ); + } + } + + // Keep at the end, even when tools are added + this.$group.append( this.getExpandCollapseTool().$element ); + + this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 ); + + // Calling jQuery's .hide() and then .show() on a detached element caches the default value of its + // 'display' attribute and restores it, and the tool uses a and can be hidden and re-shown. + // Is this a jQuery bug? http://jsfiddle.net/gtj4hu3h/ + if ( this.getExpandCollapseTool().$element.css( 'display' ) === 'inline' ) { + this.getExpandCollapseTool().$element.css( 'display', 'inline-block' ); + } + + this.updateCollapsibleState(); +}; + +OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () { + if ( this.expandCollapseTool === undefined ) { + var ExpandCollapseTool = function () { + ExpandCollapseTool.super.apply( this, arguments ); + }; + + OO.inheritClass( ExpandCollapseTool, OO.ui.Tool ); + + ExpandCollapseTool.prototype.onSelect = function () { + this.toolGroup.expanded = !this.toolGroup.expanded; + this.toolGroup.updateCollapsibleState(); + this.setActive( false ); + }; + ExpandCollapseTool.prototype.onUpdateState = function () { + // Do nothing. Tool interface requires an implementation of this function. + }; + + ExpandCollapseTool.static.name = 'more-fewer'; + + this.expandCollapseTool = new ExpandCollapseTool( this ); + } + return this.expandCollapseTool; +}; + +/** + * @inheritdoc + */ +OO.ui.ListToolGroup.prototype.onPointerUp = function ( e ) { + var ret = OO.ui.ListToolGroup.super.prototype.onPointerUp.call( this, e ); + + // Do not close the popup when the user wants to show more/fewer tools + if ( this.$( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) { + // Prevent the popup list from being hidden + this.setActive( true ); + } + + return ret; +}; + +OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () { + var i, len; + + this.getExpandCollapseTool() + .setIcon( this.expanded ? 'collapse' : 'expand' ) + .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) ); + + for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) { + this.collapsibleTools[i].toggle( this.expanded ); + } +}; + /** * Drop down menu layout of tools as selectable menu items. * @@ -7655,7 +8015,7 @@ OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) { * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget. * * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This - * allows bidrectional communication. + * allows bidirectional communication. * * Use together with OO.ui.GroupWidget to make disabled state inheritable. * @@ -7704,25 +8064,27 @@ OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) { * * Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections. * + * Subclasses that set the value of #lookupInput from their `choose` or `select` handler should + * be aware that this will cause new suggestions to be looked up for the new value. If this is + * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups. + * * @class * @abstract * * @constructor * @param {OO.ui.TextInputWidget} input Input widget * @param {Object} [config] Configuration options - * @cfg {jQuery} [$overlay] Overlay layer; defaults to the current window's overlay. + * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning + * @cfg {jQuery} [$container=input.$element] Element to render menu under */ OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) { - // Config intialization + // Configuration initialization config = config || {}; // Properties this.lookupInput = input; - this.$overlay = config.$overlay || ( this.$.$iframe || this.$element ).closest( '.oo-ui-window' ).children( '.oo-ui-window-overlay' ); - if ( this.$overlay.length === 0 ) { - this.$overlay = this.$( 'body' ); - } - this.lookupMenu = new OO.ui.TextInputMenuWidget( this, { + this.$overlay = config.$overlay || this.$element; + this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, { $: OO.ui.Element.getJQuery( this.$overlay ), input: this.lookupInput, $container: config.$container @@ -7730,21 +8092,22 @@ OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) { this.lookupCache = {}; this.lookupQuery = null; this.lookupRequest = null; - this.populating = false; + this.lookupsDisabled = false; + this.lookupInputFocused = false; // Events - this.$overlay.append( this.lookupMenu.$element ); - this.lookupInput.$input.on( { - focus: OO.ui.bind( this.onLookupInputFocus, this ), - blur: OO.ui.bind( this.onLookupInputBlur, this ), - mousedown: OO.ui.bind( this.onLookupInputMouseDown, this ) + focus: this.onLookupInputFocus.bind( this ), + blur: this.onLookupInputBlur.bind( this ), + mousedown: this.onLookupInputMouseDown.bind( this ) } ); this.lookupInput.connect( this, { change: 'onLookupInputChange' } ); + this.lookupMenu.connect( this, { toggle: 'onLookupMenuToggle' } ); // Initialization this.$element.addClass( 'oo-ui-lookupWidget' ); this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' ); + this.$overlay.append( this.lookupMenu.$element ); }; /* Methods */ @@ -7755,7 +8118,8 @@ OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) { * @param {jQuery.Event} e Input focus event */ OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () { - this.openLookupMenu(); + this.lookupInputFocused = true; + this.populateLookupMenu(); }; /** @@ -7764,7 +8128,8 @@ OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () { * @param {jQuery.Event} e Input blur event */ OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () { - this.lookupMenu.toggle( false ); + this.closeLookupMenu(); + this.lookupInputFocused = false; }; /** @@ -7773,7 +8138,13 @@ OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () { * @param {jQuery.Event} e Input mouse down event */ OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () { - this.openLookupMenu(); + // Only open the menu if the input was already focused. + // This way we allow the user to open the menu again after closing it with Esc + // by clicking in the input. Opening (and populating) the menu when initially + // clicking into the input is handled by the focus handler. + if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) { + this.populateLookupMenu(); + } }; /** @@ -7782,48 +8153,90 @@ OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () { * @param {string} value New input value */ OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () { - this.openLookupMenu(); + if ( this.lookupInputFocused ) { + this.populateLookupMenu(); + } +}; + +/** + * Handle the lookup menu being shown/hidden. + * @param {boolean} visible Whether the lookup menu is now visible. + */ +OO.ui.LookupInputWidget.prototype.onLookupMenuToggle = function ( visible ) { + if ( !visible ) { + // When the menu is hidden, abort any active request and clear the menu. + // This has to be done here in addition to closeLookupMenu(), because + // MenuSelectWidget will close itself when the user presses Esc. + this.abortLookupRequest(); + this.lookupMenu.clearItems(); + } }; /** * Get lookup menu. * - * @return {OO.ui.TextInputMenuWidget} + * @return {OO.ui.TextInputMenuSelectWidget} */ OO.ui.LookupInputWidget.prototype.getLookupMenu = function () { return this.lookupMenu; }; /** - * Open the menu. + * Disable or re-enable lookups. * - * @chainable + * When lookups are disabled, calls to #populateLookupMenu will be ignored. + * + * @param {boolean} disabled Disable lookups */ -OO.ui.LookupInputWidget.prototype.openLookupMenu = function () { - var value = this.lookupInput.getValue(); +OO.ui.LookupInputWidget.prototype.setLookupsDisabled = function ( disabled ) { + this.lookupsDisabled = !!disabled; +}; - if ( this.lookupMenu.$input.is( ':focus' ) && $.trim( value ) !== '' ) { - this.populateLookupMenu(); +/** + * Open the menu. If there are no entries in the menu, this does nothing. + * + * @chainable + */ +OO.ui.LookupInputWidget.prototype.openLookupMenu = function () { + if ( !this.lookupMenu.isEmpty() ) { this.lookupMenu.toggle( true ); - } else { - this.lookupMenu - .clearItems() - .toggle( false ); } + return this; +}; +/** + * Close the menu, empty it, and abort any pending request. + * + * @chainable + */ +OO.ui.LookupInputWidget.prototype.closeLookupMenu = function () { + this.lookupMenu.toggle( false ); + this.abortLookupRequest(); + this.lookupMenu.clearItems(); return this; }; /** - * Populate lookup menu with current information. + * Request menu items based on the input's current value, and when they arrive, + * populate the menu with these items and show the menu. + * + * If lookups have been disabled with #setLookupsDisabled, this function does nothing. * * @chainable */ OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () { - var widget = this; + var widget = this, + value = this.lookupInput.getValue(); - if ( !this.populating ) { - this.populating = true; + if ( this.lookupsDisabled ) { + return; + } + + // If the input is empty, clear the menu + if ( value === '' ) { + this.closeLookupMenu(); + // Skip population if there is already a request pending for the current value + } else if ( value !== this.lookupQuery ) { this.getLookupMenuItems() .done( function ( items ) { widget.lookupMenu.clearItems(); @@ -7832,15 +8245,12 @@ OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () { .addItems( items ) .toggle( true ); widget.initializeLookupMenuSelection(); - widget.openLookupMenu(); } else { - widget.lookupMenu.toggle( true ); + widget.lookupMenu.toggle( false ); } - widget.populating = false; } ) .fail( function () { widget.lookupMenu.clearItems(); - widget.populating = false; } ); } @@ -7848,7 +8258,7 @@ OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () { }; /** - * Set selection in the lookup menu with current information. + * Select and highlight the first selectable item in the menu. * * @chainable */ @@ -7863,50 +8273,74 @@ OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () { * Get lookup menu items for the current query. * * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument - * of the done event + * of the done event. If the request was aborted to make way for a subsequent request, + * this promise will not be rejected: it will remain pending forever. */ OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () { var widget = this, value = this.lookupInput.getValue(), - deferred = $.Deferred(); + deferred = $.Deferred(), + ourRequest; - if ( value && value !== this.lookupQuery ) { - // Abort current request if query has changed - if ( this.lookupRequest ) { - this.lookupRequest.abort(); - this.lookupQuery = null; - this.lookupRequest = null; - } - if ( value in this.lookupCache ) { - deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) ); - } else { - this.lookupQuery = value; - this.lookupRequest = this.getLookupRequest() - .always( function () { + this.abortLookupRequest(); + if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) { + deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) ); + } else { + this.lookupInput.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.lookupInput.popPending(); + } ) + .done( function ( data ) { + // 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; - } ) - .done( function ( data ) { widget.lookupCache[value] = widget.getLookupCacheItemFromData( data ); deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) ); - } ) - .fail( function () { + } + } ) + .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(); - } ); - this.pushPending(); - this.lookupRequest.always( function () { - widget.popPending(); + } } ); - } } return deferred.promise(); }; +/** + * Abort the currently pending lookup request, if any. + */ +OO.ui.LookupInputWidget.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(); + } +}; + /** * Get a new request object of the current lookup query value. * * @abstract - * @return {jqXHR} jQuery AJAX object, or promise object with an .abort() method + * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method */ OO.ui.LookupInputWidget.prototype.getLookupRequest = function () { // Stub, implemented in subclass @@ -7914,32 +8348,31 @@ OO.ui.LookupInputWidget.prototype.getLookupRequest = function () { }; /** - * Handle successful lookup request. - * - * Overriding methods should call #populateLookupMenu when results are available and cache results - * for future lookups in #lookupCache as an array of #OO.ui.MenuItemWidget objects. + * Get a list of menu item widgets from the data stored by the lookup request's done handler. * * @abstract - * @param {Mixed} data Response from server + * @param {Mixed} data Cached result data, usually an array + * @return {OO.ui.MenuOptionWidget[]} Menu items */ -OO.ui.LookupInputWidget.prototype.onLookupRequestDone = function () { +OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () { // Stub, implemented in subclass + return []; }; /** - * Get a list of menu item widgets from the data stored by the lookup request's done handler. + * Get lookup cache item from server response data. * * @abstract - * @param {Mixed} data Cached result data, usually an array - * @return {OO.ui.MenuItemWidget[]} Menu items + * @param {Mixed} data Response from server + * @return {Mixed} Cached result data */ -OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () { +OO.ui.LookupInputWidget.prototype.getLookupCacheItemFromData = function () { // Stub, implemented in subclass return []; }; /** - * Set of controls for an OO.ui.OutlineWidget. + * Set of controls for an OO.ui.OutlineSelectWidget. * * Controls include moving items up and down, removing items, and adding different kinds of items. * @@ -7949,7 +8382,7 @@ OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () { * @mixins OO.ui.IconElement * * @constructor - * @param {OO.ui.OutlineWidget} outline Outline to control + * @param {OO.ui.OutlineSelectWidget} outline Outline to control * @param {Object} [config] Configuration options */ OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) { @@ -8124,9 +8557,12 @@ OO.ui.ToggleWidget.prototype.setValue = function ( value ) { * * @constructor * @param {Object} [config] Configuration options - * @cfg {OO.ui.ButtonWidget} [items] Buttons to add + * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add */ OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) { + // Configuration initialization + config = config || {}; + // Parent constructor OO.ui.ButtonGroupWidget.super.call( this, config ); @@ -8174,7 +8610,7 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) { OO.ui.IconElement.call( this, config ); OO.ui.IndicatorElement.call( this, config ); OO.ui.LabelElement.call( this, config ); - OO.ui.TitledElement.call( this, config, $.extend( {}, config, { $titled: this.$button } ) ); + OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) ); OO.ui.FlaggedElement.call( this, config ); // Properties @@ -8184,8 +8620,8 @@ OO.ui.ButtonWidget = function OoUiButtonWidget( config ) { // Events this.$button.on( { - click: OO.ui.bind( this.onClick, this ), - keypress: OO.ui.bind( this.onKeyPress, this ) + click: this.onClick.bind( this ), + keypress: this.onKeyPress.bind( this ) } ); // Initialization @@ -8239,7 +8675,7 @@ OO.ui.ButtonWidget.prototype.onClick = function () { */ OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) { if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) { - this.onClick(); + this.emit( 'click' ); if ( this.isHyperlink ) { return true; } @@ -8321,7 +8757,7 @@ OO.ui.ButtonWidget.prototype.setTarget = function ( target ) { * @cfg {boolean} [framed=false] Render button with a frame */ OO.ui.ActionWidget = function OoUiActionWidget( config ) { - // Config intialization + // Configuration initialization config = $.extend( { framed: false }, config ); // Parent constructor @@ -8570,88 +9006,12 @@ OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) { }; /** - * Icon widget. - * - * See OO.ui.IconElement for more information. - * - * @class - * @extends OO.ui.Widget - * @mixins OO.ui.IconElement - * @mixins OO.ui.TitledElement + * Dropdown menu of options. * - * @constructor - * @param {Object} [config] Configuration options - */ -OO.ui.IconWidget = function OoUiIconWidget( config ) { - // Config intialization - config = config || {}; - - // Parent constructor - OO.ui.IconWidget.super.call( this, config ); - - // Mixin constructors - OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) ); - OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) ); - - // Initialization - this.$element.addClass( 'oo-ui-iconWidget' ); -}; - -/* Setup */ - -OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget ); -OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement ); -OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement ); - -/* Static Properties */ - -OO.ui.IconWidget.static.tagName = 'span'; - -/** - * Indicator widget. - * - * See OO.ui.IndicatorElement for more information. - * - * @class - * @extends OO.ui.Widget - * @mixins OO.ui.IndicatorElement - * @mixins OO.ui.TitledElement - * - * @constructor - * @param {Object} [config] Configuration options - */ -OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) { - // Config intialization - config = config || {}; - - // Parent constructor - OO.ui.IndicatorWidget.super.call( this, config ); - - // Mixin constructors - OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) ); - OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) ); - - // Initialization - this.$element.addClass( 'oo-ui-indicatorWidget' ); -}; - -/* Setup */ - -OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget ); -OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement ); -OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement ); - -/* Static Properties */ - -OO.ui.IndicatorWidget.static.tagName = 'span'; - -/** - * Inline menu of options. - * - * Inline menus provide a control for accessing a menu and compose a menu within the widget, which + * Dropdown menus provide a control for accessing a menu and compose a menu within the widget, which * can be accessed using the #getMenu method. * - * Use with OO.ui.MenuItemWidget. + * Use with OO.ui.MenuOptionWidget. * * @class * @extends OO.ui.Widget @@ -8664,12 +9024,12 @@ OO.ui.IndicatorWidget.static.tagName = 'span'; * @param {Object} [config] Configuration options * @cfg {Object} [menu] Configuration options to pass to menu widget */ -OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) { +OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) { // Configuration initialization config = $.extend( { indicator: 'down' }, config ); // Parent constructor - OO.ui.InlineMenuWidget.super.call( this, config ); + OO.ui.DropdownWidget.super.call( this, config ); // Mixin constructors OO.ui.IconElement.call( this, config ); @@ -8678,47 +9038,47 @@ OO.ui.InlineMenuWidget = function OoUiInlineMenuWidget( config ) { OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) ); // Properties - this.menu = new OO.ui.MenuWidget( $.extend( { $: this.$, widget: this }, config.menu ) ); + this.menu = new OO.ui.MenuSelectWidget( $.extend( { $: this.$, widget: this }, config.menu ) ); this.$handle = this.$( '' ); // Events - this.$element.on( { click: OO.ui.bind( this.onClick, this ) } ); + this.$element.on( { click: this.onClick.bind( this ) } ); this.menu.connect( this, { select: 'onMenuSelect' } ); // Initialization this.$handle - .addClass( 'oo-ui-inlineMenuWidget-handle' ) + .addClass( 'oo-ui-dropdownWidget-handle' ) .append( this.$icon, this.$label, this.$indicator ); this.$element - .addClass( 'oo-ui-inlineMenuWidget' ) + .addClass( 'oo-ui-dropdownWidget' ) .append( this.$handle, this.menu.$element ); }; /* Setup */ -OO.inheritClass( OO.ui.InlineMenuWidget, OO.ui.Widget ); -OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IconElement ); -OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.IndicatorElement ); -OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.LabelElement ); -OO.mixinClass( OO.ui.InlineMenuWidget, OO.ui.TitledElement ); +OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement ); +OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement ); +OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement ); +OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement ); /* Methods */ /** * Get the menu. * - * @return {OO.ui.MenuWidget} Menu of widget + * @return {OO.ui.MenuSelectWidget} Menu of widget */ -OO.ui.InlineMenuWidget.prototype.getMenu = function () { +OO.ui.DropdownWidget.prototype.getMenu = function () { return this.menu; }; /** * Handles menu select events. * - * @param {OO.ui.MenuItemWidget} item Selected menu item + * @param {OO.ui.MenuOptionWidget} item Selected menu item */ -OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) { +OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) { var selectedLabel; if ( !item ) { @@ -8740,7 +9100,7 @@ OO.ui.InlineMenuWidget.prototype.onMenuSelect = function ( item ) { * * @param {jQuery.Event} e Mouse click event */ -OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) { +OO.ui.DropdownWidget.prototype.onClick = function ( e ) { // Skip clicks within the menu if ( $.contains( this.menu.$element[0], e.target ) ) { return; @@ -8756,6 +9116,82 @@ OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) { return false; }; +/** + * Icon widget. + * + * See OO.ui.IconElement for more information. + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.IconElement + * @mixins OO.ui.TitledElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.IconWidget = function OoUiIconWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.IconWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) ); + OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) ); + + // Initialization + this.$element.addClass( 'oo-ui-iconWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement ); +OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement ); + +/* Static Properties */ + +OO.ui.IconWidget.static.tagName = 'span'; + +/** + * Indicator widget. + * + * See OO.ui.IndicatorElement for more information. + * + * @class + * @extends OO.ui.Widget + * @mixins OO.ui.IndicatorElement + * @mixins OO.ui.TitledElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.IndicatorWidget.super.call( this, config ); + + // Mixin constructors + OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) ); + OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) ); + + // Initialization + this.$element.addClass( 'oo-ui-indicatorWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget ); +OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement ); +OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement ); + +/* Static Properties */ + +OO.ui.IndicatorWidget.static.tagName = 'span'; + /** * Base class for input widgets. * @@ -8768,12 +9204,11 @@ OO.ui.InlineMenuWidget.prototype.onClick = function ( e ) { * @param {Object} [config] Configuration options * @cfg {string} [name=''] HTML input name * @cfg {string} [value=''] Input value - * @cfg {boolean} [readOnly=false] Prevent changes * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string. */ OO.ui.InputWidget = function OoUiInputWidget( config ) { - // Config intialization - config = $.extend( { readOnly: false }, config ); + // Configuration initialization + config = config || {}; // Parent constructor OO.ui.InputWidget.super.call( this, config ); @@ -8784,18 +9219,16 @@ OO.ui.InputWidget = function OoUiInputWidget( config ) { // Properties this.$input = this.getInputElement( config ); this.value = ''; - this.readOnly = false; this.inputFilter = config.inputFilter; // Events - this.$input.on( 'keydown mouseup cut paste change input select', OO.ui.bind( this.onEdit, this ) ); + this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) ); // Initialization this.$input .attr( 'name', config.name ) .prop( 'disabled', this.isDisabled() ); - this.setReadOnly( config.readOnly ); - this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input ); + this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '' ) ); this.setValue( config.value ); }; @@ -8808,7 +9241,7 @@ OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement ); /** * @event change - * @param value + * @param {string} value */ /* Methods */ @@ -8816,6 +9249,7 @@ OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement ); /** * Get input element. * + * @private * @param {Object} [config] Configuration options * @return {jQuery} Input element */ @@ -8870,12 +9304,12 @@ OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) { * @chainable */ OO.ui.InputWidget.prototype.setValue = function ( value ) { - value = this.sanitizeValue( value ); + value = this.cleanUpValue( value ); if ( this.value !== value ) { this.value = value; this.emit( 'change', this.value ); } - // Update the DOM if it has changed. Note that with sanitizeValue, it + // Update the DOM if it has changed. Note that with cleanUpValue, it // is possible for the DOM value to change without this.value changing. if ( this.$input.val() !== this.value ) { this.$input.val( this.value ); @@ -8884,14 +9318,15 @@ OO.ui.InputWidget.prototype.setValue = function ( value ) { }; /** - * Sanitize incoming value. + * Clean up incoming value. * - * Ensures value is a string, and converts undefined and null to empty strings. + * Ensures value is a string, and converts undefined and null to empty string. * + * @private * @param {string} value Original value - * @return {string} Sanitized value + * @return {string} Cleaned up value */ -OO.ui.InputWidget.prototype.sanitizeValue = function ( value ) { +OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) { if ( value === undefined || value === null ) { return ''; } else if ( this.inputFilter ) { @@ -8915,57 +9350,188 @@ OO.ui.InputWidget.prototype.simulateLabelClick = function () { }; /** - * Check if the widget is read-only. - * - * @return {boolean} + * @inheritdoc */ -OO.ui.InputWidget.prototype.isReadOnly = function () { - return this.readOnly; +OO.ui.InputWidget.prototype.setDisabled = function ( state ) { + OO.ui.InputWidget.super.prototype.setDisabled.call( this, state ); + if ( this.$input ) { + this.$input.prop( 'disabled', this.isDisabled() ); + } + return this; }; /** - * Set the read-only state of the widget. + * Focus the input. * - * This should probably change the widgets's appearance and prevent it from being used. + * @chainable + */ +OO.ui.InputWidget.prototype.focus = function () { + this.$input[0].focus(); + return this; +}; + +/** + * Blur the input. * - * @param {boolean} state Make input read-only * @chainable */ -OO.ui.InputWidget.prototype.setReadOnly = function ( state ) { - this.readOnly = !!state; - this.$input.prop( 'readOnly', this.readOnly ); +OO.ui.InputWidget.prototype.blur = function () { + this.$input[0].blur(); return this; }; /** - * @inheritdoc + * A button that is an input widget. Intended to be used within a OO.ui.FormLayout. + * + * @class + * @extends OO.ui.InputWidget + * @mixins OO.ui.ButtonElement + * @mixins OO.ui.IconElement + * @mixins OO.ui.IndicatorElement + * @mixins OO.ui.LabelElement + * @mixins OO.ui.TitledElement + * @mixins OO.ui.FlaggedElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [type='button'] HTML tag `type` attribute, may be 'button', 'submit' or 'reset' + * @cfg {boolean} [useInputTag=false] Whether to use `` rather than `