Merge "filecache: Use current action instead of "view" only in outage mode"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui-windows.js
index 5a06841..62a0633 100644 (file)
@@ -1,12 +1,12 @@
 /*!
- * 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 ) {
 
@@ -851,6 +851,67 @@ OO.ui.Process.prototype.next = function ( step, context ) {
        return this;
 };
 
+/**
+ * A window instance represents the life cycle for one single opening of a window
+ * until its closing.
+ *
+ * While OO.ui.WindowManager will reuse OO.ui.Window objects, each time a window is
+ * opened, a new lifecycle starts.
+ *
+ * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
+ *
+ * @class
+ *
+ * @constructor
+ */
+OO.ui.WindowInstance = function OOuiWindowInstance() {
+       var deferreds = {
+               opening: $.Deferred(),
+               opened: $.Deferred(),
+               closing: $.Deferred(),
+               closed: $.Deferred()
+       };
+
+       /**
+        * @private
+        * @property {Object}
+        */
+       this.deferreds = deferreds;
+
+       // Set these up as chained promises so that rejecting of
+       // an earlier stage automatically rejects the subsequent
+       // would-be stages as well.
+
+       /**
+        * @property {jQuery.Promise}
+        */
+       this.opening = deferreds.opening.promise();
+       /**
+        * @property {jQuery.Promise}
+        */
+       this.opened = this.opening.then( function () {
+               return deferreds.opened;
+       } );
+       /**
+        * @property {jQuery.Promise}
+        */
+       this.closing = this.opened.then( function () {
+               return deferreds.closing;
+       } );
+       /**
+        * @property {jQuery.Promise}
+        */
+       this.closed = this.closing.then( function () {
+               return deferreds.closed;
+       } );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.WindowInstance );
+
 /**
  * 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
@@ -865,13 +926,11 @@ OO.ui.Process.prototype.next = function ( step, context ) {
  * {@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
  *
@@ -921,9 +980,9 @@ OO.ui.WindowManager = function OoUiWindowManager( config ) {
        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;
@@ -952,9 +1011,9 @@ OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
  *
  * @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
  */
 
@@ -963,10 +1022,9 @@ OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
  *
  * @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
  */
 
@@ -1049,7 +1107,8 @@ OO.ui.WindowManager.prototype.afterWindowResize = function () {
  * @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';
 };
 
 /**
@@ -1059,7 +1118,9 @@ OO.ui.WindowManager.prototype.isOpening = function ( win ) {
  * @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';
 };
 
 /**
@@ -1069,7 +1130,9 @@ OO.ui.WindowManager.prototype.isClosing = function ( win ) {
  * @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';
 };
 
 /**
@@ -1191,76 +1254,107 @@ OO.ui.WindowManager.prototype.getCurrentWindow = function () {
  * @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;
 };
 
 /**
@@ -1268,15 +1362,17 @@ OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
  *
  * @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' ) {
@@ -1286,56 +1382,78 @@ OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
        }
 
        // 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;
 };
 
 /**
@@ -1438,7 +1556,7 @@ OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
                        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 );
@@ -2786,7 +2904,7 @@ OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
                                return action.getFlags().indexOf( 'primary' ) > -1;
                        } );
                        if ( actions.length > 0 ) {
-                               actions[ 0 ].$button.focus();
+                               actions[ 0 ].focus();
                        }
                }, this );
 };
@@ -3174,7 +3292,8 @@ OO.ui.ProcessDialog.prototype.fitLabel = function () {
                } 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;
@@ -3329,12 +3448,8 @@ OO.ui.alert = function ( text, options ) {
        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;
        } );
 };
 
@@ -3364,12 +3479,8 @@ OO.ui.alert = function ( text, options ) {
 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' );
        } );
 };
 
@@ -3398,29 +3509,31 @@ OO.ui.confirm = function ( text, options ) {
  *  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