Trevor Parscal has uploaded a new change for review. https://gerrit.wikimedia.org/r/133193
Change subject: [WIP] Window refactor, introduction of Processes ...................................................................... [WIP] Window refactor, introduction of Processes Processes collect and execute lists of functions. If a function returns boolean false, the process is halted. If the function returns a promise, the process waits for the promise. If the promise is rejected, the process is halted. If the promise is resolved the promise continues. Changes * Introduce OO.ui.Process * Convert window and dialog to use processes * Convert frame to return promises instead of taking callbacks * Using super everywhere Change-Id: I5f97185296e2c9a2d5fcdd9f2f7975e24995e49a --- M build/modules.json M src/Dialog.js M src/Frame.js A src/Process.js M src/Window.js M src/layouts/StackLayout.js M src/toolgroups/PopupToolGroup.js M src/widgets/ButtonOptionWidget.js M src/widgets/GroupWidget.js M src/widgets/InputWidget.js M src/widgets/ItemWidget.js M src/widgets/MenuWidget.js M src/widgets/PopupButtonWidget.js M src/widgets/SelectWidget.js M src/widgets/TextInputMenuWidget.js M src/widgets/TextInputWidget.js M src/widgets/ToggleButtonWidget.js A test/OO.ui.Process.test.js M test/index.html 19 files changed, 466 insertions(+), 169 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/oojs/ui refs/changes/93/133193/1 diff --git a/build/modules.json b/build/modules.json index 96739a8..2b083b5 100644 --- a/build/modules.json +++ b/build/modules.json @@ -10,6 +10,7 @@ "src/Dialog.js", "src/Layout.js", "src/Widget.js", + "src/Process.js", "src/elements/ButtonedElement.js", "src/elements/ClippableElement.js", "src/elements/FlaggableElement.js", diff --git a/src/Dialog.js b/src/Dialog.js index 3f4e116..2c19466 100644 --- a/src/Dialog.js +++ b/src/Dialog.js @@ -27,7 +27,7 @@ // Events this.$element.on( 'mousedown', false ); - this.connect( this, { 'opening': 'onOpening' } ); + this.connect( this, { 'open': 'onOpen' } ); // Initialization this.$element.addClass( 'oo-ui-dialog' ); @@ -113,8 +113,10 @@ } }; -/** */ -OO.ui.Dialog.prototype.onOpening = function () { +/** + * Handle window open events. + */ +OO.ui.Dialog.prototype.onOpen = function () { this.$element.addClass( 'oo-ui-dialog-open' ); }; @@ -146,7 +148,7 @@ */ OO.ui.Dialog.prototype.initialize = function () { // Parent method - OO.ui.Window.prototype.initialize.call( this ); + OO.ui.Dialog.super.prototype.initialize.call( this ); // Properties this.closeButton = new OO.ui.ButtonWidget( { @@ -172,41 +174,29 @@ /** * @inheritdoc */ -OO.ui.Dialog.prototype.setup = function ( data ) { - // Parent method - OO.ui.Window.prototype.setup.call( this, data ); - - // Prevent scrolling in top-level window - this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler ); - this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler ); +OO.ui.Dialog.prototype.getSetupProcess = function ( data ) { + return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data ) + .next( function () { + // Prevent scrolling in top-level window + this.$( window ).on( 'mousewheel', this.onWindowMouseWheelHandler ); + this.$( document ).on( 'keydown', this.onDocumentKeyDownHandler ); + }, this ); }; /** * @inheritdoc */ -OO.ui.Dialog.prototype.teardown = function ( data ) { - // Parent method - OO.ui.Window.prototype.teardown.call( this, data ); - - // Allow scrolling in top-level window - this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler ); - this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler ); -}; - -/** - * @inheritdoc - */ -OO.ui.Dialog.prototype.close = function ( data ) { - var dialog = this; - if ( !dialog.opening && !dialog.closing && dialog.visible ) { - // Trigger transition - dialog.$element.removeClass( 'oo-ui-dialog-open' ); - // Allow transition to complete before actually closing - setTimeout( function () { - // Parent method - OO.ui.Window.prototype.close.call( dialog, data ); - }, 250 ); - } +OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) { + return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data ) + .first( function () { + this.$element.removeClass( 'oo-ui-dialog-open' ); + return OO.ui.Process.static.delay( 250 ); + }, this ) + .next( function () { + // Allow scrolling in top-level window + this.$( window ).off( 'mousewheel', this.onWindowMouseWheelHandler ); + this.$( document ).off( 'keydown', this.onDocumentKeyDownHandler ); + }, this ); }; /** diff --git a/src/Frame.js b/src/Frame.js index 798ef35..9f3535e 100644 --- a/src/Frame.js +++ b/src/Frame.js @@ -16,7 +16,7 @@ OO.EventEmitter.call( this ); // Properties - this.loading = false; + this.loading = null; this.loaded = false; this.config = config; @@ -53,10 +53,10 @@ * * This loops over the style sheets in the parent document, and copies their nodes to the * frame's document. It then polls the document to see when all styles have loaded, and once they - * have, invokes the callback. + * have, resolves the promise. * * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting - * and invoke the callback anyway. This protects against cases like a display: none; iframe in + * and resolve the promise anyway. This protects against cases like a display: none; iframe in * Firefox, where the styles won't load until the iframe becomes visible. * * For details of how we arrived at the strategy used in this function, see #load. @@ -65,18 +65,19 @@ * @inheritable * @param {HTMLDocument} parentDoc Document to transplant styles from * @param {HTMLDocument} frameDoc Document to transplant styles to - * @param {Function} [callback] Callback to execute once styles have loaded * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up. + * @return {jQuery.Promise} Promise resolved when styles have loaded */ -OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, callback, timeout ) { +OO.ui.Frame.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) { var i, numSheets, styleNode, newNode, timeoutID, pollNodeId, $pendingPollNodes, $pollNodes = $( [] ), // Fake font-family value - fontFamily = 'oo-ui-frame-transplantStyles-loaded'; + fontFamily = 'oo-ui-frame-transplantStyles-loaded', + deferred = $.Deferred(); for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) { styleNode = parentDoc.styleSheets[i].ownerNode; - if ( callback && styleNode.nodeName.toLowerCase() === 'link' ) { + if ( styleNode.nodeName.toLowerCase() === 'link' ) { // External stylesheet // Create a node with a unique ID that we're going to monitor to see when the CSS // has loaded @@ -98,40 +99,40 @@ frameDoc.head.appendChild( newNode ); } - if ( callback ) { - // Poll every 100ms until all external stylesheets have loaded - $pendingPollNodes = $pollNodes; - timeoutID = setTimeout( function pollExternalStylesheets() { - while ( - $pendingPollNodes.length > 0 && - $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily - ) { - $pendingPollNodes = $pendingPollNodes.slice( 1 ); - } - - if ( $pendingPollNodes.length === 0 ) { - // We're done! - if ( timeoutID !== null ) { - timeoutID = null; - $pollNodes.remove(); - callback(); - } - } else { - timeoutID = setTimeout( pollExternalStylesheets, 100 ); - } - }, 100 ); - // ...but give up after a while - if ( timeout !== 0 ) { - setTimeout( function () { - if ( timeoutID ) { - clearTimeout( timeoutID ); - timeoutID = null; - $pollNodes.remove(); - callback(); - } - }, timeout || 5000 ); + // Poll every 100ms until all external stylesheets have loaded + $pendingPollNodes = $pollNodes; + timeoutID = setTimeout( function pollExternalStylesheets() { + while ( + $pendingPollNodes.length > 0 && + $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily + ) { + $pendingPollNodes = $pendingPollNodes.slice( 1 ); } + + if ( $pendingPollNodes.length === 0 ) { + // We're done! + if ( timeoutID !== null ) { + timeoutID = null; + $pollNodes.remove(); + deferred.resolve(); + } + } else { + timeoutID = setTimeout( pollExternalStylesheets, 100 ); + } + }, 100 ); + // ...but give up after a while + if ( timeout !== 0 ) { + setTimeout( function () { + if ( timeoutID ) { + clearTimeout( timeoutID ); + timeoutID = null; + $pollNodes.remove(); + deferred.reject(); + } + }, timeout || 5000 ); } + + return deferred.promise(); }; /* Methods */ @@ -139,7 +140,10 @@ /** * Load the frame contents. * - * Once the iframe's stylesheets are loaded, the `initialize` event will be emitted. + * Once the iframe's stylesheets are loaded, the `load` event will be emitted and the returned + * promise will be resolved. Calling while loading will return a promise but not trigger a new + * loading cycle. Calling after loading is complete will return a promise that's already been + * resolved. * * Sounds simple right? Read on... * @@ -167,15 +171,27 @@ * * All this stylesheet injection and polling magic is in #transplantStyles. * - * @private + * @return {jQuery.promise} Promise resolved when loading is complete * @fires load */ OO.ui.Frame.prototype.load = function () { - var win = this.$element.prop( 'contentWindow' ), - doc = win.document, - frame = this; + var win, doc; - this.loading = true; + // Return another promise if already loading + if ( this.loading ) { + return this.loading.promise(); + } + + // Return resolved promise if already loaded + if ( this.loaded ) { + return $.Deferred().resolve().promise(); + } + + // Load the frame + this.loading = $.Deferred(); + + win = this.$element.prop( 'contentWindow' ); + doc = win.document; // Figure out directionality: this.dir = this.$element.closest( '[dir]' ).prop( 'dir' ) || 'ltr'; @@ -197,37 +213,16 @@ this.$content = this.$( '.oo-ui-frame-content' ).attr( 'tabIndex', 0 ); this.$document = this.$( doc ); - this.constructor.static.transplantStyles( - this.getElementDocument(), - this.$document[0], - function () { - frame.loading = false; - frame.loaded = true; - frame.emit( 'load' ); - } - ); -}; + // Initialization + this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] ) + .always( OO.ui.bind( function () { + this.emit( 'load' ); + this.loading.resolve(); + this.loading = null; + this.loaded = true; + }, this ) ); -/** - * Run a callback as soon as the frame has been loaded. - * - * - * This will start loading if it hasn't already, and runs - * immediately if the frame is already loaded. - * - * Don't call this until the element is attached. - * - * @param {Function} callback - */ -OO.ui.Frame.prototype.run = function ( callback ) { - if ( this.loaded ) { - callback(); - } else { - if ( !this.loading ) { - this.load(); - } - this.once( 'load', callback ); - } + return this.loading.promise(); }; /** diff --git a/src/Process.js b/src/Process.js new file mode 100644 index 0000000..8d400be --- /dev/null +++ b/src/Process.js @@ -0,0 +1,113 @@ +/** + * A list of functions, called in sequence. + * + * If a function returns a promise, the next step in the process will not be taken until the promise + * is resolved. + * + * @class + * + * @constructor + */ +OO.ui.Process = function () { + // Properties + this.steps = []; +}; + +/* Setup */ + +OO.initClass( OO.ui.Process ); + +/* Static Methods */ + +/** + * Generate a promise which is resolved after a set amount of time. + * + * @param {number} length Number of milliseconds before resolving the promise + * @return {jQuery.Promise} Promise that will be resolved after a set amount of time + */ +OO.ui.Process.static.delay = function ( length ) { + var deferred = $.Deferred(); + + setTimeout( function () { + deferred.resolve(); + }, length ); + + return deferred.promise(); +}; + +/* Methods */ + +/** + * Start the process. + * + * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when + * any of the steps return boolean false or a promise which gets rejected + */ +OO.ui.Process.prototype.execute = function () { + var i, len, promise; + + /** + * Continue execution. + * + * @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 result = step[0].call( step[1] ); + + if ( result === false ) { + // Use rejected promise for boolean false results + return $.Deferred().reject().promise(); + } else if ( typeof result !== 'object' || !$.isFunction( result.promise ) ) { + // Use resolved promise for other non-promise results + return $.Deferred().resolve().promise(); + } else { + // Use returned promise + return result; + } + }; + } + + 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(); + } + + return promise; +}; + +/** + * Add step to the beginning of the process. + * + * @param {Function} step Function to execute; if it returns boolean false the process will stop; if + * it returns a promise the process will continue to the next step when the promise is resolved or + * will stop when the promise is rejected + * @param {Object} [context=null] Context to call the step function in + * @chainable + */ +OO.ui.Process.prototype.first = function ( step, context ) { + this.steps.unshift( [ step, context || null ] ); + return this; +}; + +/** + * Add step to the end of the process. + * + * @param {Function} step Function to execute; if it returns boolean false the process will stop; if + * it returns a promise the process will continue to the next step when the promise is resolved or + * will stop when the promise is rejected + * @param {Object} [context=null] Context to call the step function in + * @chainable + */ +OO.ui.Process.prototype.next = function ( step, context ) { + this.steps.push( [ step, context || null ] ); + return this; +}; diff --git a/src/Window.js b/src/Window.js index c6b7276..8b025ea 100644 --- a/src/Window.js +++ b/src/Window.js @@ -57,14 +57,6 @@ /* Events */ /** - * Initialize contents. - * - * Fired asynchronously after construction when iframe is ready. - * - * @event initialize - */ - -/** * Open window. * * Fired after window has been opened. @@ -268,7 +260,6 @@ * * Once this method is called, this.$$ can be used to create elements within the frame. * - * @fires initialize * @chainable */ OO.ui.Window.prototype.initialize = function () { @@ -295,97 +286,128 @@ // We can do this safely now that the iframe has initialized this.$element.hide().css( 'visibility', '' ); - this.emit( 'initialize' ); - return this; }; /** - * Setup window for use. + * Get a process for setting up a window for use. * - * Each time the window is opened, once it's ready to be interacted with, this will set it up for - * use in a particular context, based on the `data` argument. + * Each time the window is opened this process will set it up for use in a particular context, based + * on the `data` argument. * - * When you override this method, you must call the parent method at the very beginning. + * When you override this method, you can add additional setup steps to the process the parent + * method provides using the 'first' and 'then' methods. * * @abstract * @param {Object} [data] Window opening data + * @return {OO.ui.Process} Setup process */ -OO.ui.Window.prototype.setup = function () { - // Override to do something +OO.ui.Window.prototype.getSetupProcess = function () { + return new OO.ui.Process(); }; /** - * Tear down window after use. + * Get a process for readying a window for use. * - * Each time the window is closed, and it's done being interacted with, this will tear it down and - * do something with the user's interactions within the window, based on the `data` argument. + * Each time the window is open and setup, this process will ready it up for use in a particular + * context, based on the `data` argument. * - * When you override this method, you must call the parent method at the very end. + * When you override this method, you can add additional setup steps to the process the parent + * method provides using the 'first' and 'then' methods. + * + * @abstract + * @param {Object} [data] Window opening data + * @return {OO.ui.Process} Setup process + */ +OO.ui.Window.prototype.getReadyProcess = function () { + return new OO.ui.Process(); +}; + +/** + * Get a process for tearing down a window after use. + * + * Each time the window is closed this process will tear it down and do something with the user's + * interactions within the window, based on the `data` argument. + * + * When you override this method, you can add additional teardown steps to the process the parent + * method provides using the 'first' and 'then' methods. * * @abstract * @param {Object} [data] Window closing data + * @return {OO.ui.Process} Teardown process */ -OO.ui.Window.prototype.teardown = function () { - // Override to do something +OO.ui.Window.prototype.getTeardownProcess = function () { + return new OO.ui.Process(); }; /** * Open window. * - * Do not override this method. See #setup for a way to make changes each time the window opens. + * Do not override this method. Use #geSetupProcess to do something each time the window closes. * * @param {Object} [data] Window opening data + * @fires initialize * @fires opening * @fires open * @fires ready - * @chainable + * @return {jQuery.Promise} Promise resolved when window is opened */ OO.ui.Window.prototype.open = function ( data ) { + var deferred = $.Deferred(); + if ( !this.opening && !this.closing && !this.visible ) { this.opening = true; - this.frame.run( OO.ui.bind( function () { + this.frame.load().done( OO.ui.bind( function () { this.$element.show(); this.visible = true; this.emit( 'opening', data ); - this.setup( data ); - this.emit( 'open', data ); - setTimeout( OO.ui.bind( function () { - // Focus the content div (which has a tabIndex) to inactivate - // (but not clear) selections in the parent frame. - // Must happen after 'open' is emitted (to ensure it is visible) - // but before 'ready' is emitted (so subclasses can give focus to something else) - this.frame.$content.focus(); - this.emit( 'ready', data ); - this.opening = false; + this.getSetupProcess( data ).execute().done( OO.ui.bind( function () { + this.emit( 'open', data ); + setTimeout( OO.ui.bind( function () { + // Focus the content div (which has a tabIndex) to inactivate + // (but not clear) selections in the parent frame. + // Must happen after 'open' is emitted (to ensure it is visible) + // but before 'ready' is emitted (so subclasses can give focus to something + // else) + this.frame.$content.focus(); + this.getReadyProcess( data ).execute().done( OO.ui.bind( function () { + this.emit( 'ready', data ); + this.opening = false; + deferred.resolve(); + }, this ) ); + }, this ) ); }, this ) ); }, this ) ); } - return this; + return deferred; }; /** * Close window. * - * See #teardown for a way to do something each time the window closes. + * Do not override this method. Use #getTeardownProcess to do something each time the window closes. * * @param {Object} [data] Window closing data * @fires closing * @fires close - * @chainable + * @return {jQuery.Promise} Promise resolved when window is closed */ OO.ui.Window.prototype.close = function ( data ) { + var deferred = $.Deferred(); + if ( !this.opening && !this.closing && this.visible ) { this.frame.$content.find( ':focus' ).blur(); this.closing = true; - this.$element.hide(); - this.visible = false; this.emit( 'closing', data ); - this.teardown( data ); - this.emit( 'close', data ); - this.closing = false; + this.getTeardownProcess( data ).execute().done( OO.ui.bind( function () { + this.emit( 'close', data ); + this.$element.hide(); + this.visible = false; + this.closing = false; + deferred.resolve(); + }, this ) ); } - return this; + return deferred; }; diff --git a/src/layouts/StackLayout.js b/src/layouts/StackLayout.js index 9a73bb6..03fc992 100644 --- a/src/layouts/StackLayout.js +++ b/src/layouts/StackLayout.js @@ -68,6 +68,7 @@ * @chainable */ OO.ui.StackLayout.prototype.addItems = function ( items, index ) { + // Mixin method OO.ui.GroupElement.prototype.addItems.call( this, items, index ); if ( !this.currentItem && items.length ) { @@ -86,6 +87,7 @@ * @chainable */ OO.ui.StackLayout.prototype.removeItems = function ( items ) { + // Mixin method OO.ui.GroupElement.prototype.removeItems.call( this, items ); if ( $.inArray( this.currentItem, items ) !== -1 ) { this.currentItem = null; @@ -106,6 +108,8 @@ */ OO.ui.StackLayout.prototype.clearItems = function () { this.currentItem = null; + + // Mixin method OO.ui.GroupElement.prototype.clearItems.call( this ); return this; diff --git a/src/toolgroups/PopupToolGroup.js b/src/toolgroups/PopupToolGroup.js index cf72bda..70a651e 100644 --- a/src/toolgroups/PopupToolGroup.js +++ b/src/toolgroups/PopupToolGroup.js @@ -95,7 +95,7 @@ if ( !this.isDisabled() && e.which === 1 ) { this.setActive( false ); } - return OO.ui.ToolGroup.prototype.onMouseUp.call( this, e ); + return OO.ui.PopupToolGroup.super.prototype.onMouseUp.call( this, e ); }; /** diff --git a/src/widgets/ButtonOptionWidget.js b/src/widgets/ButtonOptionWidget.js index e949e08..936c01a 100644 --- a/src/widgets/ButtonOptionWidget.js +++ b/src/widgets/ButtonOptionWidget.js @@ -38,7 +38,7 @@ * @inheritdoc */ OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) { - OO.ui.OptionWidget.prototype.setSelected.call( this, state ); + OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state ); this.setActive( state ); diff --git a/src/widgets/GroupWidget.js b/src/widgets/GroupWidget.js index b3ff6d9..51eeed5 100644 --- a/src/widgets/GroupWidget.js +++ b/src/widgets/GroupWidget.js @@ -36,8 +36,8 @@ var i, len; // Parent method - // Note this is calling OO.ui.Widget; we're assuming the class this is mixed into - // is a subclass of OO.ui.Widget. + // Note: We are calling this constructor assuming this class is mixed into a subclass of + // OO.ui.Widget OO.ui.Widget.prototype.setDisabled.call( this, disabled ); // During construction, #setDisabled is called before the OO.ui.GroupElement constructor diff --git a/src/widgets/InputWidget.js b/src/widgets/InputWidget.js index aef779f..253390f 100644 --- a/src/widgets/InputWidget.js +++ b/src/widgets/InputWidget.js @@ -177,7 +177,7 @@ * @inheritdoc */ OO.ui.InputWidget.prototype.setDisabled = function ( state ) { - OO.ui.Widget.prototype.setDisabled.call( this, state ); + OO.ui.InputWidget.super.prototype.setDisabled.call( this, state ); if ( this.$input ) { this.$input.prop( 'disabled', this.isDisabled() ); } diff --git a/src/widgets/ItemWidget.js b/src/widgets/ItemWidget.js index 220af27..73c44d3 100644 --- a/src/widgets/ItemWidget.js +++ b/src/widgets/ItemWidget.js @@ -34,6 +34,8 @@ */ OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) { // Parent method + // Note: We are calling this constructor assuming this class is mixed into a subclass of + // OO.ui.Element OO.ui.Element.prototype.setElementGroup.call( this, group ); // Initialize item disabled states diff --git a/src/widgets/MenuWidget.js b/src/widgets/MenuWidget.js index 86b10aa..c2a1739 100644 --- a/src/widgets/MenuWidget.js +++ b/src/widgets/MenuWidget.js @@ -160,7 +160,7 @@ var i, len, item; // Parent method - OO.ui.SelectWidget.prototype.addItems.call( this, items, index ); + OO.ui.MenuWidget.super.prototype.addItems.call( this, items, index ); // Auto-initialize if ( !this.newItems ) { diff --git a/src/widgets/PopupButtonWidget.js b/src/widgets/PopupButtonWidget.js index f544a06..77ddfc7 100644 --- a/src/widgets/PopupButtonWidget.js +++ b/src/widgets/PopupButtonWidget.js @@ -45,7 +45,7 @@ } else { this.showPopup(); } - OO.ui.ButtonWidget.prototype.onClick.call( this ); + OO.ui.PopupButtonWidget.super.prototype.onClick.call( this ); } return false; }; diff --git a/src/widgets/SelectWidget.js b/src/widgets/SelectWidget.js index e83b836..dda03cc 100644 --- a/src/widgets/SelectWidget.js +++ b/src/widgets/SelectWidget.js @@ -436,7 +436,8 @@ this.removeItems( remove ); } - OO.ui.GroupElement.prototype.addItems.call( this, items, index ); + // Mixin method + OO.ui.GroupWidget.super.prototype.addItems.call( this, items, index ); // Always provide an index, even if it was omitted this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index ); @@ -467,7 +468,9 @@ this.selectItem( null ); } } - OO.ui.GroupElement.prototype.removeItems.call( this, items ); + + // Mixin method + OO.ui.GroupWidget.super.prototype.removeItems.call( this, items ); this.emit( 'remove', items ); @@ -487,7 +490,8 @@ // Clear all items this.hashes = {}; - OO.ui.GroupElement.prototype.clearItems.call( this ); + // Mixin method + OO.ui.GroupWidget.super.prototype.clearItems.call( this ); this.selectItem( null ); this.emit( 'remove', items ); diff --git a/src/widgets/TextInputMenuWidget.js b/src/widgets/TextInputMenuWidget.js index 4558753..9b80cb6 100644 --- a/src/widgets/TextInputMenuWidget.js +++ b/src/widgets/TextInputMenuWidget.js @@ -44,7 +44,7 @@ */ OO.ui.TextInputMenuWidget.prototype.show = function () { // Parent method - OO.ui.MenuWidget.prototype.show.call( this ); + OO.ui.TextInputMenuWidget.super.prototype.show.call( this ); this.position(); this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler ); @@ -58,7 +58,7 @@ */ OO.ui.TextInputMenuWidget.prototype.hide = function () { // Parent method - OO.ui.MenuWidget.prototype.hide.call( this ); + OO.ui.TextInputMenuWidget.super.prototype.hide.call( this ); this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler ); return this; diff --git a/src/widgets/TextInputWidget.js b/src/widgets/TextInputWidget.js index 8053245..c6eb5dd 100644 --- a/src/widgets/TextInputWidget.js +++ b/src/widgets/TextInputWidget.js @@ -90,7 +90,7 @@ this.adjustSize(); // Parent method - return OO.ui.InputWidget.prototype.onEdit.call( this ); + return OO.ui.TextInputWidget.super.prototype.onEdit.call( this ); }; /** diff --git a/src/widgets/ToggleButtonWidget.js b/src/widgets/ToggleButtonWidget.js index ee2fc8e..8bb2519 100644 --- a/src/widgets/ToggleButtonWidget.js +++ b/src/widgets/ToggleButtonWidget.js @@ -39,7 +39,7 @@ } // Parent method - return OO.ui.ButtonWidget.prototype.onClick.call( this ); + return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this ); }; /** @@ -52,7 +52,7 @@ } // Parent method - OO.ui.ToggleWidget.prototype.setValue.call( this, value ); + OO.ui.ToggleButtonWidget.super.prototype.setValue.call( this, value ); return this; }; diff --git a/test/OO.ui.Process.test.js b/test/OO.ui.Process.test.js new file mode 100644 index 0000000..63330ae --- /dev/null +++ b/test/OO.ui.Process.test.js @@ -0,0 +1,165 @@ +QUnit.module( 'OO.ui.Process' ); + +/* Tests */ + +QUnit.test( 'next', 1, function ( assert ) { + var process = new OO.ui.Process(), + result = []; + + process + .next( function () { + result.push( 0 ); + } ) + .next( function () { + result.push( 1 ); + } ) + .next( function () { + result.push( 2 ); + } ) + .execute(); + + assert.deepEqual( result, [ 0, 1, 2 ], 'Steps can be added at the end' ); +} ); + +QUnit.test( 'first', 1, function ( assert ) { + var process = new OO.ui.Process(), + result = []; + + process + .first( function () { + result.push( 0 ); + } ) + .first( function () { + result.push( 1 ); + } ) + .first( function () { + result.push( 2 ); + } ) + .execute(); + + assert.deepEqual( result, [ 2, 1, 0 ], 'Steps can be added at the beginning' ); +} ); + +QUnit.asyncTest( 'execute (async)', 1, function ( assert ) { + window.console.log( 'async test' ); + // Async + var process = new OO.ui.Process(), + result = []; + + process + .next( function () { + var deferred = $.Deferred(); + + setTimeout( function () { + result.push( 1 ); + deferred.resolve(); + }, 10 ); + + return deferred.promise(); + } ) + .first( function () { + result.push( 0 ); + } ) + .next( function () { + var deferred = $.Deferred(); + + setTimeout( function () { + result.push( 2 ); + deferred.resolve(); + }, 10 ); + + return deferred.promise(); + } ); + + process.execute().done( function () { + assert.deepEqual( + result, + [ 0, 1, 2 ], + 'Synchronous and asynchronous steps are executed in the correct order' + ); + QUnit.start(); + } ); +} ); + +QUnit.asyncTest( 'execute (return false)', 1, function ( assert ) { + var process = new OO.ui.Process(), + result = []; + + process + .next( function () { + var deferred = $.Deferred(); + + setTimeout( function () { + result.push( 0 ); + deferred.resolve(); + }, 10 ); + + return deferred.promise(); + } ) + .next( function () { + result.push( 1 ); + return false; + } ) + .next( function () { + // Should never be run because previous step is rejected + result.push( 2 ); + } ); + + process.execute().fail( function () { + assert.deepEqual( + result, + [ 0, 1 ], + 'Process is stopped when a step returns false' + ); + QUnit.start(); + } ); +} ); + +QUnit.asyncTest( 'execute (async reject)', 1, function ( assert ) { + var process = new OO.ui.Process(), + result = []; + + process + .next( function () { + result.push( 0 ); + } ) + .next( function () { + var deferred = $.Deferred(); + + setTimeout( function () { + result.push( 1 ); + deferred.reject(); + }, 10 ); + + return deferred.promise(); + } ) + .next( function () { + // Should never be run because previous step is rejected + result.push( 2 ); + } ); + + process.execute().fail( function () { + assert.deepEqual( + result, + [ 0, 1 ], + 'Process is stopped when a step returns a promise that is then rejected' + ); + QUnit.start(); + } ); +} ); + +QUnit.asyncTest( 'delay', 2, function ( assert ) { + var result = []; + + OO.ui.Process.static.delay( 10 ).done( function () { + result.push( 0 ); + } ); + + // Will still be empty because delayed promise hasn't been resolved yet + assert.deepEqual( result, [], 'Delayed promises take time to resolve' ); + + setTimeout( function () { + assert.deepEqual( result, [ 0 ], 'Delayed promises resolve after a time' ); + QUnit.start(); + }, 20 ); +} ); diff --git a/test/index.html b/test/index.html index abb89d9..a0d3645 100644 --- a/test/index.html +++ b/test/index.html @@ -15,6 +15,7 @@ <script src="../dist/oojs-ui.js"></script> <!-- Test suites --> <script src="./OO.ui.Element.test.js"></script> + <script src="./OO.ui.Process.test.js"></script> </head> <body> <div id="qunit"></div> -- To view, visit https://gerrit.wikimedia.org/r/133193 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I5f97185296e2c9a2d5fcdd9f2f7975e24995e49a Gerrit-PatchSet: 1 Gerrit-Project: oojs/ui Gerrit-Branch: master Gerrit-Owner: Trevor Parscal <tpars...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits