%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/lib/oojs-ui/ |
| Current File : //www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/lib/oojs-ui/oojs-ui-windows.js |
/*!
* OOjs UI v0.17.9
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2016 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
* Date: 2016-09-13T18:30:02Z
*/
( 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 );
/* Events */
/**
* A resize event is emitted when the size of the widget changes.
*
* @event resize
*/
/* 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();
};
/**
* Emit a resize event if the size has changed.
*
* @private
* @chainable
*/
OO.ui.ActionWidget.prototype.propagateResize = function () {
var width, height;
if ( this.isElementAttached() ) {
width = this.$element.width();
height = this.$element.height();
if ( width !== this.width || height !== this.height ) {
this.width = width;
this.height = height;
this.emit( 'resize' );
}
}
return this;
};
/**
* @inheritdoc
*/
OO.ui.ActionWidget.prototype.setIcon = function () {
// Mixin method
OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
this.propagateResize();
return this;
};
/**
* @inheritdoc
*/
OO.ui.ActionWidget.prototype.setLabel = function () {
// Mixin method
OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
this.propagateResize();
return this;
};
/**
* @inheritdoc
*/
OO.ui.ActionWidget.prototype.setFlags = function () {
// Mixin method
OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
this.propagateResize();
return this;
};
/**
* @inheritdoc
*/
OO.ui.ActionWidget.prototype.clearFlags = function () {
// Mixin method
OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
this.propagateResize();
return this;
};
/**
* Toggle the visibility of the action button.
*
* @param {boolean} [show] Show button, omit to toggle visibility
* @chainable
*/
OO.ui.ActionWidget.prototype.toggle = function () {
// Parent method
OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
this.propagateResize();
return this;
};
/* 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';
* // 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( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode.</p>' );
* this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
* this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode.</p>' );
* 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 resize
*
* A 'resize' event is emitted when an action widget is resized.
*
* @param {OO.ui.ActionWidget} action Action that was resized
*/
/**
* @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.<string,boolean>} 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 ],
resize: [ 'emit', 'resize', 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() :
$( '<div>' ).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.
* @return {Object} Step object, with `callback` and `context` properties
*/
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;
};
/**
* 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#getSetupProcess getSetupProcess} method is called on the
* window and its result executed
* - 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#getReadyProcess getReadyProcess} method is called on the
* window and its result executed
* - 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 = {};
this.opening = null;
this.opened = null;
this.closing = null;
this.preparingToOpen = null;
this.preparingToClose = null;
this.currentWindow = null;
this.globalEvents = false;
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} opening An `opening` promise resolved with a value when the window is opened successfully.
* When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
* is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
* @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} closing A `closing` promise is resolved with a value when the window
* is closed successfully. The promise emits `hold` and `teardown` notifications when those
* processes are complete. When the `closing` 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.
*
* @return {boolean} Window is opening
*/
OO.ui.WindowManager.prototype.isOpening = function ( win ) {
return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
};
/**
* Check if window is closing.
*
* @return {boolean} Window is closing
*/
OO.ui.WindowManager.prototype.isClosing = function ( win ) {
return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
};
/**
* Check if window is opened.
*
* @return {boolean} Window is opened
*/
OO.ui.WindowManager.prototype.isOpened = function ( win ) {
return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
};
/**
* 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
* @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
* See {@link #event-opening 'opening' event} for more information about `opening` promises.
* @fires opening
*/
OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
var manager = this,
opening = $.Deferred();
// Argument handling
if ( typeof win === 'string' ) {
return this.getWindow( win ).then( function ( win ) {
return manager.openWindow( win, data );
} );
}
// Error handling
if ( !this.hasWindow( win ) ) {
opening.reject( new OO.ui.Error(
'Cannot open window: window is not attached to manager'
) );
} else if ( this.preparingToOpen || this.opening || this.opened ) {
opening.reject( new OO.ui.Error(
'Cannot open window: another window is opening or open'
) );
}
// Window opening
if ( opening.state() !== 'rejected' ) {
// If a window is currently closing, wait for it to complete
this.preparingToOpen = $.when( this.closing );
// Ensure handlers get called after preparingToOpen is set
this.preparingToOpen.done( function () {
if ( manager.modal ) {
manager.toggleGlobalEvents( true );
manager.toggleAriaIsolation( true );
}
manager.currentWindow = win;
manager.opening = opening;
manager.preparingToOpen = null;
manager.emit( 'opening', win, opening, data );
setTimeout( function () {
win.setup( data ).then( function () {
manager.updateWindowSize( win );
manager.opening.notify( { state: 'setup' } );
setTimeout( function () {
win.ready( data ).then( function () {
manager.opening.notify( { state: 'ready' } );
manager.opening = null;
manager.opened = $.Deferred();
opening.resolve( manager.opened.promise(), data );
}, function () {
manager.opening = null;
manager.opened = $.Deferred();
opening.reject();
manager.closeWindow( win );
} );
}, manager.getReadyDelay() );
}, function () {
manager.opening = null;
manager.opened = $.Deferred();
opening.reject();
manager.closeWindow( win );
} );
}, manager.getSetupDelay() );
} );
}
return opening.promise();
};
/**
* 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 {jQuery.Promise} A `closing` promise resolved when the window is done closing.
* See {@link #event-closing 'closing' event} for more information about closing promises.
* @throws {Error} An error is thrown if the window is not managed by the window manager.
* @fires closing
*/
OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
var manager = this,
closing = $.Deferred(),
opened;
// Argument handling
if ( typeof win === 'string' ) {
win = this.windows[ win ];
} else if ( !this.hasWindow( win ) ) {
win = null;
}
// Error handling
if ( !win ) {
closing.reject( new OO.ui.Error(
'Cannot close window: window is not attached to manager'
) );
} else if ( win !== this.currentWindow ) {
closing.reject( new OO.ui.Error(
'Cannot close window: window already closed with different data'
) );
} else if ( this.preparingToClose || this.closing ) {
closing.reject( new OO.ui.Error(
'Cannot close window: window already closing with different data'
) );
}
// Window closing
if ( closing.state() !== 'rejected' ) {
// If the window is currently opening, close it when it's done
this.preparingToClose = $.when( this.opening );
// Ensure handlers get called after preparingToClose is set
this.preparingToClose.always( function () {
manager.closing = closing;
manager.preparingToClose = null;
manager.emit( 'closing', win, closing, data );
opened = manager.opened;
manager.opened = null;
opened.resolve( closing.promise(), data );
setTimeout( function () {
win.hold( data ).then( function () {
closing.notify( { state: 'hold' } );
setTimeout( function () {
win.teardown( data ).then( function () {
closing.notify( { state: 'teardown' } );
if ( manager.modal ) {
manager.toggleGlobalEvents( false );
manager.toggleAriaIsolation( false );
}
manager.closing = null;
manager.currentWindow = null;
closing.resolve( data );
} );
}, manager.getTeardownDelay() );
} );
}, manager.getHoldDelay() );
} );
}
return closing.promise();
};
/**
* 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
*
* @param {Object.<string,OO.ui.Window>|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 ( typeof name !== 'string' ) {
throw new Error( 'Cannot add window' );
}
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 ).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.
*
* @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 = $( '<div>' );
this.$overlay = $( '<div>' );
this.$content = $( '<div>' );
this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
this.$focusTrapAfter = $( '<div>' ).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.
var oldTransition,
styleObj = this.$frame[ 0 ].style;
oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
styleObj.MozTransition || styleObj.WebkitTransition;
styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
styleObj.MozTransition = styleObj.WebkitTransition = 'none';
callback();
// Force reflow to make sure the style changes done inside callback really are not transitioned
this.$frame.height();
styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
styleObj.MozTransition = styleObj.WebkitTransition = 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 proccess 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 = $( '<div>' );
this.$body = $( '<div>' );
this.$foot = $( '<div>' );
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.prototype.initialize = function () {
* MyDialog.parent.prototype.initialize.call( this );
* this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
* this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
* 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',
resize: 'onActionResize',
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 ) {
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 resized events.
*
* @private
* @param {OO.ui.ActionWidget} action Action that was resized
*/
OO.ui.Dialog.prototype.onActionResize = function () {
// Override in subclass
};
/**
* 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 () {
var titleId;
// Parent method
OO.ui.Dialog.parent.prototype.initialize.call( this );
titleId = OO.ui.generateElementId();
// Properties
this.title = new OO.ui.LabelWidget( {
id: titleId
} );
// Initialization
this.$content.addClass( 'oo-ui-dialog-content' );
this.$element.attr( 'aria-labelledby', titleId );
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 */
OO.ui.MessageDialog.static.name = 'message';
OO.ui.MessageDialog.static.size = 'small';
OO.ui.MessageDialog.static.verbose = false;
/**
* 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;
// Note that OO.ui.alert() and OO.ui.confirm() rely on these.
OO.ui.MessageDialog.static.actions = [
{ 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;
};
/**
* @inheritdoc
*/
OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
this.fitActions();
return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
};
/**
* 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 {boolean} [data.verbose] Message is verbose and should be styled as a long message
* @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.message.$element.toggleClass(
'oo-ui-messageDialog-message-verbose',
data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
);
}, 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 ].$button.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;
$scrollable[ 0 ].style.overflow = 'hidden';
OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
$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 = $( '<div>' );
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.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( '<p>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.</p>' );
* 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.onActionResize = function ( action ) {
if ( this.actions.isSpecial( action ) ) {
this.fitLabel();
}
return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
};
/**
* @inheritdoc
*/
OO.ui.ProcessDialog.prototype.initialize = function () {
// Parent method
OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
// Properties
this.$navigation = $( '<div>' );
this.$location = $( '<div>' );
this.$safeActions = $( '<div>' );
this.$primaryActions = $( '<div>' );
this.$otherActions = $( '<div>' );
this.dismissButton = new OO.ui.ButtonWidget( {
label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
} );
this.retryButton = new OO.ui.ButtonWidget();
this.$errors = $( '<div>' );
this.$errorsTitle = $( '<div>' );
// 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, widgets = [];
for ( i = 0, len = actions.length; i < len; i++ ) {
widgets.push(
new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
);
}
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.
this.manager.opening.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 = $( '<div>' )
.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( {
messageDialog: 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.' );
* } );
*
* @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( 'messageDialog', $.extend( {
message: text,
verbose: true,
actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
}, options ) ).then( function ( opened ) {
return opened.then( function ( closing ) {
return closing.then( function () {
return $.Deferred().resolve();
} );
} );
} );
};
/**
* 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( 'messageDialog', $.extend( {
message: text,
verbose: true
}, options ) ).then( function ( opened ) {
return opened.then( function ( closing ) {
return closing.then( function ( data ) {
return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
} );
} );
} );
};
}( OO ) );