jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/350789 )

Change subject: WindowManager: Add WindowInstance - a Promise-based lifecycle 
object
......................................................................


WindowManager: Add WindowInstance - a Promise-based lifecycle object

This is in preparation for deprecating the nested opening-opened-closing Promise
returned by openWindow().

* Add WindowInstance class.

* Remove private 'opened' and 'opening' properties from WindowManager.
  (No longer used.)
  Update the one use of it outside WindowManger, in ProcessDialog.

* Improve documentation for the now-deprecated nested-Promise behaviour.

* Add compat code for calling .then() on openWindow().
  This actually makes the currently broken OO.ui.alert() code work again
  due to not being subject to the jQuery 3 unwrapping of nested promises.

Bug: T95923
Bug: T166153
Bug: T163510
Change-Id: I9dbc9852434caba15b797266289af532721f1f32
---
M build/modules.yaml
M jsduck.categories.json
M package.json
A src/WindowInstance.js
M src/WindowManager.js
M src/dialogs/ProcessDialog.js
6 files changed, 223 insertions(+), 118 deletions(-)

Approvals:
  Bartosz Dziewoński: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/build/modules.yaml b/build/modules.yaml
index baeb1ef..d1e38d7 100644
--- a/build/modules.yaml
+++ b/build/modules.yaml
@@ -150,6 +150,7 @@
                        "src/ActionSet.js",
                        "src/Error.js",
                        "src/Process.js",
+                       "src/WindowInstance.js",
                        "src/WindowManager.js",
                        "src/Window.js",
                        "src/Dialog.js",
diff --git a/jsduck.categories.json b/jsduck.categories.json
index 7ede227..07fbd39 100644
--- a/jsduck.categories.json
+++ b/jsduck.categories.json
@@ -11,6 +11,7 @@
                                        "OO.ui.Toolbar",
                                        "OO.ui.Window",
                                        "OO.ui.Dialog",
+                                       "OO.ui.WindowInstance",
                                        "OO.ui.WindowManager",
                                        "OO.ui.Process",
                                        "OO.ui.Error",
diff --git a/package.json b/package.json
index 2b7448d..720c282 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
     "predoc": "grunt build",
     "doc": "jsduck",
     "postdoc": "grunt copy:jsduck",
+    "quickdoc": "grunt quick-build && jsduck && grunt copy:jsduck",
     "publish-build": "grunt publish-build",
     "demos": "grunt publish-build && grunt demos",
     "jenkins": "npm test && jsduck && npm run postdoc"
diff --git a/src/WindowInstance.js b/src/WindowInstance.js
new file mode 100644
index 0000000..a423f21
--- /dev/null
+++ b/src/WindowInstance.js
@@ -0,0 +1,60 @@
+/**
+ * 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 state = {
+               opening: $.Deferred(),
+               opened: $.Deferred(),
+               closing: $.Deferred(),
+               closed: $.Deferred()
+       };
+
+       /**
+        * @private
+        * @property {Object}
+        */
+       this.state = state;
+
+       // 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 = state.opening.promise();
+       /**
+        * @property {jQuery.Promise}
+        */
+       this.opened = this.opening.then( function () {
+               return state.opened;
+       } );
+       /**
+        * @property {jQuery.Promise}
+        */
+       this.closing = this.opened.then( function () {
+               return state.closing;
+       } );
+       /**
+        * @property {jQuery.Promise}
+        */
+       this.closed = this.closing.then( function () {
+               return state.closed;
+       } );
+};
+
+/* Setup */
+
+OO.initClass( OO.ui.WindowInstance );
diff --git a/src/WindowManager.js b/src/WindowManager.js
index 34abfc0..1cc73ef 100644
--- a/src/WindowManager.js
+++ b/src/WindowManager.js
@@ -12,13 +12,11 @@
  * {@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
  *
@@ -68,9 +66,9 @@
        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;
@@ -99,9 +97,9 @@
  *
  * @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
  */
 
@@ -110,10 +108,9 @@
  *
  * @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
  */
 
@@ -196,7 +193,8 @@
  * @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';
 };
 
 /**
@@ -206,7 +204,9 @@
  * @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() === 'ready' &&
+               this.lifecycle.closed.state() === 'pending';
 };
 
 /**
@@ -216,7 +216,9 @@
  * @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() === 'ready' &&
+               this.lifecycle.closing.state() === 'pending';
 };
 
 /**
@@ -338,76 +340,98 @@
  * @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).
+       lifecycle.then = compatOpening.then;
 
        // 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.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'
                ) );
-       } else if ( this.preparingToOpen || this.opening || this.opened ) {
-               opening.reject( new OO.ui.Error(
+               lifecycle.state.opening.reject( new OO.ui.Error(
+                       'Cannot open window: window is not attached to manager'
+               ) );
+               return lifecycle;
+       }
+       if ( this.preparingToOpen || ( this.lifecycle && 
this.lifecycle.closing.state() !== 'ready' ) ) {
+               compatOpening.reject( new OO.ui.Error(
                        'Cannot open window: another window is opening or open'
                ) );
+               lifecycle.state.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.state.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.state.opened.resolve( 
data );
+                                               compatOpening.resolve( 
manager.compatOpened.promise(), data );
+                                       }, function () {
+                                               lifecycle.state.opened.reject();
+                                               compatOpening.reject();
+                                               manager.closeWindow( win );
+                                       } );
+                               }, manager.getReadyDelay() );
+                       }, function () {
+                               lifecycle.state.opened.reject();
+                               compatOpening.reject();
+                               manager.closeWindow( win );
+                       } );
+               }, manager.getSetupDelay() );
+       } );
 
-       return opening.promise();
+       return lifecycle;
 };
 
 /**
@@ -415,15 +439,16 @@
  *
  * @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;
+               compatClosing = $.Deferred(),
+               lifecycle = this.lifecycle,
+               compatOpened;
 
        // Argument handling
        if ( typeof win === 'string' ) {
@@ -432,57 +457,73 @@
                win = null;
        }
 
+       // Turn lifecycle into a Thenable for backwards-compatibility with
+       // the deprecated nested-promise behaviour (T163510).
+       lifecycle.then = compatClosing.then;
+
        // Error handling
        if ( !win ) {
-               closing.reject( new OO.ui.Error(
+               compatClosing.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(
+               lifecycle.state.closing.reject( new OO.ui.Error(
+                       'Cannot close window: window is not attached to manager'
+               ) );
+               return lifecycle;
+       }
+       if ( win !== this.currentWindow ) {
+               compatClosing.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(
+               lifecycle.state.closing.reject( new OO.ui.Error(
+                       'Cannot close window: window already closed with 
different data'
+               ) );
+               return lifecycle;
+       }
+       if ( this.preparingToClose || this.closing ) {
+               compatClosing.reject( new OO.ui.Error(
                        'Cannot close window: window already closing with 
different data'
                ) );
+               lifecycle.state.closing.reject( new OO.ui.Error(
+                       'Cannot close window: window already closing with 
different data'
+               ) );
+               return lifecycle;
        }
 
-       // 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() );
-               } );
-       }
+       // 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.state.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.state.closed.resolve( 
data );
+                                               compatClosing.resolve( data );
+                                       } );
+                               }, manager.getTeardownDelay() );
+                       } );
+               }, manager.getHoldDelay() );
+       } );
 
-       return closing.promise();
+       return lifecycle;
 };
 
 /**
diff --git a/src/dialogs/ProcessDialog.js b/src/dialogs/ProcessDialog.js
index 65ddd75..e5b853e 100644
--- a/src/dialogs/ProcessDialog.js
+++ b/src/dialogs/ProcessDialog.js
@@ -239,7 +239,8 @@
                } 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;

-- 
To view, visit https://gerrit.wikimedia.org/r/350789
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I9dbc9852434caba15b797266289af532721f1f32
Gerrit-PatchSet: 22
Gerrit-Project: oojs/ui
Gerrit-Branch: master
Gerrit-Owner: Krinkle <[email protected]>
Gerrit-Reviewer: Anomie <[email protected]>
Gerrit-Reviewer: Bartosz Dziewoński <[email protected]>
Gerrit-Reviewer: Divec <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Trevor Parscal <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to