X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/resources/lib/oojs-ui/oojs-ui-windows.js diff --git a/resources/lib/oojs-ui/oojs-ui-windows.js b/resources/lib/oojs-ui/oojs-ui-windows.js new file mode 100644 index 00000000..2a0f02d6 --- /dev/null +++ b/resources/lib/oojs-ui/oojs-ui-windows.js @@ -0,0 +1,3571 @@ +/*! + * OOjs UI v0.23.0 + * https://www.mediawiki.org/wiki/OOjs_UI + * + * Copyright 2011–2017 OOjs UI Team and other contributors. + * Released under the MIT license + * http://oojs.mit-license.org + * + * Date: 2017-09-05T21:23:58Z + */ +( function ( OO ) { + +'use strict'; + +/** + * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action. + * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability + * of the actions. + * + * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}. + * Please see the [OOjs UI documentation on MediaWiki] [1] for more information + * and examples. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets + * + * @class + * @extends OO.ui.ButtonWidget + * @mixins OO.ui.mixin.PendingElement + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’). + * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action + * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method + * for more information about setting modes. + * @cfg {boolean} [framed=false] Render the action button with a frame + */ +OO.ui.ActionWidget = function OoUiActionWidget( config ) { + // Configuration initialization + config = $.extend( { framed: false }, config ); + + // Parent constructor + OO.ui.ActionWidget.parent.call( this, config ); + + // Mixin constructors + OO.ui.mixin.PendingElement.call( this, config ); + + // Properties + this.action = config.action || ''; + this.modes = config.modes || []; + this.width = 0; + this.height = 0; + + // Initialization + this.$element.addClass( 'oo-ui-actionWidget' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget ); +OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement ); + +/* Methods */ + +/** + * Check if the action is configured to be available in the specified `mode`. + * + * @param {string} mode Name of mode + * @return {boolean} The action is configured with the mode + */ +OO.ui.ActionWidget.prototype.hasMode = function ( mode ) { + return this.modes.indexOf( mode ) !== -1; +}; + +/** + * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’). + * + * @return {string} + */ +OO.ui.ActionWidget.prototype.getAction = function () { + return this.action; +}; + +/** + * Get the symbolic name of the mode or modes for which the action is configured to be available. + * + * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method. + * Only actions that are configured to be avaiable in the current mode will be visible. All other actions + * are hidden. + * + * @return {string[]} + */ +OO.ui.ActionWidget.prototype.getModes = function () { + return this.modes.slice(); +}; + +/* eslint-disable no-unused-vars */ +/** + * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them. + * Actions can be made available for specific contexts (modes) and circumstances + * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}. + * + * ActionSets contain two types of actions: + * + * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property. + * - Other: Other actions include all non-special visible actions. + * + * Please see the [OOjs UI documentation on MediaWiki][1] for more information. + * + * @example + * // Example: An action set used in a process dialog + * function MyProcessDialog( config ) { + * MyProcessDialog.parent.call( this, config ); + * } + * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog ); + * MyProcessDialog.static.title = 'An action set in a process dialog'; + * MyProcessDialog.static.name = 'myProcessDialog'; + * // An action set that uses modes ('edit' and 'help' mode, in this example). + * MyProcessDialog.static.actions = [ + * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] }, + * { action: 'help', modes: 'edit', label: 'Help' }, + * { modes: 'edit', label: 'Cancel', flags: 'safe' }, + * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' } + * ]; + * + * MyProcessDialog.prototype.initialize = function () { + * MyProcessDialog.parent.prototype.initialize.apply( this, arguments ); + * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } ); + * this.panel1.$element.append( '

This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode.

' ); + * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } ); + * this.panel2.$element.append( '

This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode.

' ); + * this.stackLayout = new OO.ui.StackLayout( { + * items: [ this.panel1, this.panel2 ] + * } ); + * this.$body.append( this.stackLayout.$element ); + * }; + * MyProcessDialog.prototype.getSetupProcess = function ( data ) { + * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data ) + * .next( function () { + * this.actions.setMode( 'edit' ); + * }, this ); + * }; + * MyProcessDialog.prototype.getActionProcess = function ( action ) { + * if ( action === 'help' ) { + * this.actions.setMode( 'help' ); + * this.stackLayout.setItem( this.panel2 ); + * } else if ( action === 'back' ) { + * this.actions.setMode( 'edit' ); + * this.stackLayout.setItem( this.panel1 ); + * } else if ( action === 'continue' ) { + * var dialog = this; + * return new OO.ui.Process( function () { + * dialog.close(); + * } ); + * } + * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action ); + * }; + * MyProcessDialog.prototype.getBodyHeight = function () { + * return this.panel1.$element.outerHeight( true ); + * }; + * var windowManager = new OO.ui.WindowManager(); + * $( 'body' ).append( windowManager.$element ); + * var dialog = new MyProcessDialog( { + * size: 'medium' + * } ); + * windowManager.addWindows( [ dialog ] ); + * windowManager.openWindow( dialog ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets + * + * @abstract + * @class + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.ActionSet = function OoUiActionSet( config ) { + // Configuration initialization + config = config || {}; + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.list = []; + this.categories = { + actions: 'getAction', + flags: 'getFlags', + modes: 'getModes' + }; + this.categorized = {}; + this.special = {}; + this.others = []; + this.organized = false; + this.changing = false; + this.changed = false; +}; +/* eslint-enable no-unused-vars */ + +/* Setup */ + +OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter ); + +/* Static Properties */ + +/** + * Symbolic name of the flags used to identify special actions. Special actions are displayed in the + * header of a {@link OO.ui.ProcessDialog process dialog}. + * See the [OOjs UI documentation on MediaWiki][2] for more information and examples. + * + * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs + * + * @abstract + * @static + * @inheritable + * @property {string} + */ +OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ]; + +/* Events */ + +/** + * @event click + * + * A 'click' event is emitted when an action is clicked. + * + * @param {OO.ui.ActionWidget} action Action that was clicked + */ + +/** + * @event add + * + * An 'add' event is emitted when actions are {@link #method-add added} to the action set. + * + * @param {OO.ui.ActionWidget[]} added Actions added + */ + +/** + * @event remove + * + * A 'remove' event is emitted when actions are {@link #method-remove removed} + * or {@link #clear cleared}. + * + * @param {OO.ui.ActionWidget[]} added Actions removed + */ + +/** + * @event change + * + * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared}, + * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed. + * + */ + +/* Methods */ + +/** + * Handle action change events. + * + * @private + * @fires change + */ +OO.ui.ActionSet.prototype.onActionChange = function () { + this.organized = false; + if ( this.changing ) { + this.changed = true; + } else { + this.emit( 'change' ); + } +}; + +/** + * Check if an action is one of the special actions. + * + * @param {OO.ui.ActionWidget} action Action to check + * @return {boolean} Action is special + */ +OO.ui.ActionSet.prototype.isSpecial = function ( action ) { + var flag; + + for ( flag in this.special ) { + if ( action === this.special[ flag ] ) { + return true; + } + } + + return false; +}; + +/** + * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’, + * or ‘disabled’. + * + * @param {Object} [filters] Filters to use, omit to get all actions + * @param {string|string[]} [filters.actions] Actions that action widgets must have + * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe') + * @param {string|string[]} [filters.modes] Modes that action widgets must have + * @param {boolean} [filters.visible] Action widgets must be visible + * @param {boolean} [filters.disabled] Action widgets must be disabled + * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria + */ +OO.ui.ActionSet.prototype.get = function ( filters ) { + var i, len, list, category, actions, index, match, matches; + + if ( filters ) { + this.organize(); + + // Collect category candidates + matches = []; + for ( category in this.categorized ) { + list = filters[ category ]; + if ( list ) { + if ( !Array.isArray( list ) ) { + list = [ list ]; + } + for ( i = 0, len = list.length; i < len; i++ ) { + actions = this.categorized[ category ][ list[ i ] ]; + if ( Array.isArray( actions ) ) { + matches.push.apply( matches, actions ); + } + } + } + } + // Remove by boolean filters + for ( i = 0, len = matches.length; i < len; i++ ) { + match = matches[ i ]; + if ( + ( filters.visible !== undefined && match.isVisible() !== filters.visible ) || + ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled ) + ) { + matches.splice( i, 1 ); + len--; + i--; + } + } + // Remove duplicates + for ( i = 0, len = matches.length; i < len; i++ ) { + match = matches[ i ]; + index = matches.lastIndexOf( match ); + while ( index !== i ) { + matches.splice( index, 1 ); + len--; + index = matches.lastIndexOf( match ); + } + } + return matches; + } + return this.list.slice(); +}; + +/** + * Get 'special' actions. + * + * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'. + * Special flags can be configured in subclasses by changing the static #specialFlags property. + * + * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets. + */ +OO.ui.ActionSet.prototype.getSpecial = function () { + this.organize(); + return $.extend( {}, this.special ); +}; + +/** + * Get 'other' actions. + * + * Other actions include all non-special visible action widgets. + * + * @return {OO.ui.ActionWidget[]} 'Other' action widgets + */ +OO.ui.ActionSet.prototype.getOthers = function () { + this.organize(); + return this.others.slice(); +}; + +/** + * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured + * to be available in the specified mode will be made visible. All other actions will be hidden. + * + * @param {string} mode The mode. Only actions configured to be available in the specified + * mode will be made visible. + * @chainable + * @fires toggle + * @fires change + */ +OO.ui.ActionSet.prototype.setMode = function ( mode ) { + var i, len, action; + + this.changing = true; + for ( i = 0, len = this.list.length; i < len; i++ ) { + action = this.list[ i ]; + action.toggle( action.hasMode( mode ) ); + } + + this.organized = false; + this.changing = false; + this.emit( 'change' ); + + return this; +}; + +/** + * Set the abilities of the specified actions. + * + * Action widgets that are configured with the specified actions will be enabled + * or disabled based on the boolean values specified in the `actions` + * parameter. + * + * @param {Object.} actions A list keyed by action name with boolean + * values that indicate whether or not the action should be enabled. + * @chainable + */ +OO.ui.ActionSet.prototype.setAbilities = function ( actions ) { + var i, len, action, item; + + for ( i = 0, len = this.list.length; i < len; i++ ) { + item = this.list[ i ]; + action = item.getAction(); + if ( actions[ action ] !== undefined ) { + item.setDisabled( !actions[ action ] ); + } + } + + return this; +}; + +/** + * Executes a function once per action. + * + * When making changes to multiple actions, use this method instead of iterating over the actions + * manually to defer emitting a #change event until after all actions have been changed. + * + * @param {Object|null} filter Filters to use to determine which actions to iterate over; see #get + * @param {Function} callback Callback to run for each action; callback is invoked with three + * arguments: the action, the action's index, the list of actions being iterated over + * @chainable + */ +OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) { + this.changed = false; + this.changing = true; + this.get( filter ).forEach( callback ); + this.changing = false; + if ( this.changed ) { + this.emit( 'change' ); + } + + return this; +}; + +/** + * Add action widgets to the action set. + * + * @param {OO.ui.ActionWidget[]} actions Action widgets to add + * @chainable + * @fires add + * @fires change + */ +OO.ui.ActionSet.prototype.add = function ( actions ) { + var i, len, action; + + this.changing = true; + for ( i = 0, len = actions.length; i < len; i++ ) { + action = actions[ i ]; + action.connect( this, { + click: [ 'emit', 'click', action ], + toggle: [ 'onActionChange' ] + } ); + this.list.push( action ); + } + this.organized = false; + this.emit( 'add', actions ); + this.changing = false; + this.emit( 'change' ); + + return this; +}; + +/** + * Remove action widgets from the set. + * + * To remove all actions, you may wish to use the #clear method instead. + * + * @param {OO.ui.ActionWidget[]} actions Action widgets to remove + * @chainable + * @fires remove + * @fires change + */ +OO.ui.ActionSet.prototype.remove = function ( actions ) { + var i, len, index, action; + + this.changing = true; + for ( i = 0, len = actions.length; i < len; i++ ) { + action = actions[ i ]; + index = this.list.indexOf( action ); + if ( index !== -1 ) { + action.disconnect( this ); + this.list.splice( index, 1 ); + } + } + this.organized = false; + this.emit( 'remove', actions ); + this.changing = false; + this.emit( 'change' ); + + return this; +}; + +/** + * Remove all action widets from the set. + * + * To remove only specified actions, use the {@link #method-remove remove} method instead. + * + * @chainable + * @fires remove + * @fires change + */ +OO.ui.ActionSet.prototype.clear = function () { + var i, len, action, + removed = this.list.slice(); + + this.changing = true; + for ( i = 0, len = this.list.length; i < len; i++ ) { + action = this.list[ i ]; + action.disconnect( this ); + } + + this.list = []; + + this.organized = false; + this.emit( 'remove', removed ); + this.changing = false; + this.emit( 'change' ); + + return this; +}; + +/** + * Organize 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 + * @chainable + */ +OO.ui.ActionSet.prototype.organize = function () { + var i, iLen, j, jLen, flag, action, category, list, item, special, + specialFlags = this.constructor.static.specialFlags; + + if ( !this.organized ) { + this.categorized = {}; + this.special = {}; + this.others = []; + for ( i = 0, iLen = this.list.length; i < iLen; i++ ) { + action = this.list[ i ]; + if ( action.isVisible() ) { + // Populate categories + for ( category in this.categories ) { + if ( !this.categorized[ category ] ) { + this.categorized[ category ] = {}; + } + list = action[ this.categories[ category ] ](); + if ( !Array.isArray( list ) ) { + list = [ list ]; + } + for ( j = 0, jLen = list.length; j < jLen; j++ ) { + item = list[ j ]; + if ( !this.categorized[ category ][ item ] ) { + this.categorized[ category ][ item ] = []; + } + this.categorized[ category ][ item ].push( action ); + } + } + // Populate special/others + special = false; + for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) { + flag = specialFlags[ j ]; + if ( !this.special[ flag ] && action.hasFlag( flag ) ) { + this.special[ flag ] = action; + special = true; + break; + } + } + if ( !special ) { + this.others.push( action ); + } + } + } + this.organized = true; + } + + return this; +}; + +/** + * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong + * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the + * appearance and functionality of the error interface. + * + * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error + * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget + * that initiated the failed process will be disabled. + * + * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the + * process again. + * + * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1]. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors + * + * @class + * + * @constructor + * @param {string|jQuery} message Description of error + * @param {Object} [config] Configuration options + * @cfg {boolean} [recoverable=true] Error is recoverable. + * By default, errors are recoverable, and users can try the process again. + * @cfg {boolean} [warning=false] Error is a warning. + * If the error is a warning, the error interface will include a + * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning + * is not triggered a second time if the user chooses to continue. + */ +OO.ui.Error = function OoUiError( message, config ) { + // Allow passing positional parameters inside the config object + if ( OO.isPlainObject( message ) && config === undefined ) { + config = message; + message = config.message; + } + + // Configuration initialization + config = config || {}; + + // Properties + this.message = message instanceof jQuery ? message : String( message ); + this.recoverable = config.recoverable === undefined || !!config.recoverable; + this.warning = !!config.warning; +}; + +/* Setup */ + +OO.initClass( OO.ui.Error ); + +/* Methods */ + +/** + * Check if the error is recoverable. + * + * If the error is recoverable, users are able to try the process again. + * + * @return {boolean} Error is recoverable + */ +OO.ui.Error.prototype.isRecoverable = function () { + return this.recoverable; +}; + +/** + * Check if the error is a warning. + * + * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button. + * + * @return {boolean} Error is warning + */ +OO.ui.Error.prototype.isWarning = function () { + return this.warning; +}; + +/** + * Get error message as DOM nodes. + * + * @return {jQuery} Error message in DOM nodes + */ +OO.ui.Error.prototype.getMessage = function () { + return this.message instanceof jQuery ? + this.message.clone() : + $( '
' ).text( this.message ).contents(); +}; + +/** + * Get the error message text. + * + * @return {string} Error message + */ +OO.ui.Error.prototype.getMessageText = function () { + return this.message instanceof jQuery ? this.message.text() : this.message; +}; + +/** + * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise, + * or a function: + * + * - **number**: the process will wait for the specified number of milliseconds before proceeding. + * - **promise**: the process will continue to the next step when the promise is successfully resolved + * or stop if the promise is rejected. + * - **function**: the process will execute the function. The process will stop if the function returns + * either a boolean `false` or a promise that is rejected; if the function returns a number, the process + * will wait for that number of milliseconds before proceeding. + * + * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is + * configured, users can dismiss the error and try the process again, or not. If a process is stopped, + * its remaining steps will not be performed. + * + * @class + * + * @constructor + * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise + * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information + * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is + * a number or promise. + */ +OO.ui.Process = function ( step, context ) { + // Properties + this.steps = []; + + // Initialization + if ( step !== undefined ) { + this.next( step, context ); + } +}; + +/* Setup */ + +OO.initClass( OO.ui.Process ); + +/* Methods */ + +/** + * Start the process. + * + * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed. + * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected + * and any remaining steps are not performed. + */ +OO.ui.Process.prototype.execute = function () { + var i, len, promise; + + /** + * Continue execution. + * + * @ignore + * @param {Array} step A function and the context it should be called in + * @return {Function} Function that continues the process + */ + function proceed( step ) { + return function () { + // Execute step in the correct context + var deferred, + result = step.callback.call( step.context ); + + if ( result === false ) { + // Use rejected promise for boolean false results + return $.Deferred().reject( [] ).promise(); + } + if ( typeof result === 'number' ) { + if ( result < 0 ) { + throw new Error( 'Cannot go back in time: flux capacitor is out of service' ); + } + // Use a delayed promise for numbers, expecting them to be in milliseconds + deferred = $.Deferred(); + setTimeout( deferred.resolve, result ); + return deferred.promise(); + } + if ( result instanceof OO.ui.Error ) { + // Use rejected promise for error + return $.Deferred().reject( [ result ] ).promise(); + } + if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) { + // Use rejected promise for list of errors + return $.Deferred().reject( result ).promise(); + } + // Duck-type the object to see if it can produce a promise + if ( result && $.isFunction( result.promise ) ) { + // Use a promise generated from the result + return result.promise(); + } + // Use resolved promise for other results + return $.Deferred().resolve().promise(); + }; + } + + if ( this.steps.length ) { + // Generate a chain reaction of promises + promise = proceed( this.steps[ 0 ] )(); + for ( i = 1, len = this.steps.length; i < len; i++ ) { + promise = promise.then( proceed( this.steps[ i ] ) ); + } + } else { + promise = $.Deferred().resolve().promise(); + } + + return promise; +}; + +/** + * Create a process step. + * + * @private + * @param {number|jQuery.Promise|Function} step + * + * - Number of milliseconds to wait before proceeding + * - Promise that must be resolved before proceeding + * - Function to execute + * - If the function returns a boolean false the process will stop + * - If the function returns a promise, the process will continue to the next + * step when the promise is resolved or stop if the promise is rejected + * - If the function returns a number, the process will wait for that number of + * milliseconds before proceeding + * @param {Object} [context=null] Execution context of the function. The context is + * ignored if the step is a number or promise. + * @return {Object} Step object, with `callback` and `context` properties + */ +OO.ui.Process.prototype.createStep = function ( step, context ) { + if ( typeof step === 'number' || $.isFunction( step.promise ) ) { + return { + callback: function () { + return step; + }, + context: null + }; + } + if ( $.isFunction( step ) ) { + return { + callback: step, + context: context + }; + } + throw new Error( 'Cannot create process step: number, promise or function expected' ); +}; + +/** + * Add step to the beginning of the process. + * + * @inheritdoc #createStep + * @return {OO.ui.Process} this + * @chainable + */ +OO.ui.Process.prototype.first = function ( step, context ) { + this.steps.unshift( this.createStep( step, context ) ); + return this; +}; + +/** + * Add step to the end of the process. + * + * @inheritdoc #createStep + * @return {OO.ui.Process} this + * @chainable + */ +OO.ui.Process.prototype.next = function ( step, context ) { + this.steps.push( this.createStep( step, context ) ); + return this; +}; + +/** + * A window instance represents the life cycle for one single opening of a window + * until its closing. + * + * While OO.ui.WindowManager will reuse OO.ui.Window objects, each time a window is + * opened, a new lifecycle starts. + * + * For more information, please see the [OOjs UI documentation on MediaWiki] [1]. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows + * + * @class + * + * @constructor + */ +OO.ui.WindowInstance = function OOuiWindowInstance() { + var deferreds = { + opening: $.Deferred(), + opened: $.Deferred(), + closing: $.Deferred(), + closed: $.Deferred() + }; + + /** + * @private + * @property {Object} + */ + this.deferreds = deferreds; + + // Set these up as chained promises so that rejecting of + // an earlier stage automatically rejects the subsequent + // would-be stages as well. + + /** + * @property {jQuery.Promise} + */ + this.opening = deferreds.opening.promise(); + /** + * @property {jQuery.Promise} + */ + this.opened = this.opening.then( function () { + return deferreds.opened; + } ); + /** + * @property {jQuery.Promise} + */ + this.closing = this.opened.then( function () { + return deferreds.closing; + } ); + /** + * @property {jQuery.Promise} + */ + this.closed = this.closing.then( function () { + return deferreds.closed; + } ); +}; + +/* Setup */ + +OO.initClass( OO.ui.WindowInstance ); + +/** + * Check if window is opening. + * + * @return {boolean} Window is opening + */ +OO.ui.WindowInstance.prototype.isOpening = function () { + return this.deferreds.opened.state() === 'pending'; +}; + +/** + * Check if window is opened. + * + * @return {boolean} Window is opened + */ +OO.ui.WindowInstance.prototype.isOpened = function () { + return this.deferreds.opened.state() === 'resolved' && + this.deferreds.closing.state() === 'pending'; +}; + +/** + * Check if window is closing. + * + * @return {boolean} Window is closing + */ +OO.ui.WindowInstance.prototype.isClosing = function () { + return this.deferreds.closing.state() === 'resolved' && + this.deferreds.closed.state() === 'pending'; +}; + +/** + * Check if window is closed. + * + * @return {boolean} Window is closed + */ +OO.ui.WindowInstance.prototype.isClosed = function () { + return this.deferreds.closed.state() === 'resolved'; +}; + +/** + * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation. + * Managed windows are mutually exclusive. If a new window is opened while a current window is opening + * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows + * themselves are persistent and—rather than being torn down when closed—can be repopulated with the + * pertinent data and reused. + * + * Over the lifecycle of a window, the window manager makes available three promises: `opening`, + * `opened`, and `closing`, which represent the primary stages of the cycle: + * + * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s + * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window. + * + * - an `opening` event is emitted with an `opening` promise + * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before the + * window’s {@link OO.ui.Window#method-setup setup} method is called which executes OO.ui.Window#getSetupProcess. + * - a `setup` progress notification is emitted from the `opening` promise + * - the #getReadyDelay method is called the returned value is used to time a pause in execution before the + * window’s {@link OO.ui.Window#method-ready ready} method is called which executes OO.ui.Window#getReadyProcess. + * - a `ready` progress notification is emitted from the `opening` promise + * - the `opening` promise is resolved with an `opened` promise + * + * **Opened**: the window is now open. + * + * **Closing**: the closing stage begins when the window manager's #closeWindow or the + * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins + * to close the window. + * + * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted + * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before + * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the + * window and its result executed + * - a `hold` progress notification is emitted from the `closing` promise + * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before + * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the + * window and its result executed + * - a `teardown` progress notification is emitted from the `closing` promise + * - the `closing` promise is resolved. The window is now closed + * + * See the [OOjs UI documentation on MediaWiki][1] for more information. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers + * + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation + * Note that window classes that are instantiated with a factory must have + * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name. + * @cfg {boolean} [modal=true] Prevent interaction outside the dialog + */ +OO.ui.WindowManager = function OoUiWindowManager( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.WindowManager.parent.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.factory = config.factory; + this.modal = config.modal === undefined || !!config.modal; + this.windows = {}; + // Deprecated placeholder promise given to compatOpening in openWindow() + // that is resolved in closeWindow(). + this.compatOpened = null; + this.preparingToOpen = null; + this.preparingToClose = null; + this.currentWindow = null; + this.globalEvents = false; + this.$returnFocusTo = null; + this.$ariaHidden = null; + this.onWindowResizeTimeout = null; + this.onWindowResizeHandler = this.onWindowResize.bind( this ); + this.afterWindowResizeHandler = this.afterWindowResize.bind( this ); + + // Initialization + this.$element + .addClass( 'oo-ui-windowManager' ) + .toggleClass( 'oo-ui-windowManager-modal', this.modal ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.WindowManager, OO.ui.Element ); +OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter ); + +/* Events */ + +/** + * An 'opening' event is emitted when the window begins to be opened. + * + * @event opening + * @param {OO.ui.Window} win Window that's being opened + * @param {jQuery.Promise} opened A promise resolved with a value when the window is opened successfully. + * This promise also emits `setup` and `ready` notifications. When this promise is resolved, the first + * argument of the value is an 'closed' promise, the second argument is the opening data. + * @param {Object} data Window opening data + */ + +/** + * A 'closing' event is emitted when the window begins to be closed. + * + * @event closing + * @param {OO.ui.Window} win Window that's being closed + * @param {jQuery.Promise} closed A promise resolved with a value when the window is closed successfully. + * This promise also emits `hold` and `teardown` notifications. When this promise is resolved, the first + * argument of its value is the closing data. + * @param {Object} data Window closing data + */ + +/** + * A 'resize' event is emitted when a window is resized. + * + * @event resize + * @param {OO.ui.Window} win Window that was resized + */ + +/* Static Properties */ + +/** + * Map of the symbolic name of each window size and its CSS properties. + * + * @static + * @inheritable + * @property {Object} + */ +OO.ui.WindowManager.static.sizes = { + small: { + width: 300 + }, + medium: { + width: 500 + }, + large: { + width: 700 + }, + larger: { + width: 900 + }, + full: { + // These can be non-numeric because they are never used in calculations + width: '100%', + height: '100%' + } +}; + +/** + * Symbolic name of the default window size. + * + * The default size is used if the window's requested size is not recognized. + * + * @static + * @inheritable + * @property {string} + */ +OO.ui.WindowManager.static.defaultSize = 'medium'; + +/* Methods */ + +/** + * Handle window resize events. + * + * @private + * @param {jQuery.Event} e Window resize event + */ +OO.ui.WindowManager.prototype.onWindowResize = function () { + clearTimeout( this.onWindowResizeTimeout ); + this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 ); +}; + +/** + * Handle window resize events. + * + * @private + * @param {jQuery.Event} e Window resize event + */ +OO.ui.WindowManager.prototype.afterWindowResize = function () { + if ( this.currentWindow ) { + this.updateWindowSize( this.currentWindow ); + } +}; + +/** + * Check if window is opening. + * + * @param {OO.ui.Window} win Window to check + * @return {boolean} Window is opening + */ +OO.ui.WindowManager.prototype.isOpening = function ( win ) { + return win === this.currentWindow && !!this.lifecycle && + this.lifecycle.isOpening(); +}; + +/** + * Check if window is closing. + * + * @param {OO.ui.Window} win Window to check + * @return {boolean} Window is closing + */ +OO.ui.WindowManager.prototype.isClosing = function ( win ) { + return win === this.currentWindow && !!this.lifecycle && + this.lifecycle.isClosing(); +}; + +/** + * Check if window is opened. + * + * @param {OO.ui.Window} win Window to check + * @return {boolean} Window is opened + */ +OO.ui.WindowManager.prototype.isOpened = function ( win ) { + return win === this.currentWindow && !!this.lifecycle && + this.lifecycle.isOpened(); +}; + +/** + * Check if a window is being managed. + * + * @param {OO.ui.Window} win Window to check + * @return {boolean} Window is being managed + */ +OO.ui.WindowManager.prototype.hasWindow = function ( win ) { + var name; + + for ( name in this.windows ) { + if ( this.windows[ name ] === win ) { + return true; + } + } + + return false; +}; + +/** + * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process. + * + * @param {OO.ui.Window} win Window being opened + * @param {Object} [data] Window opening data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getSetupDelay = function () { + return 0; +}; + +/** + * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process. + * + * @param {OO.ui.Window} win Window being opened + * @param {Object} [data] Window opening data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getReadyDelay = function () { + return 0; +}; + +/** + * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process. + * + * @param {OO.ui.Window} win Window being closed + * @param {Object} [data] Window closing data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getHoldDelay = function () { + return 0; +}; + +/** + * Get the number of milliseconds to wait after the ‘hold’ process has finished before + * executing the ‘teardown’ process. + * + * @param {OO.ui.Window} win Window being closed + * @param {Object} [data] Window closing data + * @return {number} Milliseconds to wait + */ +OO.ui.WindowManager.prototype.getTeardownDelay = function () { + return this.modal ? 250 : 0; +}; + +/** + * Get a window by its symbolic name. + * + * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be + * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3] + * for more information about using factories. + * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers + * + * @param {string} name Symbolic name of the window + * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error + * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory. + * @throws {Error} An error is thrown if the named window is not recognized as a managed window. + */ +OO.ui.WindowManager.prototype.getWindow = function ( name ) { + var deferred = $.Deferred(), + win = this.windows[ name ]; + + if ( !( win instanceof OO.ui.Window ) ) { + if ( this.factory ) { + if ( !this.factory.lookup( name ) ) { + deferred.reject( new OO.ui.Error( + 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory' + ) ); + } else { + win = this.factory.create( name ); + this.addWindows( [ win ] ); + deferred.resolve( win ); + } + } else { + deferred.reject( new OO.ui.Error( + 'Cannot get unmanaged window: symbolic name unrecognized as a managed window' + ) ); + } + } else { + deferred.resolve( win ); + } + + return deferred.promise(); +}; + +/** + * Get current window. + * + * @return {OO.ui.Window|null} Currently opening/opened/closing window + */ +OO.ui.WindowManager.prototype.getCurrentWindow = function () { + return this.currentWindow; +}; + +/** + * Open a window. + * + * @param {OO.ui.Window|string} win Window object or symbolic name of window to open + * @param {Object} [data] Window opening data + * @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when closed. + * Defaults the current activeElement. If set to null, focus isn't changed on close. + * @return {OO.ui.WindowInstance|jQuery.Promise} A lifecycle object representing this particular + * opening of the window. For backwards-compatibility, then object is also a Thenable that is resolved + * when the window is done opening, with nested promise for when closing starts. This behaviour + * is deprecated and is not compatible with jQuery 3. See T163510. + * @fires opening + */ +OO.ui.WindowManager.prototype.openWindow = function ( win, data, lifecycle, compatOpening ) { + var error, + manager = this; + data = data || {}; + + // Internal parameter 'lifecycle' allows this method to always return + // a lifecycle even if the window still needs to be created + // asynchronously when 'win' is a string. + lifecycle = lifecycle || new OO.ui.WindowInstance(); + compatOpening = compatOpening || $.Deferred(); + + // Turn lifecycle into a Thenable for backwards-compatibility with + // the deprecated nested-promise behaviour, see T163510. + [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ] + .forEach( function ( method ) { + lifecycle[ method ] = function () { + OO.ui.warnDeprecation( + 'Using the return value of openWindow as a promise is deprecated. ' + + 'Use .openWindow( ... ).opening.' + method + '( ... ) instead.' + ); + return compatOpening[ method ].apply( this, arguments ); + }; + } ); + + // Argument handling + if ( typeof win === 'string' ) { + this.getWindow( win ).then( + function ( win ) { + manager.openWindow( win, data, lifecycle, compatOpening ); + }, + function ( err ) { + lifecycle.deferreds.opening.reject( err ); + } + ); + return lifecycle; + } + + // Error handling + if ( !this.hasWindow( win ) ) { + error = 'Cannot open window: window is not attached to manager'; + } else if ( this.lifecycle && this.lifecycle.isOpened() ) { + error = 'Cannot open window: another window is open'; + } else if ( this.preparingToOpen || ( this.lifecycle && this.lifecycle.isOpening() ) ) { + error = 'Cannot open window: another window is opening'; + } + + if ( error ) { + compatOpening.reject( new OO.ui.Error( error ) ); + lifecycle.deferreds.opening.reject( new OO.ui.Error( error ) ); + return lifecycle; + } + + // If a window is currently closing, wait for it to complete + this.preparingToOpen = $.when( this.lifecycle && this.lifecycle.closed ); + // Ensure handlers get called after preparingToOpen is set + this.preparingToOpen.done( function () { + if ( manager.modal ) { + manager.toggleGlobalEvents( true ); + manager.toggleAriaIsolation( true ); + } + manager.$returnFocusTo = data.$returnFocusTo !== undefined ? data.$returnFocusTo : $( document.activeElement ); + manager.currentWindow = win; + manager.lifecycle = lifecycle; + manager.preparingToOpen = null; + manager.emit( 'opening', win, compatOpening, data ); + lifecycle.deferreds.opening.resolve( data ); + setTimeout( function () { + manager.compatOpened = $.Deferred(); + win.setup( data ).then( function () { + manager.updateWindowSize( win ); + compatOpening.notify( { state: 'setup' } ); + setTimeout( function () { + win.ready( data ).then( function () { + compatOpening.notify( { state: 'ready' } ); + lifecycle.deferreds.opened.resolve( data ); + compatOpening.resolve( manager.compatOpened.promise(), data ); + }, function () { + lifecycle.deferreds.opened.reject(); + compatOpening.reject(); + manager.closeWindow( win ); + } ); + }, manager.getReadyDelay() ); + }, function () { + lifecycle.deferreds.opened.reject(); + compatOpening.reject(); + manager.closeWindow( win ); + } ); + }, manager.getSetupDelay() ); + } ); + + return lifecycle; +}; + +/** + * Close a window. + * + * @param {OO.ui.Window|string} win Window object or symbolic name of window to close + * @param {Object} [data] Window closing data + * @return {OO.ui.WindowInstance|jQuery.Promise} A lifecycle object representing this particular + * opening of the window. For backwards-compatibility, the object is also a Thenable that is resolved + * when the window is done closing, see T163510. + * @fires closing + */ +OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) { + var error, + manager = this, + compatClosing = $.Deferred(), + lifecycle = this.lifecycle, + compatOpened; + + // Argument handling + if ( typeof win === 'string' ) { + win = this.windows[ win ]; + } else if ( !this.hasWindow( win ) ) { + win = null; + } + + // Error handling + if ( !lifecycle ) { + error = 'Cannot close window: no window is currently open'; + } else if ( !win ) { + error = 'Cannot close window: window is not attached to manager'; + } else if ( win !== this.currentWindow || this.lifecycle.isClosed() ) { + error = 'Cannot close window: window already closed with different data'; + } else if ( this.preparingToClose || this.lifecycle.isClosing() ) { + error = 'Cannot close window: window already closing with different data'; + } + + if ( error ) { + // This function was called for the wrong window and we don't want to mess with the current + // window's state. + lifecycle = new OO.ui.WindowInstance(); + // Pretend the window has been opened, so that we can pretend to fail to close it. + lifecycle.deferreds.opening.resolve( {} ); + lifecycle.deferreds.opened.resolve( {} ); + } + + // Turn lifecycle into a Thenable for backwards-compatibility with + // the deprecated nested-promise behaviour, see T163510. + [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ] + .forEach( function ( method ) { + lifecycle[ method ] = function () { + OO.ui.warnDeprecation( + 'Using the return value of closeWindow as a promise is deprecated. ' + + 'Use .closeWindow( ... ).closed.' + method + '( ... ) instead.' + ); + return compatClosing[ method ].apply( this, arguments ); + }; + } ); + + if ( error ) { + compatClosing.reject( new OO.ui.Error( error ) ); + lifecycle.deferreds.closing.reject( new OO.ui.Error( error ) ); + return lifecycle; + } + + // If the window is currently opening, close it when it's done + this.preparingToClose = $.when( this.lifecycle.opened ); + // Ensure handlers get called after preparingToClose is set + this.preparingToClose.always( function () { + manager.preparingToClose = null; + manager.emit( 'closing', win, compatClosing, data ); + lifecycle.deferreds.closing.resolve( data ); + compatOpened = manager.compatOpened; + manager.compatOpened = null; + compatOpened.resolve( compatClosing.promise(), data ); + setTimeout( function () { + win.hold( data ).then( function () { + compatClosing.notify( { state: 'hold' } ); + setTimeout( function () { + win.teardown( data ).then( function () { + compatClosing.notify( { state: 'teardown' } ); + if ( manager.modal ) { + manager.toggleGlobalEvents( false ); + manager.toggleAriaIsolation( false ); + } + if ( manager.$returnFocusTo && manager.$returnFocusTo.length ) { + manager.$returnFocusTo[ 0 ].focus(); + } + manager.currentWindow = null; + manager.lifecycle = null; + lifecycle.deferreds.closed.resolve( data ); + compatClosing.resolve( data ); + } ); + }, manager.getTeardownDelay() ); + } ); + }, manager.getHoldDelay() ); + } ); + + return lifecycle; +}; + +/** + * Add windows to the window manager. + * + * Windows can be added by reference, symbolic name, or explicitly defined symbolic names. + * See the [OOjs ui documentation on MediaWiki] [2] for examples. + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers + * + * This function can be called in two manners: + * + * 1. `.addWindows( [ windowA, windowB, ... ] )` (where `windowA`, `windowB` are OO.ui.Window objects) + * + * This syntax registers windows under the symbolic names defined in their `.static.name` + * properties. For example, if `windowA.constructor.static.name` is `'nameA'`, calling + * `.openWindow( 'nameA' )` afterwards will open the window `windowA`. This syntax requires the + * static name to be set, otherwise an exception will be thrown. + * + * This is the recommended way, as it allows for an easier switch to using a window factory. + * + * 2. `.addWindows( { nameA: windowA, nameB: windowB, ... } )` + * + * This syntax registers windows under the explicitly given symbolic names. In this example, + * calling `.openWindow( 'nameA' )` afterwards will open the window `windowA`, regardless of what + * its `.static.name` is set to. The static name is not required to be set. + * + * This should only be used if you need to override the default symbolic names. + * + * Example: + * + * var windowManager = new OO.ui.WindowManager(); + * $( 'body' ).append( windowManager.$element ); + * + * // Add a window under the default name: see OO.ui.MessageDialog.static.name + * windowManager.addWindows( [ new OO.ui.MessageDialog() ] ); + * // Add a window under an explicit name + * windowManager.addWindows( { myMessageDialog: new OO.ui.MessageDialog() } ); + * + * // Open window by default name + * windowManager.openWindow( 'message' ); + * // Open window by explicitly given name + * windowManager.openWindow( 'myMessageDialog' ); + * + * + * @param {Object.|OO.ui.Window[]} windows An array of window objects specified + * by reference, symbolic name, or explicitly defined symbolic names. + * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an + * explicit nor a statically configured symbolic name. + */ +OO.ui.WindowManager.prototype.addWindows = function ( windows ) { + var i, len, win, name, list; + + if ( Array.isArray( windows ) ) { + // Convert to map of windows by looking up symbolic names from static configuration + list = {}; + for ( i = 0, len = windows.length; i < len; i++ ) { + name = windows[ i ].constructor.static.name; + if ( !name ) { + throw new Error( 'Windows must have a `name` static property defined.' ); + } + list[ name ] = windows[ i ]; + } + } else if ( OO.isPlainObject( windows ) ) { + list = windows; + } + + // Add windows + for ( name in list ) { + win = list[ name ]; + this.windows[ name ] = win.toggle( false ); + this.$element.append( win.$element ); + win.setManager( this ); + } +}; + +/** + * Remove the specified windows from the windows manager. + * + * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use + * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no + * longer listens to events, use the #destroy method. + * + * @param {string[]} names Symbolic names of windows to remove + * @return {jQuery.Promise} Promise resolved when window is closed and removed + * @throws {Error} An error is thrown if the named windows are not managed by the window manager. + */ +OO.ui.WindowManager.prototype.removeWindows = function ( names ) { + var i, len, win, name, cleanupWindow, + manager = this, + promises = [], + cleanup = function ( name, win ) { + delete manager.windows[ name ]; + win.$element.detach(); + }; + + for ( i = 0, len = names.length; i < len; i++ ) { + name = names[ i ]; + win = this.windows[ name ]; + if ( !win ) { + throw new Error( 'Cannot remove window' ); + } + cleanupWindow = cleanup.bind( null, name, win ); + promises.push( this.closeWindow( name ).closed.then( cleanupWindow, cleanupWindow ) ); + } + + return $.when.apply( $, promises ); +}; + +/** + * Remove all windows from the window manager. + * + * Windows will be closed before they are removed. Note that the window manager, though not in use, will still + * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead. + * To remove just a subset of windows, use the #removeWindows method. + * + * @return {jQuery.Promise} Promise resolved when all windows are closed and removed + */ +OO.ui.WindowManager.prototype.clearWindows = function () { + return this.removeWindows( Object.keys( this.windows ) ); +}; + +/** + * Set dialog size. In general, this method should not be called directly. + * + * Fullscreen mode will be used if the dialog is too wide to fit in the screen. + * + * @param {OO.ui.Window} win Window to update, should be the current window + * @chainable + */ +OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) { + var isFullscreen; + + // Bypass for non-current, and thus invisible, windows + if ( win !== this.currentWindow ) { + return; + } + + isFullscreen = win.getSize() === 'full'; + + this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen ); + this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen ); + win.setDimensions( win.getSizeProperties() ); + + this.emit( 'resize', win ); + + return this; +}; + +/** + * Bind or unbind global events for scrolling. + * + * @private + * @param {boolean} [on] Bind global events + * @chainable + */ +OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) { + var scrollWidth, bodyMargin, + $body = $( this.getElementDocument().body ), + // We could have multiple window managers open so only modify + // the body css at the bottom of the stack + stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0; + + on = on === undefined ? !!this.globalEvents : !!on; + + if ( on ) { + if ( !this.globalEvents ) { + $( this.getElementWindow() ).on( { + // Start listening for top-level window dimension changes + 'orientationchange resize': this.onWindowResizeHandler + } ); + if ( stackDepth === 0 ) { + scrollWidth = window.innerWidth - document.documentElement.clientWidth; + bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0; + $body.css( { + overflow: 'hidden', + 'margin-right': bodyMargin + scrollWidth + } ); + } + stackDepth++; + this.globalEvents = true; + } + } else if ( this.globalEvents ) { + $( this.getElementWindow() ).off( { + // Stop listening for top-level window dimension changes + 'orientationchange resize': this.onWindowResizeHandler + } ); + stackDepth--; + if ( stackDepth === 0 ) { + $body.css( { + overflow: '', + 'margin-right': '' + } ); + } + this.globalEvents = false; + } + $body.data( 'windowManagerGlobalEvents', stackDepth ); + + return this; +}; + +/** + * Toggle screen reader visibility of content other than the window manager. + * + * @private + * @param {boolean} [isolate] Make only the window manager visible to screen readers + * @chainable + */ +OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) { + isolate = isolate === undefined ? !this.$ariaHidden : !!isolate; + + if ( isolate ) { + if ( !this.$ariaHidden ) { + // Hide everything other than the window manager from screen readers + this.$ariaHidden = $( 'body' ) + .children() + .not( this.$element.parentsUntil( 'body' ).last() ) + .attr( 'aria-hidden', '' ); + } + } else if ( this.$ariaHidden ) { + // Restore screen reader visibility + this.$ariaHidden.removeAttr( 'aria-hidden' ); + this.$ariaHidden = null; + } + + return this; +}; + +/** + * Destroy the window manager. + * + * Destroying the window manager ensures that it will no longer listen to events. If you would like to + * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method + * instead. + */ +OO.ui.WindowManager.prototype.destroy = function () { + this.toggleGlobalEvents( false ); + this.toggleAriaIsolation( false ); + this.clearWindows(); + this.$element.remove(); +}; + +/** + * A window is a container for elements that are in a child frame. They are used with + * a window manager (OO.ui.WindowManager), which is used to open and close the window and control + * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’, + * ‘large’), which is interpreted by the window manager. If the requested size is not recognized, + * the window manager will choose a sensible fallback. + * + * The lifecycle of a window has three primary stages (opening, opened, and closing) in which + * different processes are executed: + * + * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow + * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open + * the window. + * + * - {@link #getSetupProcess} method is called and its result executed + * - {@link #getReadyProcess} method is called and its result executed + * + * **opened**: The window is now open + * + * **closing**: The closing stage begins when the window manager's + * {@link OO.ui.WindowManager#closeWindow closeWindow} + * or the window's {@link #close} methods are used, and the window manager begins to close the window. + * + * - {@link #getHoldProcess} method is called and its result executed + * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed + * + * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses + * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess + * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous + * processing can complete. Always assume window processes are executed asynchronously. + * + * For more information, please see the [OOjs UI documentation on MediaWiki] [1]. + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows + * + * @abstract + * @class + * @extends OO.ui.Element + * @mixins OO.EventEmitter + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or + * `full`. If omitted, the value of the {@link #static-size static size} property will be used. + */ +OO.ui.Window = function OoUiWindow( config ) { + // Configuration initialization + config = config || {}; + + // Parent constructor + OO.ui.Window.parent.call( this, config ); + + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.manager = null; + this.size = config.size || this.constructor.static.size; + this.$frame = $( '
' ); + /** + * Overlay element to use for the `$overlay` configuration option of widgets that support it. + * Things put inside of it are overlaid on top of the window and are not bound to its dimensions. + * See . + * + * MyDialog.prototype.initialize = function () { + * ... + * var popupButton = new OO.ui.PopupButtonWidget( { + * $overlay: this.$overlay, + * label: 'Popup button', + * popup: { + * $content: $( '

Popup contents.

Popup contents.

Popup contents.

' ), + * padded: true + * } + * } ); + * ... + * }; + * + * @property {jQuery} + */ + this.$overlay = $( '
' ); + this.$content = $( '
' ); + + this.$focusTrapBefore = $( '
' ).prop( 'tabIndex', 0 ); + this.$focusTrapAfter = $( '
' ).prop( 'tabIndex', 0 ); + this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter ); + + // Initialization + this.$overlay.addClass( 'oo-ui-window-overlay' ); + this.$content + .addClass( 'oo-ui-window-content' ) + .attr( 'tabindex', 0 ); + this.$frame + .addClass( 'oo-ui-window-frame' ) + .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter ); + + this.$element + .addClass( 'oo-ui-window' ) + .append( this.$frame, this.$overlay ); + + // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods + // that reference properties not initialized at that time of parent class construction + // TODO: Find a better way to handle post-constructor setup + this.visible = false; + this.$element.addClass( 'oo-ui-element-hidden' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.Window, OO.ui.Element ); +OO.mixinClass( OO.ui.Window, OO.EventEmitter ); + +/* Static Properties */ + +/** + * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`. + * + * The static size is used if no #size is configured during construction. + * + * @static + * @inheritable + * @property {string} + */ +OO.ui.Window.static.size = 'medium'; + +/* Methods */ + +/** + * Handle mouse down events. + * + * @private + * @param {jQuery.Event} e Mouse down event + */ +OO.ui.Window.prototype.onMouseDown = function ( e ) { + // Prevent clicking on the click-block from stealing focus + if ( e.target === this.$element[ 0 ] ) { + return false; + } +}; + +/** + * Check if the window has been initialized. + * + * Initialization occurs when a window is added to a manager. + * + * @return {boolean} Window has been initialized + */ +OO.ui.Window.prototype.isInitialized = function () { + return !!this.manager; +}; + +/** + * Check if the window is visible. + * + * @return {boolean} Window is visible + */ +OO.ui.Window.prototype.isVisible = function () { + return this.visible; +}; + +/** + * Check if the window is opening. + * + * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening} + * method. + * + * @return {boolean} Window is opening + */ +OO.ui.Window.prototype.isOpening = function () { + return this.manager.isOpening( this ); +}; + +/** + * Check if the window is closing. + * + * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method. + * + * @return {boolean} Window is closing + */ +OO.ui.Window.prototype.isClosing = function () { + return this.manager.isClosing( this ); +}; + +/** + * Check if the window is opened. + * + * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method. + * + * @return {boolean} Window is opened + */ +OO.ui.Window.prototype.isOpened = function () { + return this.manager.isOpened( this ); +}; + +/** + * Get the window manager. + * + * All windows must be attached to a window manager, which is used to open + * and close the window and control its presentation. + * + * @return {OO.ui.WindowManager} Manager of window + */ +OO.ui.Window.prototype.getManager = function () { + return this.manager; +}; + +/** + * Get the symbolic name of the window size (e.g., `small` or `medium`). + * + * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full` + */ +OO.ui.Window.prototype.getSize = function () { + var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ), + sizes = this.manager.constructor.static.sizes, + size = this.size; + + if ( !sizes[ size ] ) { + size = this.manager.constructor.static.defaultSize; + } + if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) { + size = 'full'; + } + + return size; +}; + +/** + * Get the size properties associated with the current window size + * + * @return {Object} Size properties + */ +OO.ui.Window.prototype.getSizeProperties = function () { + return this.manager.constructor.static.sizes[ this.getSize() ]; +}; + +/** + * Disable transitions on window's frame for the duration of the callback function, then enable them + * back. + * + * @private + * @param {Function} callback Function to call while transitions are disabled + */ +OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) { + // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements. + // Disable transitions first, otherwise we'll get values from when the window was animating. + // We need to build the transition CSS properties using these specific properties since + // Firefox doesn't return anything useful when asked just for 'transition'. + var oldTransition = this.$frame.css( 'transition-property' ) + ' ' + + this.$frame.css( 'transition-duration' ) + ' ' + + this.$frame.css( 'transition-timing-function' ) + ' ' + + this.$frame.css( 'transition-delay' ); + + this.$frame.css( 'transition', 'none' ); + callback(); + + // Force reflow to make sure the style changes done inside callback + // really are not transitioned + this.$frame.height(); + this.$frame.css( 'transition', oldTransition ); +}; + +/** + * Get the height of the full window contents (i.e., the window head, body and foot together). + * + * What consistitutes the head, body, and foot varies depending on the window type. + * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body, + * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title + * and special actions in the head, and dialog content in the body. + * + * To get just the height of the dialog body, use the #getBodyHeight method. + * + * @return {number} The height of the window contents (the dialog head, body and foot) in pixels + */ +OO.ui.Window.prototype.getContentHeight = function () { + var bodyHeight, + win = this, + bodyStyleObj = this.$body[ 0 ].style, + frameStyleObj = this.$frame[ 0 ].style; + + // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements. + // Disable transitions first, otherwise we'll get values from when the window was animating. + this.withoutSizeTransitions( function () { + var oldHeight = frameStyleObj.height, + oldPosition = bodyStyleObj.position; + frameStyleObj.height = '1px'; + // Force body to resize to new width + bodyStyleObj.position = 'relative'; + bodyHeight = win.getBodyHeight(); + frameStyleObj.height = oldHeight; + bodyStyleObj.position = oldPosition; + } ); + + return ( + // Add buffer for border + ( this.$frame.outerHeight() - this.$frame.innerHeight() ) + + // Use combined heights of children + ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) ) + ); +}; + +/** + * Get the height of the window body. + * + * To get the height of the full window contents (the window body, head, and foot together), + * use #getContentHeight. + * + * When this function is called, the window will temporarily have been resized + * to height=1px, so .scrollHeight measurements can be taken accurately. + * + * @return {number} Height of the window body in pixels + */ +OO.ui.Window.prototype.getBodyHeight = function () { + return this.$body[ 0 ].scrollHeight; +}; + +/** + * Get the directionality of the frame (right-to-left or left-to-right). + * + * @return {string} Directionality: `'ltr'` or `'rtl'` + */ +OO.ui.Window.prototype.getDir = function () { + return OO.ui.Element.static.getDir( this.$content ) || 'ltr'; +}; + +/** + * Get the 'setup' process. + * + * The setup process is used to set up a window for use in a particular context, + * based on the `data` argument. This method is called during the opening phase of the window’s + * lifecycle. + * + * Override this method to add additional steps to the ‘setup’ process the parent method provides + * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods + * of OO.ui.Process. + * + * To add window content that persists between openings, you may wish to use the #initialize method + * instead. + * + * @param {Object} [data] Window opening data + * @return {OO.ui.Process} Setup process + */ +OO.ui.Window.prototype.getSetupProcess = function () { + return new OO.ui.Process(); +}; + +/** + * Get the ‘ready’ process. + * + * The ready process is used to ready a window for use in a particular + * context, based on the `data` argument. This method is called during the opening phase of + * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}. + * + * Override this method to add additional steps to the ‘ready’ process the parent method + * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} + * methods of OO.ui.Process. + * + * @param {Object} [data] Window opening data + * @return {OO.ui.Process} Ready process + */ +OO.ui.Window.prototype.getReadyProcess = function () { + return new OO.ui.Process(); +}; + +/** + * Get the 'hold' process. + * + * The hold process is used to keep a window from being used in a particular context, + * based on the `data` argument. This method is called during the closing phase of the window’s + * lifecycle. + * + * Override this method to add additional steps to the 'hold' process the parent method provides + * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods + * of OO.ui.Process. + * + * @param {Object} [data] Window closing data + * @return {OO.ui.Process} Hold process + */ +OO.ui.Window.prototype.getHoldProcess = function () { + return new OO.ui.Process(); +}; + +/** + * Get the ‘teardown’ process. + * + * The teardown process is used to teardown a window after use. During teardown, + * user interactions within the window are conveyed and the window is closed, based on the `data` + * argument. This method is called during the closing phase of the window’s lifecycle. + * + * Override this method to add additional steps to the ‘teardown’ process the parent method provides + * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods + * of OO.ui.Process. + * + * @param {Object} [data] Window closing data + * @return {OO.ui.Process} Teardown process + */ +OO.ui.Window.prototype.getTeardownProcess = function () { + return new OO.ui.Process(); +}; + +/** + * Set the window manager. + * + * This will cause the window to initialize. Calling it more than once will cause an error. + * + * @param {OO.ui.WindowManager} manager Manager for this window + * @throws {Error} An error is thrown if the method is called more than once + * @chainable + */ +OO.ui.Window.prototype.setManager = function ( manager ) { + if ( this.manager ) { + throw new Error( 'Cannot set window manager, window already has a manager' ); + } + + this.manager = manager; + this.initialize(); + + return this; +}; + +/** + * Set the window size by symbolic name (e.g., 'small' or 'medium') + * + * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or + * `full` + * @chainable + */ +OO.ui.Window.prototype.setSize = function ( size ) { + this.size = size; + this.updateSize(); + return this; +}; + +/** + * Update the window size. + * + * @throws {Error} An error is thrown if the window is not attached to a window manager + * @chainable + */ +OO.ui.Window.prototype.updateSize = function () { + if ( !this.manager ) { + throw new Error( 'Cannot update window size, must be attached to a manager' ); + } + + this.manager.updateWindowSize( this ); + + return this; +}; + +/** + * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager} + * when the window is opening. In general, setDimensions should not be called directly. + * + * To set the size of the window, use the #setSize method. + * + * @param {Object} dim CSS dimension properties + * @param {string|number} [dim.width] Width + * @param {string|number} [dim.minWidth] Minimum width + * @param {string|number} [dim.maxWidth] Maximum width + * @param {string|number} [dim.height] Height, omit to set based on height of contents + * @param {string|number} [dim.minHeight] Minimum height + * @param {string|number} [dim.maxHeight] Maximum height + * @chainable + */ +OO.ui.Window.prototype.setDimensions = function ( dim ) { + var height, + win = this, + styleObj = this.$frame[ 0 ].style; + + // Calculate the height we need to set using the correct width + if ( dim.height === undefined ) { + this.withoutSizeTransitions( function () { + var oldWidth = styleObj.width; + win.$frame.css( 'width', dim.width || '' ); + height = win.getContentHeight(); + styleObj.width = oldWidth; + } ); + } else { + height = dim.height; + } + + this.$frame.css( { + width: dim.width || '', + minWidth: dim.minWidth || '', + maxWidth: dim.maxWidth || '', + height: height || '', + minHeight: dim.minHeight || '', + maxHeight: dim.maxHeight || '' + } ); + + return this; +}; + +/** + * Initialize window contents. + * + * Before the window is opened for the first time, #initialize is called so that content that + * persists between openings can be added to the window. + * + * To set up a window with new content each time the window opens, use #getSetupProcess. + * + * @throws {Error} An error is thrown if the window is not attached to a window manager + * @chainable + */ +OO.ui.Window.prototype.initialize = function () { + if ( !this.manager ) { + throw new Error( 'Cannot initialize window, must be attached to a manager' ); + } + + // Properties + this.$head = $( '
' ); + this.$body = $( '
' ); + this.$foot = $( '
' ); + this.$document = $( this.getElementDocument() ); + + // Events + this.$element.on( 'mousedown', this.onMouseDown.bind( this ) ); + + // Initialization + this.$head.addClass( 'oo-ui-window-head' ); + this.$body.addClass( 'oo-ui-window-body' ); + this.$foot.addClass( 'oo-ui-window-foot' ); + this.$content.append( this.$head, this.$body, this.$foot ); + + return this; +}; + +/** + * Called when someone tries to focus the hidden element at the end of the dialog. + * Sends focus back to the start of the dialog. + * + * @param {jQuery.Event} event Focus event + */ +OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) { + var backwards = this.$focusTrapBefore.is( event.target ), + element = OO.ui.findFocusable( this.$content, backwards ); + if ( element ) { + // There's a focusable element inside the content, at the front or + // back depending on which focus trap we hit; select it. + element.focus(); + } else { + // There's nothing focusable inside the content. As a fallback, + // this.$content is focusable, and focusing it will keep our focus + // properly trapped. It's not a *meaningful* focus, since it's just + // the content-div for the Window, but it's better than letting focus + // escape into the page. + this.$content.focus(); + } +}; + +/** + * Open the window. + * + * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow} + * method, which returns a promise resolved when the window is done opening. + * + * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess. + * + * @param {Object} [data] Window opening data + * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected + * if the window fails to open. When the promise is resolved successfully, the first argument of the + * value is a new promise, which is resolved when the window begins closing. + * @throws {Error} An error is thrown if the window is not attached to a window manager + */ +OO.ui.Window.prototype.open = function ( data ) { + if ( !this.manager ) { + throw new Error( 'Cannot open window, must be attached to a manager' ); + } + + return this.manager.openWindow( this, data ); +}; + +/** + * Close the window. + * + * This method is a wrapper around a call to the window + * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method, + * which returns a closing promise resolved when the window is done closing. + * + * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing + * phase of the window’s lifecycle and can be used to specify closing behavior each time + * the window closes. + * + * @param {Object} [data] Window closing data + * @return {jQuery.Promise} Promise resolved when window is closed + * @throws {Error} An error is thrown if the window is not attached to a window manager + */ +OO.ui.Window.prototype.close = function ( data ) { + if ( !this.manager ) { + throw new Error( 'Cannot close window, must be attached to a manager' ); + } + + return this.manager.closeWindow( this, data ); +}; + +/** + * Setup window. + * + * 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 + * @return {jQuery.Promise} Promise resolved when window is setup + */ +OO.ui.Window.prototype.setup = function ( data ) { + var win = this; + + this.toggle( true ); + + this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this ); + this.$focusTraps.on( 'focus', this.focusTrapHandler ); + + return this.getSetupProcess( data ).execute().then( function () { + // Force redraw by asking the browser to measure the elements' widths + win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width(); + win.$content.addClass( 'oo-ui-window-content-setup' ).width(); + } ); +}; + +/** + * Ready window. + * + * 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 + * @return {jQuery.Promise} Promise resolved when window is ready + */ +OO.ui.Window.prototype.ready = function ( data ) { + var win = this; + + this.$content.focus(); + return this.getReadyProcess( data ).execute().then( function () { + // Force redraw by asking the browser to measure the elements' widths + win.$element.addClass( 'oo-ui-window-ready' ).width(); + win.$content.addClass( 'oo-ui-window-content-ready' ).width(); + } ); +}; + +/** + * Hold window. + * + * 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 + * @return {jQuery.Promise} Promise resolved when window is held + */ +OO.ui.Window.prototype.hold = function ( data ) { + var win = this; + + return this.getHoldProcess( data ).execute().then( function () { + // Get the focused element within the window's content + var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement ); + + // Blur the focused element + if ( $focus.length ) { + $focus[ 0 ].blur(); + } + + // Force redraw by asking the browser to measure the elements' widths + win.$element.removeClass( 'oo-ui-window-ready' ).width(); + win.$content.removeClass( 'oo-ui-window-content-ready' ).width(); + } ); +}; + +/** + * Teardown window. + * + * 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 + * @return {jQuery.Promise} Promise resolved when window is torn down + */ +OO.ui.Window.prototype.teardown = function ( data ) { + var win = this; + + return this.getTeardownProcess( data ).execute().then( function () { + // Force redraw by asking the browser to measure the elements' widths + win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width(); + win.$content.removeClass( 'oo-ui-window-content-setup' ).width(); + win.$focusTraps.off( 'focus', win.focusTrapHandler ); + win.toggle( false ); + } ); +}; + +/** + * The Dialog class serves as the base class for the other types of dialogs. + * Unless extended to include controls, the rendered dialog box is a simple window + * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager, + * which opens, closes, and controls the presentation of the window. See the + * [OOjs UI documentation on MediaWiki] [1] for more information. + * + * @example + * // A simple dialog window. + * function MyDialog( config ) { + * MyDialog.parent.call( this, config ); + * } + * OO.inheritClass( MyDialog, OO.ui.Dialog ); + * MyDialog.static.name = 'myDialog'; + * MyDialog.prototype.initialize = function () { + * MyDialog.parent.prototype.initialize.call( this ); + * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } ); + * this.content.$element.append( '

A simple dialog window. Press \'Esc\' to close.

' ); + * this.$body.append( this.content.$element ); + * }; + * MyDialog.prototype.getBodyHeight = function () { + * return this.content.$element.outerHeight( true ); + * }; + * var myDialog = new MyDialog( { + * size: 'medium' + * } ); + * // Create and append a window manager, which opens and closes the window. + * var windowManager = new OO.ui.WindowManager(); + * $( 'body' ).append( windowManager.$element ); + * windowManager.addWindows( [ myDialog ] ); + * // Open the window! + * windowManager.openWindow( myDialog ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs + * + * @abstract + * @class + * @extends OO.ui.Window + * @mixins OO.ui.mixin.PendingElement + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.Dialog = function OoUiDialog( config ) { + // Parent constructor + OO.ui.Dialog.parent.call( this, config ); + + // Mixin constructors + OO.ui.mixin.PendingElement.call( this ); + + // Properties + this.actions = new OO.ui.ActionSet(); + this.attachedActions = []; + this.currentAction = null; + this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this ); + + // Events + this.actions.connect( this, { + click: 'onActionClick', + change: 'onActionsChange' + } ); + + // Initialization + this.$element + .addClass( 'oo-ui-dialog' ) + .attr( 'role', 'dialog' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.Dialog, OO.ui.Window ); +OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement ); + +/* Static Properties */ + +/** + * Symbolic name of dialog. + * + * The dialog class must have a symbolic name in order to be registered with OO.Factory. + * Please see the [OOjs UI documentation on MediaWiki] [3] for more information. + * + * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers + * + * @abstract + * @static + * @inheritable + * @property {string} + */ +OO.ui.Dialog.static.name = ''; + +/** + * The dialog title. + * + * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function + * that will produce a Label node or string. The title can also be specified with data passed to the + * constructor (see #getSetupProcess). In this case, the static value will be overridden. + * + * @abstract + * @static + * @inheritable + * @property {jQuery|string|Function} + */ +OO.ui.Dialog.static.title = ''; + +/** + * An array of configured {@link OO.ui.ActionWidget action widgets}. + * + * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static + * value will be overridden. + * + * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets + * + * @static + * @inheritable + * @property {Object[]} + */ +OO.ui.Dialog.static.actions = []; + +/** + * Close the dialog when the 'Esc' key is pressed. + * + * @static + * @abstract + * @inheritable + * @property {boolean} + */ +OO.ui.Dialog.static.escapable = true; + +/* Methods */ + +/** + * Handle frame document key down events. + * + * @private + * @param {jQuery.Event} e Key down event + */ +OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) { + var actions; + if ( e.which === OO.ui.Keys.ESCAPE && this.constructor.static.escapable ) { + this.executeAction( '' ); + e.preventDefault(); + e.stopPropagation(); + } else if ( e.which === OO.ui.Keys.ENTER && ( e.ctrlKey || e.metaKey ) ) { + actions = this.actions.get( { flags: 'primary', visible: true, disabled: false } ); + if ( actions.length > 0 ) { + this.executeAction( actions[ 0 ].getAction() ); + e.preventDefault(); + e.stopPropagation(); + } + } +}; + +/** + * Handle action click events. + * + * @private + * @param {OO.ui.ActionWidget} action Action that was clicked + */ +OO.ui.Dialog.prototype.onActionClick = function ( action ) { + if ( !this.isPending() ) { + this.executeAction( action.getAction() ); + } +}; + +/** + * Handle actions change event. + * + * @private + */ +OO.ui.Dialog.prototype.onActionsChange = function () { + this.detachActions(); + if ( !this.isClosing() ) { + this.attachActions(); + } +}; + +/** + * Get the set of actions used by the dialog. + * + * @return {OO.ui.ActionSet} + */ +OO.ui.Dialog.prototype.getActions = function () { + return this.actions; +}; + +/** + * Get a process for taking action. + * + * When you override this method, you can create a new OO.ui.Process and return it, or add additional + * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'} + * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process. + * + * @param {string} [action] Symbolic name of action + * @return {OO.ui.Process} Action process + */ +OO.ui.Dialog.prototype.getActionProcess = function ( action ) { + return new OO.ui.Process() + .next( function () { + if ( !action ) { + // An empty action always closes the dialog without data, which should always be + // safe and make no changes + this.close(); + } + }, this ); +}; + +/** + * @inheritdoc + * + * @param {Object} [data] Dialog opening data + * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use + * the {@link #static-title static title} + * @param {Object[]} [data.actions] List of configuration options for each + * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}. + */ +OO.ui.Dialog.prototype.getSetupProcess = function ( data ) { + data = data || {}; + + // Parent method + return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data ) + .next( function () { + var config = this.constructor.static, + actions = data.actions !== undefined ? data.actions : config.actions, + title = data.title !== undefined ? data.title : config.title; + + this.title.setLabel( title ).setTitle( title ); + this.actions.add( this.getActionWidgets( actions ) ); + + this.$element.on( 'keydown', this.onDialogKeyDownHandler ); + }, this ); +}; + +/** + * @inheritdoc + */ +OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) { + // Parent method + return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data ) + .first( function () { + this.$element.off( 'keydown', this.onDialogKeyDownHandler ); + + this.actions.clear(); + this.currentAction = null; + }, this ); +}; + +/** + * @inheritdoc + */ +OO.ui.Dialog.prototype.initialize = function () { + // Parent method + OO.ui.Dialog.parent.prototype.initialize.call( this ); + + // Properties + this.title = new OO.ui.LabelWidget(); + + // Initialization + this.$content.addClass( 'oo-ui-dialog-content' ); + this.$element.attr( 'aria-labelledby', this.title.getElementId() ); + this.setPendingElement( this.$head ); +}; + +/** + * Get action widgets from a list of configs + * + * @param {Object[]} actions Action widget configs + * @return {OO.ui.ActionWidget[]} Action widgets + */ +OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) { + var i, len, widgets = []; + for ( i = 0, len = actions.length; i < len; i++ ) { + widgets.push( + new OO.ui.ActionWidget( actions[ i ] ) + ); + } + return widgets; +}; + +/** + * Attach action actions. + * + * @protected + */ +OO.ui.Dialog.prototype.attachActions = function () { + // Remember the list of potentially attached actions + this.attachedActions = this.actions.get(); +}; + +/** + * Detach action actions. + * + * @protected + * @chainable + */ +OO.ui.Dialog.prototype.detachActions = function () { + var i, len; + + // Detach all actions that may have been previously attached + for ( i = 0, len = this.attachedActions.length; i < len; i++ ) { + this.attachedActions[ i ].$element.detach(); + } + this.attachedActions = []; +}; + +/** + * Execute an action. + * + * @param {string} action Symbolic name of action to execute + * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails + */ +OO.ui.Dialog.prototype.executeAction = function ( action ) { + this.pushPending(); + this.currentAction = action; + return this.getActionProcess( action ).execute() + .always( this.popPending.bind( this ) ); +}; + +/** + * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box + * consists of a header that contains the dialog title, a body with the message, and a footer that + * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type + * of {@link OO.ui.Dialog dialog} that is usually instantiated directly. + * + * There are two basic types of message dialogs, confirmation and alert: + * + * - **confirmation**: the dialog title describes what a progressive action will do and the message provides + * more details about the consequences. + * - **alert**: the dialog title describes which event occurred and the message provides more information + * about why the event occurred. + * + * The MessageDialog class specifies two actions: ‘accept’, the primary + * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window, + * passing along the selected action. + * + * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1]. + * + * @example + * // Example: Creating and opening a message dialog window. + * var messageDialog = new OO.ui.MessageDialog(); + * + * // Create and append a window manager. + * var windowManager = new OO.ui.WindowManager(); + * $( 'body' ).append( windowManager.$element ); + * windowManager.addWindows( [ messageDialog ] ); + * // Open the window. + * windowManager.openWindow( messageDialog, { + * title: 'Basic message dialog', + * message: 'This is the message' + * } ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs + * + * @class + * @extends OO.ui.Dialog + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.MessageDialog = function OoUiMessageDialog( config ) { + // Parent constructor + OO.ui.MessageDialog.parent.call( this, config ); + + // Properties + this.verticalActionLayout = null; + + // Initialization + this.$element.addClass( 'oo-ui-messageDialog' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog ); + +/* Static Properties */ + +/** + * @static + * @inheritdoc + */ +OO.ui.MessageDialog.static.name = 'message'; + +/** + * @static + * @inheritdoc + */ +OO.ui.MessageDialog.static.size = 'small'; + +/** + * Dialog title. + * + * The title of a confirmation dialog describes what a progressive action will do. The + * title of an alert dialog describes which event occurred. + * + * @static + * @inheritable + * @property {jQuery|string|Function|null} + */ +OO.ui.MessageDialog.static.title = null; + +/** + * The message displayed in the dialog body. + * + * A confirmation message describes the consequences of a progressive action. An alert + * message describes why an event occurred. + * + * @static + * @inheritable + * @property {jQuery|string|Function|null} + */ +OO.ui.MessageDialog.static.message = null; + +/** + * @static + * @inheritdoc + */ +OO.ui.MessageDialog.static.actions = [ + // Note that OO.ui.alert() and OO.ui.confirm() rely on these. + { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' }, + { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' } +]; + +/* Methods */ + +/** + * @inheritdoc + */ +OO.ui.MessageDialog.prototype.setManager = function ( manager ) { + OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager ); + + // Events + this.manager.connect( this, { + resize: 'onResize' + } ); + + return this; +}; + +/** + * Handle window resized events. + * + * @private + */ +OO.ui.MessageDialog.prototype.onResize = function () { + var dialog = this; + dialog.fitActions(); + // Wait for CSS transition to finish and do it again :( + setTimeout( function () { + dialog.fitActions(); + }, 300 ); +}; + +/** + * Toggle action layout between vertical and horizontal. + * + * @private + * @param {boolean} [value] Layout actions vertically, omit to toggle + * @chainable + */ +OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) { + value = value === undefined ? !this.verticalActionLayout : !!value; + + if ( value !== this.verticalActionLayout ) { + this.verticalActionLayout = value; + this.$actions + .toggleClass( 'oo-ui-messageDialog-actions-vertical', value ) + .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value ); + } + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) { + if ( action ) { + return new OO.ui.Process( function () { + this.close( { action: action } ); + }, this ); + } + return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action ); +}; + +/** + * @inheritdoc + * + * @param {Object} [data] Dialog opening data + * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed + * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence + * @param {string} [data.size] Symbolic name of the dialog size, see OO.ui.Window + * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each + * action item + */ +OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) { + data = data || {}; + + // Parent method + return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data ) + .next( function () { + this.title.setLabel( + data.title !== undefined ? data.title : this.constructor.static.title + ); + this.message.setLabel( + data.message !== undefined ? data.message : this.constructor.static.message + ); + this.size = data.size !== undefined ? data.size : this.constructor.static.size; + }, this ); +}; + +/** + * @inheritdoc + */ +OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) { + data = data || {}; + + // Parent method + return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data ) + .next( function () { + // Focus the primary action button + var actions = this.actions.get(); + actions = actions.filter( function ( action ) { + return action.getFlags().indexOf( 'primary' ) > -1; + } ); + if ( actions.length > 0 ) { + actions[ 0 ].focus(); + } + }, this ); +}; + +/** + * @inheritdoc + */ +OO.ui.MessageDialog.prototype.getBodyHeight = function () { + var bodyHeight, oldOverflow, + $scrollable = this.container.$element; + + oldOverflow = $scrollable[ 0 ].style.overflow; + $scrollable[ 0 ].style.overflow = 'hidden'; + + OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] ); + + bodyHeight = 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.parent.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, + activeElement = document.activeElement; + + $scrollable[ 0 ].style.overflow = 'hidden'; + + OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] ); + + // Check reconsiderScrollbars didn't destroy our focus, as we + // are doing this after the ready process. + if ( activeElement && activeElement !== document.activeElement && activeElement.focus ) { + activeElement.focus(); + } + + $scrollable[ 0 ].style.overflow = oldOverflow; + }, 300 ); + + return this; +}; + +/** + * @inheritdoc + */ +OO.ui.MessageDialog.prototype.initialize = function () { + // Parent method + OO.ui.MessageDialog.parent.prototype.initialize.call( this ); + + // Properties + this.$actions = $( '
' ); + this.container = new OO.ui.PanelLayout( { + scrollable: true, classes: [ 'oo-ui-messageDialog-container' ] + } ); + this.text = new OO.ui.PanelLayout( { + padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ] + } ); + this.message = new OO.ui.LabelWidget( { + classes: [ 'oo-ui-messageDialog-message' ] + } ); + + // Initialization + this.title.$element.addClass( 'oo-ui-messageDialog-title' ); + this.$content.addClass( 'oo-ui-messageDialog-content' ); + this.container.$element.append( this.text.$element ); + this.text.$element.append( this.title.$element, this.message.$element ); + this.$body.append( this.container.$element ); + this.$actions.addClass( 'oo-ui-messageDialog-actions' ); + this.$foot.append( this.$actions ); +}; + +/** + * @inheritdoc + */ +OO.ui.MessageDialog.prototype.attachActions = function () { + var i, len, other, special, others; + + // Parent method + OO.ui.MessageDialog.parent.prototype.attachActions.call( this ); + + special = this.actions.getSpecial(); + others = this.actions.getOthers(); + + if ( special.safe ) { + this.$actions.append( special.safe.$element ); + special.safe.toggleFramed( false ); + } + if ( others.length ) { + for ( i = 0, len = others.length; i < len; i++ ) { + other = others[ i ]; + this.$actions.append( other.$element ); + other.toggleFramed( false ); + } + } + if ( special.primary ) { + this.$actions.append( special.primary.$element ); + special.primary.toggleFramed( false ); + } + + if ( !this.isOpening() ) { + // If the dialog is currently opening, this will be called automatically soon. + // This also calls #fitActions. + this.updateSize(); + } +}; + +/** + * Fit action actions into columns or rows. + * + * Columns will be used if all labels can fit without overflow, otherwise rows will be used. + * + * @private + */ +OO.ui.MessageDialog.prototype.fitActions = function () { + var i, len, action, + previous = this.verticalActionLayout, + actions = this.actions.get(); + + // Detect clipping + this.toggleVerticalActionLayout( false ); + for ( i = 0, len = actions.length; i < len; i++ ) { + action = actions[ i ]; + if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) { + this.toggleVerticalActionLayout( true ); + break; + } + } + + // Move the body out of the way of the foot + this.$body.css( 'bottom', this.$foot.outerHeight( true ) ); + + if ( this.verticalActionLayout !== previous ) { + // We changed the layout, window height might need to be updated. + this.updateSize(); + } +}; + +/** + * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary + * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error + * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when + * relevant. The ProcessDialog class is always extended and customized with the actions and content + * required for each process. + * + * The process dialog box consists of a header that visually represents the ‘working’ state of long + * processes with an animation. The header contains the dialog title as well as + * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and + * a ‘primary’ action on the right (e.g., ‘Done’). + * + * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}. + * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples. + * + * @example + * // Example: Creating and opening a process dialog window. + * function MyProcessDialog( config ) { + * MyProcessDialog.parent.call( this, config ); + * } + * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog ); + * + * MyProcessDialog.static.name = 'myProcessDialog'; + * MyProcessDialog.static.title = 'Process dialog'; + * MyProcessDialog.static.actions = [ + * { action: 'save', label: 'Done', flags: 'primary' }, + * { label: 'Cancel', flags: 'safe' } + * ]; + * + * MyProcessDialog.prototype.initialize = function () { + * MyProcessDialog.parent.prototype.initialize.apply( this, arguments ); + * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } ); + * this.content.$element.append( '

This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action) on the right.

' ); + * this.$body.append( this.content.$element ); + * }; + * MyProcessDialog.prototype.getActionProcess = function ( action ) { + * var dialog = this; + * if ( action ) { + * return new OO.ui.Process( function () { + * dialog.close( { action: action } ); + * } ); + * } + * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action ); + * }; + * + * var windowManager = new OO.ui.WindowManager(); + * $( 'body' ).append( windowManager.$element ); + * + * var dialog = new MyProcessDialog(); + * windowManager.addWindows( [ dialog ] ); + * windowManager.openWindow( dialog ); + * + * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs + * + * @abstract + * @class + * @extends OO.ui.Dialog + * + * @constructor + * @param {Object} [config] Configuration options + */ +OO.ui.ProcessDialog = function OoUiProcessDialog( config ) { + // Parent constructor + OO.ui.ProcessDialog.parent.call( this, config ); + + // Properties + this.fitOnOpen = false; + + // Initialization + this.$element.addClass( 'oo-ui-processDialog' ); +}; + +/* Setup */ + +OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog ); + +/* Methods */ + +/** + * Handle dismiss button click events. + * + * Hides errors. + * + * @private + */ +OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () { + this.hideErrors(); +}; + +/** + * Handle retry button click events. + * + * Hides errors and then tries again. + * + * @private + */ +OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () { + this.hideErrors(); + this.executeAction( this.currentAction ); +}; + +/** + * @inheritdoc + */ +OO.ui.ProcessDialog.prototype.initialize = function () { + // Parent method + OO.ui.ProcessDialog.parent.prototype.initialize.call( this ); + + // Properties + this.$navigation = $( '
' ); + this.$location = $( '
' ); + this.$safeActions = $( '
' ); + this.$primaryActions = $( '
' ); + this.$otherActions = $( '
' ); + this.dismissButton = new OO.ui.ButtonWidget( { + label: OO.ui.msg( 'ooui-dialog-process-dismiss' ) + } ); + this.retryButton = new OO.ui.ButtonWidget(); + this.$errors = $( '
' ); + this.$errorsTitle = $( '
' ); + + // Events + this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } ); + this.retryButton.connect( this, { click: 'onRetryButtonClick' } ); + + // Initialization + this.title.$element.addClass( 'oo-ui-processDialog-title' ); + this.$location + .append( this.title.$element ) + .addClass( 'oo-ui-processDialog-location' ); + this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' ); + this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' ); + this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' ); + this.$errorsTitle + .addClass( 'oo-ui-processDialog-errors-title' ) + .text( OO.ui.msg( 'ooui-dialog-process-error' ) ); + this.$errors + .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' ) + .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element ); + this.$content + .addClass( 'oo-ui-processDialog-content' ) + .append( this.$errors ); + this.$navigation + .addClass( 'oo-ui-processDialog-navigation' ) + // Note: Order of appends below is important. These are in the order + // we want tab to go through them. Display-order is handled entirely + // by CSS absolute-positioning. As such, primary actions like "done" + // should go first. + .append( this.$primaryActions, this.$location, this.$safeActions ); + this.$head.append( this.$navigation ); + this.$foot.append( this.$otherActions ); +}; + +/** + * @inheritdoc + */ +OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) { + var i, len, config, + isMobile = OO.ui.isMobile(), + widgets = []; + + for ( i = 0, len = actions.length; i < len; i++ ) { + config = $.extend( { framed: !OO.ui.isMobile() }, actions[ i ] ); + if ( isMobile && + ( config.flags === 'back' || ( Array.isArray( config.flags ) && config.flags.indexOf( 'back' ) !== -1 ) ) + ) { + $.extend( config, { + icon: 'previous', + label: '' + } ); + } + widgets.push( + new OO.ui.ActionWidget( config ) + ); + } + return widgets; +}; + +/** + * @inheritdoc + */ +OO.ui.ProcessDialog.prototype.attachActions = function () { + var i, len, other, special, others; + + // Parent method + OO.ui.ProcessDialog.parent.prototype.attachActions.call( this ); + + special = this.actions.getSpecial(); + others = this.actions.getOthers(); + if ( special.primary ) { + this.$primaryActions.append( special.primary.$element ); + } + for ( i = 0, len = others.length; i < len; i++ ) { + other = others[ i ]; + this.$otherActions.append( other.$element ); + } + if ( special.safe ) { + this.$safeActions.append( special.safe.$element ); + } + + this.fitLabel(); + this.$body.css( 'bottom', this.$foot.outerHeight( true ) ); +}; + +/** + * @inheritdoc + */ +OO.ui.ProcessDialog.prototype.executeAction = function ( action ) { + var process = this; + return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action ) + .fail( function ( errors ) { + process.showErrors( errors || [] ); + } ); +}; + +/** + * @inheritdoc + */ +OO.ui.ProcessDialog.prototype.setDimensions = function () { + // Parent method + OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments ); + + this.fitLabel(); +}; + +/** + * Fit label between actions. + * + * @private + * @chainable + */ +OO.ui.ProcessDialog.prototype.fitLabel = function () { + var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth, + size = this.getSizeProperties(); + + if ( typeof size.width !== 'number' ) { + if ( this.isOpened() ) { + navigationWidth = this.$head.width() - 20; + } else if ( this.isOpening() ) { + if ( !this.fitOnOpen ) { + // Size is relative and the dialog isn't open yet, so wait. + // FIXME: This should ideally be handled by setup somehow. + this.manager.lifecycle.opened.done( this.fitLabel.bind( this ) ); + this.fitOnOpen = true; + } + return; + } else { + return; + } + } else { + navigationWidth = size.width - 20; + } + + safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0; + primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0; + biggerWidth = Math.max( safeWidth, primaryWidth ); + + labelWidth = this.title.$element.width(); + + if ( 2 * biggerWidth + labelWidth < navigationWidth ) { + // We have enough space to center the label + leftWidth = rightWidth = biggerWidth; + } else { + // Let's hope we at least have enough space not to overlap, because we can't wrap the label… + if ( this.getDir() === 'ltr' ) { + leftWidth = safeWidth; + rightWidth = primaryWidth; + } else { + leftWidth = primaryWidth; + rightWidth = safeWidth; + } + } + + this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } ); + + return this; +}; + +/** + * Handle errors that occurred during accept or reject processes. + * + * @private + * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled + */ +OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) { + var i, len, $item, actions, + items = [], + abilities = {}, + recoverable = true, + warning = false; + + if ( errors instanceof OO.ui.Error ) { + errors = [ errors ]; + } + + for ( i = 0, len = errors.length; i < len; i++ ) { + if ( !errors[ i ].isRecoverable() ) { + recoverable = false; + } + if ( errors[ i ].isWarning() ) { + warning = true; + } + $item = $( '
' ) + .addClass( 'oo-ui-processDialog-error' ) + .append( errors[ i ].getMessage() ); + items.push( $item[ 0 ] ); + } + this.$errorItems = $( items ); + if ( recoverable ) { + abilities[ this.currentAction ] = true; + // Copy the flags from the first matching action + actions = this.actions.get( { actions: this.currentAction } ); + if ( actions.length ) { + this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() ); + } + } else { + abilities[ this.currentAction ] = false; + this.actions.setAbilities( abilities ); + } + 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.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 ); +}; + +/** + * Hide errors. + * + * @private + */ +OO.ui.ProcessDialog.prototype.hideErrors = function () { + this.$errors.addClass( 'oo-ui-element-hidden' ); + if ( this.$errorItems ) { + this.$errorItems.remove(); + this.$errorItems = null; + } +}; + +/** + * @inheritdoc + */ +OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) { + // Parent method + return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data ) + .first( function () { + // Make sure to hide errors + this.hideErrors(); + this.fitOnOpen = false; + }, this ); +}; + +/** + * @class OO.ui + */ + +/** + * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and + * OO.ui.confirm. + * + * @private + * @return {OO.ui.WindowManager} + */ +OO.ui.getWindowManager = function () { + if ( !OO.ui.windowManager ) { + OO.ui.windowManager = new OO.ui.WindowManager(); + $( 'body' ).append( OO.ui.windowManager.$element ); + OO.ui.windowManager.addWindows( [ new OO.ui.MessageDialog() ] ); + } + return OO.ui.windowManager; +}; + +/** + * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the + * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog + * has only one action button, labelled "OK", clicking it will simply close the dialog. + * + * A window manager is created automatically when this function is called for the first time. + * + * @example + * OO.ui.alert( 'Something happened!' ).done( function () { + * console.log( 'User closed the dialog.' ); + * } ); + * + * OO.ui.alert( 'Something larger happened!', { size: 'large' } ); + * + * @param {jQuery|string} text Message text to display + * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess + * @return {jQuery.Promise} Promise resolved when the user closes the dialog + */ +OO.ui.alert = function ( text, options ) { + return OO.ui.getWindowManager().openWindow( 'message', $.extend( { + message: text, + actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ] + }, options ) ).closed.then( function () { + return undefined; + } ); +}; + +/** + * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open, + * the rest of the page will be dimmed out and the user won't be able to interact with it. The + * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it + * (labelled "Cancel"). + * + * A window manager is created automatically when this function is called for the first time. + * + * @example + * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) { + * if ( confirmed ) { + * console.log( 'User clicked "OK"!' ); + * } else { + * console.log( 'User clicked "Cancel" or closed the dialog.' ); + * } + * } ); + * + * @param {jQuery|string} text Message text to display + * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess + * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to + * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean + * `false`. + */ +OO.ui.confirm = function ( text, options ) { + return OO.ui.getWindowManager().openWindow( 'message', $.extend( { + message: text + }, options ) ).closed.then( function ( data ) { + return !!( data && data.action === 'accept' ); + } ); +}; + +/** + * Display a quick modal prompt dialog, using a OO.ui.MessageDialog. While the dialog is open, + * the rest of the page will be dimmed out and the user won't be able to interact with it. The + * dialog has a text input widget and two action buttons, one to confirm an operation (labelled "OK") + * and one to cancel it (labelled "Cancel"). + * + * A window manager is created automatically when this function is called for the first time. + * + * @example + * OO.ui.prompt( 'Choose a line to go to', { textInput: { placeholder: 'Line number' } } ).done( function ( result ) { + * if ( result !== null ) { + * console.log( 'User typed "' + result + '" then clicked "OK".' ); + * } else { + * console.log( 'User clicked "Cancel" or closed the dialog.' ); + * } + * } ); + * + * @param {jQuery|string} text Message text to display + * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess + * @param {Object} [options.textInput] Additional options for text input widget, see OO.ui.TextInputWidget + * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to + * confirm, the promise will resolve with the value of the text input widget; otherwise, it will + * resolve to `null`. + */ +OO.ui.prompt = function ( text, options ) { + var instance, + manager = OO.ui.getWindowManager(), + textInput = new OO.ui.TextInputWidget( ( options && options.textInput ) || {} ), + textField = new OO.ui.FieldLayout( textInput, { + align: 'top', + label: text + } ); + + instance = manager.openWindow( 'message', $.extend( { + message: textField.$element + }, options ) ); + + // TODO: This is a little hacky, and could be done by extending MessageDialog instead. + instance.opened.then( function () { + textInput.on( 'enter', function () { + manager.getCurrentWindow().close( { action: 'accept' } ); + } ); + textInput.focus(); + } ); + + return instance.closed.then( function ( data ) { + return data && data.action === 'accept' ? textInput.getValue() : null; + } ); +}; + +}( OO ) ); + +//# sourceMappingURL=oojs-ui-windows.js.map \ No newline at end of file