/*!
- * OOjs UI v0.21.2
+ * OOjs UI v0.22.2
* https://www.mediawiki.org/wiki/OOjs_UI
*
* Copyright 2011–2017 OOjs UI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
- * Date: 2017-04-26T01:05:10Z
+ * Date: 2017-06-28T19:51:59Z
*/
( function ( OO ) {
return this;
};
+/**
+ * A window instance represents the life cycle for one single opening of a window
+ * until its closing.
+ *
+ * While OO.ui.WindowManager will reuse OO.ui.Window objects, each time a window is
+ * opened, a new lifecycle starts.
+ *
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
+ *
+ * @class
+ *
+ * @constructor
+ */
+OO.ui.WindowInstance = function OOuiWindowInstance() {
+ var deferreds = {
+ opening: $.Deferred(),
+ opened: $.Deferred(),
+ closing: $.Deferred(),
+ closed: $.Deferred()
+ };
+
+ /**
+ * @private
+ * @property {Object}
+ */
+ this.deferreds = deferreds;
+
+ // Set these up as chained promises so that rejecting of
+ // an earlier stage automatically rejects the subsequent
+ // would-be stages as well.
+
+ /**
+ * @property {jQuery.Promise}
+ */
+ this.opening = deferreds.opening.promise();
+ /**
+ * @property {jQuery.Promise}
+ */
+ this.opened = this.opening.then( function () {
+ return deferreds.opened;
+ } );
+ /**
+ * @property {jQuery.Promise}
+ */
+ this.closing = this.opened.then( function () {
+ return deferreds.closing;
+ } );
+ /**
+ * @property {jQuery.Promise}
+ */
+ this.closed = this.closing.then( function () {
+ return deferreds.closed;
+ } );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.WindowInstance );
+
/**
* 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
* {@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
+ * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before the
+ * window’s {@link OO.ui.Window#method-setup setup} method is called which executes OO.ui.Window#getSetupProcess.
* - a `setup` progress notification is emitted from the `opening` promise
- * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
- * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
- * window and its result executed
+ * - the #getReadyDelay method is called the returned value is used to time a pause in execution before the
+ * window’s {@link OO.ui.Window#method-ready ready} method is called which executes OO.ui.Window#getReadyProcess.
* - a `ready` progress notification is emitted from the `opening` promise
* - the `opening` promise is resolved with an `opened` promise
*
this.factory = config.factory;
this.modal = config.modal === undefined || !!config.modal;
this.windows = {};
- this.opening = null;
- this.opened = null;
- this.closing = null;
+ // Deprecated placeholder promise given to compatOpening in openWindow()
+ // that is resolved in closeWindow().
+ this.compatOpened = null;
this.preparingToOpen = null;
this.preparingToClose = null;
this.currentWindow = null;
*
* @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 {jQuery.Promise} opened A promise resolved with a value when the window is opened successfully.
+ * This promise also emits `setup` and `ready` notifications. When this promise is resolved, the first
+ * argument of the value is an 'closed' promise, the second argument is the opening data.
* @param {Object} data Window opening data
*/
*
* @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 {jQuery.Promise} closed A promise resolved with a value when the window is closed successfully.
+ * This promise also emits `hold` and `teardown` notifications. When this promise is resolved, the first
+ * argument of its value is the closing data.
* @param {Object} data Window closing data
*/
* @return {boolean} Window is opening
*/
OO.ui.WindowManager.prototype.isOpening = function ( win ) {
- return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
+ return win === this.currentWindow && !!this.lifecycle &&
+ this.lifecycle.opened.state() === 'pending';
};
/**
* @return {boolean} Window is closing
*/
OO.ui.WindowManager.prototype.isClosing = function ( win ) {
- return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
+ return win === this.currentWindow && !!this.lifecycle &&
+ this.lifecycle.closing.state() === 'resolved' &&
+ this.lifecycle.closed.state() === 'pending';
};
/**
* @return {boolean} Window is opened
*/
OO.ui.WindowManager.prototype.isOpened = function ( win ) {
- return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
+ return win === this.currentWindow && !!this.lifecycle &&
+ this.lifecycle.opened.state() === 'resolved' &&
+ this.lifecycle.closing.state() === 'pending';
};
/**
* @param {Object} [data] Window opening data
* @param {jQuery|null} [data.$returnFocusTo] Element to which the window will return focus when closed.
* Defaults the current activeElement. If set to null, focus isn't changed on close.
- * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
- * See {@link #event-opening 'opening' event} for more information about `opening` promises.
+ * @return {OO.ui.WindowInstance|jQuery.Promise} A lifecycle object representing this particular
+ * opening of the window. For backwards-compatibility, then object is also a Thenable that is resolved
+ * when the window is done opening, with nested promise for when closing starts. This behaviour
+ * is deprecated and is not compatible with jQuery 3 (T163510).
* @fires opening
*/
-OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
- var manager = this,
- opening = $.Deferred();
+OO.ui.WindowManager.prototype.openWindow = function ( win, data, lifecycle, compatOpening ) {
+ var manager = this;
data = data || {};
+ // Internal parameter 'lifecycle' allows this method to always return
+ // a lifecycle even if the window still needs to be created
+ // asynchronously when 'win' is a string.
+ lifecycle = lifecycle || new OO.ui.WindowInstance();
+ compatOpening = compatOpening || $.Deferred();
+
+ // Turn lifecycle into a Thenable for backwards-compatibility with
+ // the deprecated nested-promise behaviour (T163510).
+ [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
+ .forEach( function ( method ) {
+ lifecycle[ method ] = function () {
+ OO.ui.warnDeprecation(
+ 'Using the return value of openWindow as a promise is deprecated. ' +
+ 'Use .openWindow( ... ).opening.' + method + '( ... ) instead.'
+ );
+ return compatOpening[ method ].apply( this, arguments );
+ };
+ } );
+
// Argument handling
if ( typeof win === 'string' ) {
- return this.getWindow( win ).then( function ( win ) {
- return manager.openWindow( win, data );
- } );
+ this.getWindow( win ).then(
+ function ( win ) {
+ manager.openWindow( win, data, lifecycle, compatOpening );
+ },
+ function ( err ) {
+ lifecycle.deferreds.opening.reject( err );
+ }
+ );
+ return lifecycle;
}
// Error handling
if ( !this.hasWindow( win ) ) {
- opening.reject( new OO.ui.Error(
+ compatOpening.reject( new OO.ui.Error(
+ 'Cannot open window: window is not attached to manager'
+ ) );
+ lifecycle.deferreds.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(
+ return lifecycle;
+ }
+ if ( this.preparingToOpen || ( this.lifecycle && this.lifecycle.closing.state() !== 'resolved' ) ) {
+ compatOpening.reject( new OO.ui.Error(
+ 'Cannot open window: another window is opening or open'
+ ) );
+ lifecycle.deferreds.opening.reject( new OO.ui.Error(
'Cannot open window: another window is opening or open'
) );
+ return lifecycle;
}
- // 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.$returnFocusTo = data.$returnFocusTo !== undefined ? data.$returnFocusTo : $( document.activeElement );
- 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() );
- } );
- }
+ // If a window is currently closing, wait for it to complete
+ this.preparingToOpen = $.when( this.lifecycle && this.lifecycle.closed );
+ // Ensure handlers get called after preparingToOpen is set
+ this.preparingToOpen.done( function () {
+ if ( manager.modal ) {
+ manager.toggleGlobalEvents( true );
+ manager.toggleAriaIsolation( true );
+ }
+ manager.$returnFocusTo = data.$returnFocusTo !== undefined ? data.$returnFocusTo : $( document.activeElement );
+ manager.currentWindow = win;
+ manager.lifecycle = lifecycle;
+ manager.preparingToOpen = null;
+ manager.emit( 'opening', win, compatOpening, data );
+ lifecycle.deferreds.opening.resolve( data );
+ setTimeout( function () {
+ manager.compatOpened = $.Deferred();
+ win.setup( data ).then( function () {
+ manager.updateWindowSize( win );
+ compatOpening.notify( { state: 'setup' } );
+ setTimeout( function () {
+ win.ready( data ).then( function () {
+ compatOpening.notify( { state: 'ready' } );
+ lifecycle.deferreds.opened.resolve( data );
+ compatOpening.resolve( manager.compatOpened.promise(), data );
+ }, function () {
+ lifecycle.deferreds.opened.reject();
+ compatOpening.reject();
+ manager.closeWindow( win );
+ } );
+ }, manager.getReadyDelay() );
+ }, function () {
+ lifecycle.deferreds.opened.reject();
+ compatOpening.reject();
+ manager.closeWindow( win );
+ } );
+ }, manager.getSetupDelay() );
+ } );
- return opening.promise();
+ return lifecycle;
};
/**
*
* @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.
+ * @return {OO.ui.WindowInstance|jQuery.Promise} A lifecycle object representing this particular
+ * opening of the window. For backwards-compatibility, the object is also a Thenable that is resolved
+ * when the window is done closing (T163510).
* @fires closing
*/
OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
- var manager = this,
- closing = $.Deferred(),
- opened;
+ var error,
+ manager = this,
+ compatClosing = $.Deferred(),
+ lifecycle = this.lifecycle,
+ compatOpened;
// Argument handling
if ( typeof win === 'string' ) {
}
// Error handling
- if ( !win ) {
- closing.reject( new OO.ui.Error(
- 'Cannot close window: window is not attached to manager'
- ) );
+ if ( !lifecycle ) {
+ error = 'Cannot close window: no window is currently open';
+ } else if ( !win ) {
+ error = 'Cannot close window: window is not attached to manager';
} else if ( win !== this.currentWindow ) {
- 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 );
- }
- if ( manager.$returnFocusTo && manager.$returnFocusTo.length ) {
- manager.$returnFocusTo[ 0 ].focus();
- }
- manager.closing = null;
- manager.currentWindow = null;
- closing.resolve( data );
- } );
- }, manager.getTeardownDelay() );
- } );
- }, manager.getHoldDelay() );
+ error = 'Cannot close window: window already closed with different data';
+ } else if ( this.preparingToClose || lifecycle.closing.state() === 'resolved' ) {
+ error = 'Cannot close window: window already closing with different data';
+ }
+
+ if ( error ) {
+ // This function was called for the wrong window and we don't want to mess with the current
+ // window's state.
+ lifecycle = new OO.ui.WindowInstance();
+ // Pretend the window has been opened, so that we can pretend to fail to close it.
+ lifecycle.deferreds.opening.resolve( {} );
+ lifecycle.deferreds.opened.resolve( {} );
+ }
+
+ // Turn lifecycle into a Thenable for backwards-compatibility with
+ // the deprecated nested-promise behaviour (T163510).
+ [ 'state', 'always', 'catch', 'pipe', 'then', 'promise', 'progress', 'done', 'fail' ]
+ .forEach( function ( method ) {
+ lifecycle[ method ] = function () {
+ OO.ui.warnDeprecation(
+ 'Using the return value of closeWindow as a promise is deprecated. ' +
+ 'Use .closeWindow( ... ).closed.' + method + '( ... ) instead.'
+ );
+ return compatClosing[ method ].apply( this, arguments );
+ };
} );
- }
- return closing.promise();
+ if ( error ) {
+ compatClosing.reject( new OO.ui.Error( error ) );
+ lifecycle.deferreds.closing.reject( new OO.ui.Error( error ) );
+ return lifecycle;
+ }
+
+ // If the window is currently opening, close it when it's done
+ this.preparingToClose = $.when( this.lifecycle.opened );
+ // Ensure handlers get called after preparingToClose is set
+ this.preparingToClose.always( function () {
+ manager.preparingToClose = null;
+ manager.emit( 'closing', win, compatClosing, data );
+ lifecycle.deferreds.closing.resolve( data );
+ compatOpened = manager.compatOpened;
+ manager.compatOpened = null;
+ compatOpened.resolve( compatClosing.promise(), data );
+ setTimeout( function () {
+ win.hold( data ).then( function () {
+ compatClosing.notify( { state: 'hold' } );
+ setTimeout( function () {
+ win.teardown( data ).then( function () {
+ compatClosing.notify( { state: 'teardown' } );
+ if ( manager.modal ) {
+ manager.toggleGlobalEvents( false );
+ manager.toggleAriaIsolation( false );
+ }
+ if ( manager.$returnFocusTo && manager.$returnFocusTo.length ) {
+ manager.$returnFocusTo[ 0 ].focus();
+ }
+ manager.currentWindow = null;
+ manager.lifecycle = null;
+ lifecycle.deferreds.closed.resolve( data );
+ compatClosing.resolve( data );
+ } );
+ }, manager.getTeardownDelay() );
+ } );
+ }, manager.getHoldDelay() );
+ } );
+
+ return lifecycle;
};
/**
throw new Error( 'Cannot remove window' );
}
cleanupWindow = cleanup.bind( null, name, win );
- promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
+ promises.push( this.closeWindow( name ).closed.then( cleanupWindow, cleanupWindow ) );
}
return $.when.apply( $, promises );
return action.getFlags().indexOf( 'primary' ) > -1;
} );
if ( actions.length > 0 ) {
- actions[ 0 ].$button.focus();
+ actions[ 0 ].focus();
}
}, this );
};
} 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 ) );
+ // FIXME: This should ideally be handled by setup somehow.
+ this.manager.lifecycle.opened.done( this.fitLabel.bind( this ) );
this.fitOnOpen = true;
}
return;
return OO.ui.getWindowManager().openWindow( 'message', $.extend( {
message: text,
actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
- }, options ) ).then( function ( opened ) {
- return opened.then( function ( closing ) {
- return closing.then( function () {
- return $.Deferred().resolve();
- } );
- } );
+ }, options ) ).closed.then( function () {
+ return undefined;
} );
};
OO.ui.confirm = function ( text, options ) {
return OO.ui.getWindowManager().openWindow( 'message', $.extend( {
message: text
- }, options ) ).then( function ( opened ) {
- return opened.then( function ( closing ) {
- return closing.then( function ( data ) {
- return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
- } );
- } );
+ }, options ) ).closed.then( function ( data ) {
+ return !!( data && data.action === 'accept' );
} );
};
* resolve to `null`.
*/
OO.ui.prompt = function ( text, options ) {
- var manager = OO.ui.getWindowManager(),
+ var instance,
+ manager = OO.ui.getWindowManager(),
textInput = new OO.ui.TextInputWidget( ( options && options.textInput ) || {} ),
textField = new OO.ui.FieldLayout( textInput, {
align: 'top',
label: text
} );
- // TODO: This is a little hacky, and could be done by extending MessageDialog instead.
-
- return manager.openWindow( 'message', $.extend( {
+ instance = manager.openWindow( 'message', $.extend( {
message: textField.$element
- }, options ) ).then( function ( opened ) {
- // After ready
+ }, options ) );
+
+ // TODO: This is a little hacky, and could be done by extending MessageDialog instead.
+ instance.opened.then( function () {
textInput.on( 'enter', function () {
manager.getCurrentWindow().close( { action: 'accept' } );
} );
textInput.focus();
- return opened.then( function ( closing ) {
- return closing.then( function ( data ) {
- return $.Deferred().resolve( data && data.action === 'accept' ? textInput.getValue() : null );
- } );
- } );
+ } );
+
+ return instance.closed.then( function ( data ) {
+ return data && data.action === 'accept' ? textInput.getValue() : null;
} );
};
}( OO ) );
+
+//# sourceMappingURL=oojs-ui-windows.js.map
\ No newline at end of file