From 4759c2fc57aaf7cb73af6dbf5b84f512bb7efdb0 Mon Sep 17 00:00:00 2001 From: YUI Builder Date: Tue, 15 Dec 2009 14:13:42 -0800 Subject: [PATCH] gallery-2009.12.15-22 ipeychev gallery-undo --- src/gallery-undo/README | 5 + src/gallery-undo/apidocs.sh | 32 + src/gallery-undo/build.properties | 5 + src/gallery-undo/build.xml | 9 + src/gallery-undo/js/gallery-undoableaction.js | 230 ++++++ src/gallery-undo/js/gallery-undomanager.js | 724 ++++++++++++++++++ src/gallery-undo/tests/test_async.html | 18 + src/gallery-undo/tests/test_async.js | 191 +++++ src/gallery-undo/tests/test_sync.html | 18 + src/gallery-undo/tests/test_sync.js | 395 ++++++++++ 10 files changed, 1627 insertions(+) create mode 100644 src/gallery-undo/README create mode 100755 src/gallery-undo/apidocs.sh create mode 100644 src/gallery-undo/build.properties create mode 100644 src/gallery-undo/build.xml create mode 100644 src/gallery-undo/js/gallery-undoableaction.js create mode 100644 src/gallery-undo/js/gallery-undomanager.js create mode 100644 src/gallery-undo/tests/test_async.html create mode 100644 src/gallery-undo/tests/test_async.js create mode 100644 src/gallery-undo/tests/test_sync.html create mode 100644 src/gallery-undo/tests/test_sync.js diff --git a/src/gallery-undo/README b/src/gallery-undo/README new file mode 100644 index 0000000000..3412eb60ce --- /dev/null +++ b/src/gallery-undo/README @@ -0,0 +1,5 @@ +Undo Manager for YUI 3 + +I. Changelog + +16.11.2009 - Initial load \ No newline at end of file diff --git a/src/gallery-undo/apidocs.sh b/src/gallery-undo/apidocs.sh new file mode 100755 index 0000000000..18e7a145f1 --- /dev/null +++ b/src/gallery-undo/apidocs.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# The location of your yuidoc install +yuidoc_home=../../../yuidoc + +# The location of the files to parse. Parses subdirectories, but will fail if +# there are duplicate file names in these directories. You can specify multiple +# source trees: +# parser_in="%HOME/www/yui/src %HOME/www/event/src" +parser_in="js" + +# The location to output the parser data. This output is a file containing a +# json string, and copies of the parsed files. +parser_out=../../build/gallery-undo/apidocs/parser_out + +# The directory to put the html file outputted by the generator +generator_out=../../build/gallery-undo/apidocs/ + +# The location of the template files. Any subdirectories here will be copied +# verbatim to the destination directory. +template=$yuidoc_home/template + +# The version of your project to display within the documentation. +version=1.00 + +# The version of YUI the project is using. This effects the output for +# YUI configuration attributes. This should start with '2' or '3'. +yuiversion=3 + +############################################################################## +# add -s to the end of the line to show items marked private + +$yuidoc_home/bin/yuidoc.py $parser_in -p $parser_out -o $generator_out -t $template -v $version -Y $yuiversion diff --git a/src/gallery-undo/build.properties b/src/gallery-undo/build.properties new file mode 100644 index 0000000000..8fbb78aa39 --- /dev/null +++ b/src/gallery-undo/build.properties @@ -0,0 +1,5 @@ +builddir=../../../builder/componentbuild + +component=gallery-undo +component.jsfiles=gallery-undomanager.js gallery-undoableaction.js +component.requires=base,event diff --git a/src/gallery-undo/build.xml b/src/gallery-undo/build.xml new file mode 100644 index 0000000000..a04a1b5246 --- /dev/null +++ b/src/gallery-undo/build.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/gallery-undo/js/gallery-undoableaction.js b/src/gallery-undo/js/gallery-undoableaction.js new file mode 100644 index 0000000000..f1a78d5271 --- /dev/null +++ b/src/gallery-undo/js/gallery-undoableaction.js @@ -0,0 +1,230 @@ +/** + * Provides UndoableAction class + * + * @module gallery-undo + */ + +(function(){ + + +/** + * Create a UndoableAction + * + * @class UndoableAction + * @extends Base + * @param config {Object} Configuration object + * @constructor + */ +function UndoableAction( config ){ + UndoableAction.superclass.constructor.apply( this, arguments ); +} + +var Lang = Y.Lang, + UAName = "UndoableAction", + LABEL = "label", + BEFOREUNDO = "beforeUndo", + UNDOFINISHED = "undoFinished", + BEFOREREDO = "beforeRedo", + REDOFINISHED = "redoFinished"; + +Y.mix( UndoableAction, { + /** + * The identity of UndoableAction. + * + * @property UndoableAction.NAME + * @type String + * @static + */ + NAME : UAName, + + /** + * Static property used to define the default attribute configuration of UndoableAction. + * + * @property UndoableAction.ATTRS + * @type Object + * @protected + * @static + */ + ATTRS : { + /** + * The label of action + * + * @attribute label + * @type String + * @default "" + */ + label: { + value: "", + validator: Lang.isString + }, + + + /** + * Boolean, indicates if action must be processed asynchronously. + * If true, undo method must fire undoFinished event. + * Respectively, redo method must fire redoFinished event + * + * @attribute asyncProcessing + * @type Boolean + * @default false + */ + asyncProcessing : { + value: false, + validator: Lang.isBoolean + } + } +}); + + +Y.extend( UndoableAction, Y.Base, { + + /** + * Container for child actions of this action + * + * @property _childActions + * @protected + * @type Array + */ + _childActions : [], + + /** + * Publishes events + * + * @method initializer + * @protected + */ + initializer : function( cfg ) { + this._initEvents(); + }, + + /** + * Destructor lifecycle implementation for UndoableAction class. + * + * @method destructor + * @protected + */ + destructor : function() { + }, + + + /** + * Publishes UndoableAction's events + * + * @method _initEvents + * @protected + */ + _initEvents : function(){ + + /** + * Signals the beginning of action undo. + * + * @event beforeUndo + * @param event {Event.Facade} An Event Facade object + */ + this.publish( BEFOREUNDO ); + + /** + * Signals the end of action undo. + * + * @event undoFinished + * @param event {Event.Facade} An Event Facade object + */ + this.publish( UNDOFINISHED ); + + /** + * Signals the beginning of action redo. + * + * @event beforeRedo + * @param event {Event.Facade} An Event Facade object + */ + this.publish( BEFOREREDO ); + + /** + * Signals the end of action redo. + * + * @event redoFinished + * @param event {Event.Facade} An Event Facade object + */ + this.publish( REDOFINISHED ); + }, + + + /** + * The default implemetation undoes all child actions in reverse order. + * + * @method undo + */ + undo : function(){ + var childActions, action, i; + + this.fire( BEFOREUNDO ); + + childActions = this._childActions; + + for( i = childActions.length - 1; i > 0; i-- ){ + action = childActions[i]; + action.undo(); + } + + this.fire( UNDOFINISHED ); + }, + + + /** + * The default implemetation redoes all child actions. + * + * @method redo + */ + redo : function(){ + var childActions, action, i, length; + + this.fire( BEFOREREDO ); + + childActions = this._childActions; + length = childActions.length; + + for( i = 0; i < length; i++ ){ + action = childActions[i]; + action.redo(); + } + + this.fire( REDOFINISHED ); + }, + + + /** + * Depending on the application, an UndoableAction may merge with another action. If merge was successfull, merge must return true; otherwise returns false. + * The default implemetation returns false. + * + * @method merge + * @param {Y.UndoableAction} newAction The action to merge with + * @return {Boolean} false + */ + merge : function( newAction ){ + return false; + }, + + + /** + * UndoManager invokes cancel method of action before removing it from the list.
+ * The default implemetation does nothing. + * + * @method cancel + */ + cancel : function(){ + }, + + + /** + * Overrides toString() method.
+ * The default implementation returns the value of label property. + * + */ + toString : function(){ + return this.get( LABEL ); + } +}); + +Y.UndoableAction = UndoableAction; + +}()); diff --git a/src/gallery-undo/js/gallery-undomanager.js b/src/gallery-undo/js/gallery-undomanager.js new file mode 100644 index 0000000000..432c30bd47 --- /dev/null +++ b/src/gallery-undo/js/gallery-undomanager.js @@ -0,0 +1,724 @@ +/** + * Provides UndoManager class + * + * @module gallery-undo + */ + +(function(){ + + /** + * Create a UndoManager to manage list of undoable actions. + * + * @class UndoManager + * @extends Base + * @param config {Object} Configuration object + * @constructor + */ + function UndoManager( config ){ + UndoManager.superclass.constructor.apply( this, arguments ); + } + + var Lang = Y.Lang, + UMName = "UndoManager", + ACTIONADDED = "actionAdded", + ACTIONMERGED = "actionMerged", + BEFORECANCELING = "beforeCanceling", + ACTIONCANCELED = "actionCanceled", + CANCELINGFINISHED = "cancelingFinished", + BEFOREUNDO = "beforeUndo", + ACTIONUNDONE = "actionUndone", + UNDOFINISHED = "undoFinished", + BEFOREPURGE = "beforePurge", + PURGEFINISHED = "purgeFinished", + BEFOREREDO = "beforeRedo", + REDOFINISHED = "redoFinished", + ACTIONREDONE = "actionRedone", + ASYNCPROCESSING = "asyncProcessing", + UNLIMITED = 0; + + Y.mix( UndoManager, { + /** + * The identity of UndoManager. + * + * @property UndoManager.NAME + * @type String + * @static + */ + NAME : UMName, + + /** + * Static property used to define the default attribute configuration of UndoManager. + * + * @property UndoManager.ATTRS + * @type Object + * @protected + * @static + */ + ATTRS : { + + /** + * Holds the maximum number of actions in UndoManager. By default the number of actions is not limited. + * + * @attribute limit + * @type Number + * @default 0 (unlimited) + */ + limit: { + value: UNLIMITED, + validator: function( value ){ + return Lang.isNumber( value ) && value >= 0; + } + }, + + /** + * The index of command, that will be executed on the next call to redo(). + * If undo() has been not invoked, the value is the size of the current list of actions. + * Otherwise, it is the index of the last action that was undone. + * + * @attribute undoIndex + * @type Number + * @readOnly + */ + undoIndex : { + readOnly: true, + getter: function(){ + return this._undoIndex; + } + } + } + }); + + + Y.extend( UndoManager, Y.Base, { + + /** + * Collection of actions. + * @property _actions + * @protected + * @type Array + */ + _actions : [], + + /** + * If undo() has been not invoked, _undoIndex is the size of the current list of actions. + * Otherwise, it is the index of the last action that was undone. + * + * @property _undoIndex + * @protected + * @type Number + */ + _undoIndex : 0, + + + /** + * The handle of the currently executed asynchronous action + * + * @property _actionHandle + * @protected + * @type Object + */ + _actionHandle : null, + + /** + * Boolean, indicates if UndoManager is currently processing an action + * + * @property _processing + * @protected + * @type Boolean + */ + _processing : false, + + + /** + * Publishes events and subscribes to after event for limit. + * + * @method initializer + * @protected + */ + initializer : function( cfg ) { + this._initEvents(); + + this.after( "limitChange", Y.bind( this._afterLimit, this ) ); + }, + + /** + * Destructor lifecycle implementation for UndoManager class. + * Removes and cancels the added actions. + * + * @method destructor + * @protected + */ + destructor : function() { + this.purgeAll(); + }, + + + /** + * Publishes UndoManager's events + * + * @method _initEvents + * @protected + */ + _initEvents : function(){ + /** + * Signals an Y.UndoableAction has been added to list + * + * @event actionAdded + * @param event {Event.Facade} An Event Facade object with the following attribute specific properties added: + *
+ *
action
+ *
An Y.UndoableAction added to the list
+ *
+ */ + this.publish( ACTIONADDED ); + + /** + * Signals an Y.UndoableAction has been merged with another one + * + * @event actionMerged + * @param event {Event.Facade} An Event Facade object with the following attribute specific properties added: + *
+ *
Y.UndoableAction action
+ *
The action, accepted merge
+ *
Y.UndoableAction mergedAction
+ *
The merged action
+ *
+ */ + this.publish( ACTIONMERGED ); + + /** + * Signals the beginning of a process in which one or more actions will be canceled. + * + * @event beforeCanceling + * @param event {Event.Facade} An Event Facade object + */ + this.publish( BEFORECANCELING ); + + /** + * Signals an action has been canceled. + * + * @event actionCanceled + * @param event {Event.Facade} An Event Facade object with the following attribute specific properties added: + *
+ *
action
+ *
An Y.UndoableAction canceled
+ *
index
+ *
The index of the action in the list
+ *
+ */ + this.publish( ACTIONCANCELED ); + + + /** + * Signals a canceling actions process has been finished. + * + * @event cancelingFinished + * @param event {Event.Facade} An Event Facade object + */ + this.publish( CANCELINGFINISHED ); + + /** + * Signals the beginning of a process in which one or more actions will be purged from the list. + * + * @event beforePurge + * @param event {Event.Facade} An Event Facade object + */ + this.publish( BEFOREPURGE ); + + + /** + * Signals the end of purge process. UndoManager cancels each action before its removing. + * + * @event purgeFinished + * @param event {Event.Facade} An Event Facade object + */ + this.publish( PURGEFINISHED ); + + /** + * Signals the beginning of a process in which one or more actions will be undone. + * + * @event beforeUndo + * @param event {Event.Facade} An Event Facade object + */ + this.publish( BEFOREUNDO ); + + + /** + * Signals an action has been undone. + * + * @event actionUndone + * @param event {Event.Facade} An Event Facade object with the following attribute specific properties added: + *
+ *
action
+ *
An Y.UndoableAction undone
+ *
index
+ *
The index of the action in the list
+ *
+ */ + this.publish( ACTIONUNDONE ); + + + /** + * Signals the end of undo process. + * + * @event undoFinished + * @param event {Event.Facade} An Event Facade object + */ + this.publish( UNDOFINISHED ); + + + /** + * Signals the beginning of a process in which one or more actions will be redone. + * + * @event beforeRedo + * @param event {Event.Facade} An Event Facade object + */ + this.publish( BEFOREREDO ); + + + /** + * Signals an action has been redone. + * + * @event actionRedone + * @param event {Event.Facade} An Event Facade object with the following attribute specific properties added: + *
+ *
action
+ *
An Y.UndoableAction redone
+ *
index
+ *
The index of the action in the list
+ *
+ */ + this.publish( ACTIONREDONE ); + + + /** + * Signals the end of redo process. + * + * @event redoFinished + * @param event {Event.Facade} An Event Facade object + */ + this.publish( REDOFINISHED ); + }, + + + /** + * Adds an UndoableAction to UndoManager.
+ * Removes and cancels all actions from the current action index till the end of the list. + * Tries to merge the current action with the newAction, passed as parameter. If currentAction.merge(newAction) returns false, UndoManager places the newAction at the end of the list.
+ * Fires actionAdded event if action has been added to the list, or actionMerged if newAction has been merged. + * @method add + * @param {Y.UndoableAction} newAction The action to be added + * @return {Boolean} True if action was added to the list. The result might be False if UndoManager was processing another (asynchronous) action. + */ + add : function( newAction ){ + var curAction = null, actions, undoIndex, tmp, merged = false; + + if( this._processing ){ + return false; + } + + actions = this._actions; + undoIndex = this._undoIndex; + + if( undoIndex > 0 ){ + curAction = actions[ undoIndex - 1 ]; + } + + if( undoIndex < actions.length ){ + this.fire( BEFORECANCELING ); + + while( undoIndex < actions.length ){ + tmp = actions.splice( -1, 1 )[0]; + + tmp.cancel(); + this.fire( ACTIONCANCELED, { + action: tmp, + index : actions.length + }); + } + + this.fire( CANCELINGFINISHED ); + } + + if( curAction ){ + merged = curAction.merge( newAction ); + + if( !merged ){ + actions.push( newAction ); + } + } else { + actions.push( newAction ); + } + + if( !merged ){ + this._undoIndex++; + this._limitActions(); + this.fire( ACTIONADDED, newAction ); + } else { + this.fire( ACTIONMERGED, curAction, newAction ); + } + + return true; + }, + + + /** + * Removes actions from the list if their number exceedes the limit + * + * @method _limitActions + * @param {Number} limit The max number of actions in the list + * @protected + */ + _limitActions : function( limit ){ + var actions, action, + halfLimit, actionsLeft, actionsRight, deleteLeft, deleteRight, + index, i, j; + + if( !limit ){ + limit = this.get( "limit" ); + } + + if( limit === UNLIMITED ){ + return; + } + + actions = this._actions; + + if( actions.length <= limit ){ + return; + } + + index = this._undoIndex; + + halfLimit = parseInt( limit / 2, 10 ); + + actionsLeft = limit - halfLimit; + actionsRight = limit - actionsLeft; + + deleteLeft = index - actionsLeft; + deleteRight = actions.length - index - actionsRight; + + if( deleteLeft < 0 ){ + deleteRight += deleteLeft; + } else if( deleteRight < 0 ){ + deleteLeft += deleteRight; + } + + if( deleteLeft > 0 || deleteRight > 0 ){ + this.fire( BEFORECANCELING ); + + for( i = 0; i < deleteLeft; i++ ){ + this._undoIndex--; + + action = actions.splice( 0, 1 )[0]; + action.cancel(); + this.fire( ACTIONCANCELED, { + 'action': action, + index : 0 + }); + } + + for( i = actions.length - 1, j = 0; j < deleteRight; i--, j++ ){ + action = actions.splice( i, 1 )[0]; + action.cancel(); + this.fire( ACTIONCANCELED, { + 'action': action, + index : i + }); + } + + this.fire( CANCELINGFINISHED ); + } + }, + + /** + * Invokes _limitActions in order to keep the number of actions in the list according to the limit. + * + * @method _afterLimit + * @param params {Event} limitChange custom event + * @protected + */ + _afterLimit : function( params ){ + this._limitActions( params.newVal ); + }, + + + /** + * Undoes the action before current index by calling its undo method. + * If asyncProcessing property of the action is true, UndoManager waits until action fires undoFinished event. + * During this time undoing/redoing and adding new actions will be suspended. + * + * @method undo + */ + undo : function(){ + if( this.canUndo() ){ + this._undoTo( this._undoIndex - 1 ); + } + }, + + /** + * Redoes the action at current index by calling its redo method. + * If asyncProcessing property of the action is true, UndoManager waits until action fires redoFinished event. + * During this time undoing/redoing and adding new actions will be suspended. + * + * @method redo + */ + redo : function(){ + if( this.canRedo() ){ + this._redoTo( this._undoIndex + 1 ); + } + }, + + + /** + * Checks if undo can be done. The function will return false if there are no actions in the list, + * the current index is 0 or UndoManager is waiting for another asynchronous action to complete. + * + * @method canUndo + * @return {Boolean} true if undo is possible, false otherwise + */ + canUndo : function(){ + return !this._processing && this._undoIndex > 0; + }, + + + /** + * Checks if redo can be done. The function will return false if there are no actions in the list, + * current index is equal to the length of the list or UndoManager is waiting for another asynchronous action to complete. + * + * @method canRedo + * @return {Boolean} true if redo is possible, false otherwise + */ + canRedo : function(){ + return !this._processing && this._undoIndex < this._actions.length; + }, + + + /** + * If undo is posible, returns the value of label property of the action to be undone. + * + * @method getUndoLabel + * @return {String} The value of label property + */ + getUndoLabel : function(){ + var action; + + if( this.canUndo() ){ + action = this._actions[ this._undoIndex - 1 ]; + return action.get( "label" ); + } + + return null; + }, + + + /** + * If redo is posible, returns the value of label property of the action to be redone. + * + * @method getRedoLabel + * @return {String} The value of label property + */ + getRedoLabel : function(){ + var action; + + if( this.canRedo() ){ + action = this._actions[ this._undoIndex ]; + return action.get( "label" ); + } + + return null; + }, + + + /** + * Cancels and removes all actions from the list + * + * @method purgeAll + */ + purgeAll : function(){ + this.purgeTo( 0 ); + }, + + + /** + * Cancels and removes actions from the end of the list (the most recent actions) to the index, passed as parameter. + * + * @method purgeTo + * @param {Number} index The index in the list to which actions should be be removed + */ + purgeTo : function( index ){ + var action, i = this._actions.length - 1; + + if( i >= index ){ + this.fire( BEFOREPURGE ); + + for( ; i >= index; i-- ) { + action = this._actions.splice( i, 1 )[0]; + + action.cancel(); + this.fire( ACTIONCANCELED, { + 'action': action, + index : i + }); + } + + if( this._undoIndex > index ){ + this._undoIndex = index; + } + + this._processing = false; + + this.fire( PURGEFINISHED ); + } + }, + + + /** + * Calls undo or redo methods of the actions registered while current index is less or greater than the newIndex passed. + * + * @method processTo + * @param newIndex The new value of undoIndex + */ + processTo : function( newIndex ){ + if( Lang.isNumber(newIndex) && !this._processing && + newIndex >= 0 && newIndex <= this._actions.length ){ + if( this._undoIndex < newIndex ){ + this._redoTo( newIndex ); + } else if( this._undoIndex > newIndex ){ + this._undoTo( newIndex ); + } + } + }, + + + /** + * Redoes all actions from current index to newIndex. In case of asynchronous action, waits until action fires redoFinished event. + * + * @method _redoTo + * @protected + * @param newIndex The new value of undoIndex + */ + _redoTo : function( newIndex ){ + var action = this._actions[ this._undoIndex++ ]; + + if( !this._processing ){ + this.fire( BEFOREREDO ); + this._processing = true; + } + + if( !action.get( ASYNCPROCESSING ) ){ + action.redo(); + this.fire( ACTIONREDONE, { + 'action' : action, + index : this._undoIndex - 1 + } ); + + if( this._undoIndex < newIndex ){ + this._redoTo( newIndex ); + } else { + this._processing = false; + this.fire( REDOFINISHED ); + } + } else { + this._actionHandle = action.on( REDOFINISHED, + Y.bind( this._onAsyncRedoFinished, this, action, newIndex ) ); + + action.redo(); + } + }, + + + /** + * Undoes all actions from current index to newIndex. In case of asynchronous action, waits until action fires undoFinished event. + * + * @method _undoTo + * @protected + * @param newIndex The new value of undoIndex + */ + _undoTo : function( newIndex ){ + var action = this._actions[ --this._undoIndex ]; + + if( !this._processing ){ + this.fire( BEFOREUNDO ); + this._processing = true; + } + + if( !action.get( ASYNCPROCESSING ) ){ + action.undo(); + this.fire( ACTIONUNDONE, { + 'action': action, + index : this._undoIndex + }); + + if( this._undoIndex > newIndex ){ + this._undoTo( newIndex ); + } else { + this._processing = false; + this.fire( UNDOFINISHED ); + } + } else { + this._actionHandle = action.on( UNDOFINISHED, + Y.bind( this._onAsyncUndoFinished, this, action, newIndex ) ); + + action.undo(); + } + }, + + + /** + * Handles the completion of undo method of asynchronous action. + * Fires actionUndone event. Checks if newIndex is less than current index. If true, invokes _undoTo again, or fires undoFinished event otherwise. + * + * @method _onAsyncUndoFinished + * @protected + * @param {Y.UndoableAction} action The asynchronous action which undo method has been completed. + * @param {Number} newIndex The new value of undoIndex + */ + _onAsyncUndoFinished : function( action, newIndex ){ + this._actionHandle.detach(); + this._actionHandle = null; + + this.fire( ACTIONUNDONE, { + 'action': action, + index : this._undoIndex + }); + + if( this._undoIndex > newIndex ){ + this._undoTo( newIndex ); + } else { + this._processing = false; + this.fire( UNDOFINISHED, action ); + } + }, + + + /** + * Handles the completion of redo method of asynchronous action. + * Fires actionRedone event. Checks if newIndex is bigger than current index. If true, invokes _redoTo again, or fires redoFinished event otherwise. + * + * @method _onAsyncRedoFinished + * @protected + * @param {Y.UndoableAction} action The asynchronous action which redo method has been completed. + * @param {Number} newIndex The new value of undoIndex + */ + _onAsyncRedoFinished : function( action, newIndex ){ + this._actionHandle.detach(); + this._actionHandle = null; + + this.fire( ACTIONREDONE, { + 'action': action, + index : this._undoIndex - 1 + }); + + if( this._undoIndex < newIndex ){ + this._redoTo( newIndex ); + } else { + this._processing = false; + this.fire( REDOFINISHED, action ); + } + } + }); + + Y.UndoManager = UndoManager; + +}()); diff --git a/src/gallery-undo/tests/test_async.html b/src/gallery-undo/tests/test_async.html new file mode 100644 index 0000000000..40f0040c0b --- /dev/null +++ b/src/gallery-undo/tests/test_async.html @@ -0,0 +1,18 @@ + + + + + UndoManager asynchronous undo test page + + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/src/gallery-undo/tests/test_async.js b/src/gallery-undo/tests/test_async.js new file mode 100644 index 0000000000..e50c3d3319 --- /dev/null +++ b/src/gallery-undo/tests/test_async.js @@ -0,0 +1,191 @@ + +YUI({ + combine: false, + debug: true, + filter:"RAW" +}).use('gallery-undo', 'test', 'console', function(Y) { + var that = this, testArray = [], console, asyncActions = 20; + + function TestAsyncUndoableAction( config ){ + TestAsyncUndoableAction.superclass.constructor.apply( this, arguments ); + } + + Y.extend( TestAsyncUndoableAction, Y.UndoableAction, { + undo : function(){ + window.setTimeout( Y.bind(function(){ + testArray.splice( -1, 1 ); + this.fire( "undoFinished" ); + }, this), 100 ); + }, + + redo : function(){ + window.setTimeout( Y.bind(function(){ + testArray.push( testArray.length ); + this.fire( "redoFinished" ); + }, this), 100 ); + } + }); + + this.undoManager = new Y.UndoManager(); + + var testAsynchronousActions = new Y.Test.Case({ + name: "Test asynchronous action", + + testAddActions: function(){ + var undoableAction, canUndo, canRedo, i; + + for( i = 0; i < asyncActions; i++ ){ + undoableAction = new TestAsyncUndoableAction({ + asyncProcessing: true, + label : "Async action: " + i + }); + + testArray.push( testArray.length ); + that.undoManager.add( undoableAction ); + } + + canUndo = that.undoManager.canUndo(); + canRedo = that.undoManager.canRedo(); + + Y.Assert.areEqual( true, canUndo, "Undoing must be allowed" ); + Y.Assert.areEqual( false, canRedo, "Redoing must be not allowed" ); + Y.Assert.areEqual( asyncActions, testArray.length, "There must be " + asyncActions + " actions in testArray" ); + Y.Assert.areEqual( asyncActions, that.undoManager.get( "undoIndex" ), "Undo index must be: " + asyncActions ); + }, + + testUndoAction: function(){ + var undoIndex, i = asyncActions - 1; + + this._undoFinishedHandler = that.undoManager.subscribe( "undoFinished", Y.bind( function(){ + if( i > 0 ){ + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( i, undoIndex, "Undo index must be: " + i ); + Y.Assert.areEqual( true, that.undoManager.canUndo(), "Undoing must be allowed" ); + Y.Assert.areEqual( true, that.undoManager.canRedo(), "Redoing must be allowed" ); + Y.Assert.areEqual( i, testArray.length, "Test array must contain:" + i + " items" ); + that.undoManager.undo(); + } else { + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( 0, undoIndex, "Undo index must be: " + 0 ); + Y.Assert.areEqual( false, that.undoManager.canUndo(), "Undoing must be not allowed" ); + Y.Assert.areEqual( true, that.undoManager.canRedo(), "Redoing must be allowed" ); + Y.Assert.areEqual( 0, testArray.length, "Test array must be empty" ); + + this._undoFinishedHandler.detach(); + this._undoFinishedHandler = null; + this.resume(); + } + + --i; + }, this)); + + that.undoManager.undo(); + this.wait( null, 0 ); + }, + + testRedoAction: function(){ + var undoIndex, i = 0; + + this._redoFinishedHandler = that.undoManager.subscribe( "redoFinished", Y.bind( function(){ + if( i < asyncActions - 1 ){ + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( i + 1, undoIndex, "Undo index must be: " + (i + 1) ); + Y.Assert.areEqual( true, that.undoManager.canUndo(), "Undoing must be allowed" ); + Y.Assert.areEqual( true, that.undoManager.canRedo(), "Redoing must be allowed" ); + Y.Assert.areEqual( i + 1, testArray.length, "Test array must contain:" + (i + 1) + " items" ); + that.undoManager.redo(); + } else { + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( asyncActions, undoIndex, "Undo index must be: " + asyncActions ); + Y.Assert.areEqual( true, that.undoManager.canUndo(), "Undoing must be allowed" ); + Y.Assert.areEqual( false, that.undoManager.canRedo(), "Redoing must be not allowed" ); + + this._redoFinishedHandler.detach(); + this._redoFinishedHandler = null; + this.resume(); + } + + ++i; + }, this )); + + that.undoManager.redo(); + this.wait( null, 0 ); + }, + + testMultipleUndo: function(){ + var undoIndex; + + this._undoFinishedHandler = that.undoManager.subscribe( "undoFinished", Y.bind( function(){ + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( 0, undoIndex, "Undo index must be: " + 0 ); + Y.Assert.areEqual( 0, testArray.length, "Test array must be ampty" ); + + this._undoFinishedHandler.detach(); + this._undoFinishedHandler = null; + this.resume(); + }, this )); + + that.undoManager.processTo( 0 ); + this.wait( null, 0 ); + }, + + testMultipleRedo: function(){ + var undoIndex; + + this._redoFinishedHandler = that.undoManager.subscribe( "redoFinished", Y.bind( function(){ + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( asyncActions, undoIndex, "Undo index must be: " + asyncActions ); + Y.Assert.areEqual( asyncActions, testArray.length, "Test array must contain " + asyncActions + "actions" ); + + this._redoFinishedHandler.detach(); + this._redoFinishedHandler = null; + this.resume(); + }, this )); + + that.undoManager.processTo( asyncActions ); + this.wait( null, 0 ); + } + }); + + + var testAsynchronousActionsLimit = new Y.Test.Case({ + testSetLimit: function(){ + var undoableAction, actions; + + actions = that.undoManager._actions; + that.undoManager.set( "limit", asyncActions ); + + undoableAction = new TestAsyncUndoableAction({ + asyncProcessing : true, + "label" : "Action, added after limit" + }); + + that.undoManager.add( undoableAction ); + + Y.Assert.areEqual( asyncActions, testArray.length, "There must be total: " + asyncActions ); + Y.Assert.areEqual( undoableAction, actions[ actions.length - 1 ], "The new added action must be the last one" ); + + // set unlimited number of actions + that.undoManager.set( "limit", 0 ); + Y.Assert.areEqual( 0, that.undoManager.get( "limit" ), "The number of actions must be unlimited now" ); + } + }); + + + Y.Test.Runner.add(testAsynchronousActions); + Y.Test.Runner.add(testAsynchronousActionsLimit); + + + console = new Y.Console({ + verbose : false, + printTimeout: 0, + newestOnTop : false, + + entryTemplate: '
'+
+                '{label}'+
+                '{message}'+
+        '
' + }).render(); + + Y.Test.Runner.run(); +}); \ No newline at end of file diff --git a/src/gallery-undo/tests/test_sync.html b/src/gallery-undo/tests/test_sync.html new file mode 100644 index 0000000000..4f11b0b8c3 --- /dev/null +++ b/src/gallery-undo/tests/test_sync.html @@ -0,0 +1,18 @@ + + + + + UndoManager synchronous undo test page + + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/src/gallery-undo/tests/test_sync.js b/src/gallery-undo/tests/test_sync.js new file mode 100644 index 0000000000..8396d6c2fc --- /dev/null +++ b/src/gallery-undo/tests/test_sync.js @@ -0,0 +1,395 @@ + +YUI({ + combine: false, + debug: true, + filter:"RAW" +}).use('gallery-undo', 'test', 'console', function(Y) { + var that = this, testArray = [], console, synActions = 20; + + function TestUndoableAction( config ){ + TestUndoableAction.superclass.constructor.apply( this, arguments ); + } + + Y.extend( TestUndoableAction, Y.UndoableAction, { + undo : function(){ + testArray.splice( -1, 1 ); + }, + + redo : function(){ + testArray.push( testArray.length ); + }, + + toString : function(){ + return this.get( "label" ); + } + }); + + + this.undoManager = new Y.UndoManager(); + + var testSynchronousActions = new Y.Test.Case({ + name: "Test synchronous action", + + testAddActions: function(){ + var undoableAction, canUndo, canRedo, i; + + for( i = 0; i < synActions; i++ ){ + undoableAction = new TestUndoableAction({ + "label" : "Action: " + i + }); + + undoableAction.redo(); + that.undoManager.add( undoableAction ); + } + + canUndo = that.undoManager.canUndo(); + canRedo = that.undoManager.canRedo(); + + Y.Assert.areEqual( true, canUndo, "Undoing must be allowed" ); + Y.Assert.areEqual( false, canRedo, "Redoing must be not allowed" ); + Y.Assert.areEqual( synActions, testArray.length, "There must be " + synActions + " actions in testArray" ); + Y.Assert.areEqual( synActions, that.undoManager.get( "undoIndex" ), "Undo index must be: " + synActions ); + }, + + testUndoAction: function(){ + var undoIndex, i; + + for( i = synActions - 1; i > 0; i-- ){ + that.undoManager.undo(); + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( i, undoIndex, "Undo index must be: " + i ); + Y.Assert.areEqual( true, that.undoManager.canUndo(), "Undoing must be allowed" ); + Y.Assert.areEqual( true, that.undoManager.canRedo(), "Redoing must be allowed" ); + Y.Assert.areEqual( i, testArray.length, "Test array must contain:" + i + " actions" ); + } + + that.undoManager.undo(); + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( 0, undoIndex, "Undo index must be: " + 0 ); + Y.Assert.areEqual( false, that.undoManager.canUndo(), "Undoing must be not allowed" ); + Y.Assert.areEqual( true, that.undoManager.canRedo(), "Redoing must be allowed" ); + + Y.Assert.areEqual( 0, testArray.length, "Test array must be empty" ); + }, + + testRedoAction: function(){ + var undoIndex, i; + + for( i = 0; i < synActions - 1; i++ ){ + that.undoManager.redo(); + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( i + 1, undoIndex, "Undo index must be: " + (i + 1) ); + Y.Assert.areEqual( true, that.undoManager.canUndo(), "Undoing must be allowed" ); + Y.Assert.areEqual( true, that.undoManager.canRedo(), "Redoing must be allowed" ); + Y.Assert.areEqual( i + 1, testArray.length, "Test array must contain:" + (i + 1) + " actions" ); + } + + that.undoManager.redo(); + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( synActions, undoIndex, "Undo index must be: " + synActions ); + Y.Assert.areEqual( true, that.undoManager.canUndo(), "Undoing must be allowed" ); + Y.Assert.areEqual( false, that.undoManager.canRedo(), "Redoing must be not allowed" ); + }, + + testMultipleUndo: function(){ + var undoIndex; + + that.undoManager.processTo( 0 ); + + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( 0, undoIndex, "Undo index must be: " + 0 ); + Y.Assert.areEqual( 0, testArray.length, "Test array must be ampty" ); + }, + + testMultipleRedo: function(){ + var undoIndex; + + that.undoManager.processTo( synActions ); + + undoIndex = that.undoManager.get( "undoIndex" ); + Y.Assert.areEqual( synActions, undoIndex, "Undo index must be: " + synActions ); + Y.Assert.areEqual( synActions, testArray.length, "Test array must contain " + synActions + "actions" ); + } + }); + + + var testSynchronousActionsLimit = new Y.Test.Case({ + + prepareLimitTest : function(){ + var undoableAction, i; + + that.undoManager.purgeAll(); + that.undoManager.set( "limit", 0 ); + + for( i = 0; i < 5; i++ ){ + undoableAction = new TestUndoableAction({ + "label" : "Action" + i + }); + + that.undoManager.add(undoableAction); + } + }, + + testSetLimit: function(){ + var undoableAction, actions; + + actions = that.undoManager._actions; + that.undoManager.set( "limit", synActions ); + + undoableAction = new TestUndoableAction({ + "label" : "Action, added after limit" + }); + + that.undoManager.add( undoableAction ); + + Y.Assert.areEqual( synActions, testArray.length, "There must be total: " + synActions ); + Y.Assert.areEqual( undoableAction, actions[ actions.length - 1 ], "The new added action must be the last one" ); + + // set unlimited number of actions + that.undoManager.set( "limit", 0 ); + Y.Assert.areEqual( 0, that.undoManager.get( "limit" ), "The number of actions must be unlimited now" ); + }, + + testLimitTo1Action: function(){ + var actions = that.undoManager._actions, undoableAction; + + that.undoManager.purgeAll(); + that.undoManager.set( "limit", 1 ); + + undoableAction = new TestUndoableAction({ + "label" : "Action0" + }); + that.undoManager.add( undoableAction ); + + Y.Assert.areEqual( 1, actions.length, "There must be 1 item" ); + Y.Assert.areEqual( "Action0", actions[0].get( "label" ), "Label must be Action0" ); + + undoableAction = new TestUndoableAction({ + "label" : "Action1" + }); + that.undoManager.add( undoableAction ); + + Y.Assert.areEqual( 1, actions.length, "There must be 1 item" ); + Y.Assert.areEqual( "Action1", actions[0].get( "label" ), "Label must be Action1" ); + + undoableAction = new TestUndoableAction({ + "label" : "Action2" + }); + that.undoManager.add( undoableAction ); + + Y.Assert.areEqual( 1, actions.length, "There must be 1 item" ); + Y.Assert.areEqual( "Action2", actions[0].get( "label" ), "Label must be Action2" ); + }, + + testSetLimit0: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo(0); + that.undoManager.set( "limit", 3 ); + + Y.Assert.areEqual( 3, actions.length, "There must be 3 actions" ); + Y.Assert.areEqual( "Action0", actions[0].get( "label" ), "Label must be Action0" ); + Y.Assert.areEqual( "Action1", actions[1].get( "label" ), "Label must be Action1" ); + Y.Assert.areEqual( "Action2", actions[2].get( "label" ), "Label must be Action2" ); + }, + + testSetLimit1: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo(1); + that.undoManager.set( "limit", 3 ); + + Y.Assert.areEqual( 3, actions.length, "There must be 3 actions" ); + Y.Assert.areEqual( "Action0", actions[0].get( "label" ), "Label must be Action0" ); + Y.Assert.areEqual( "Action1", actions[1].get( "label" ), "Label must be Action1" ); + Y.Assert.areEqual( "Action2", actions[2].get( "label" ), "Label must be Action2" ); + }, + + testSetLimit2: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo( 2 ); + that.undoManager.set( "limit", 3 ); + + Y.Assert.areEqual( 3, actions.length, "There must be 3 actions" ); + Y.Assert.areEqual( "Action0", actions[0].get( "label" ), "Label must be Action0" ); + Y.Assert.areEqual( "Action1", actions[1].get( "label" ), "Label must be Action1" ); + Y.Assert.areEqual( "Action2", actions[2].get( "label" ), "Label must be Action2" ); + }, + + testSetLimit3: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo( 3 ); + that.undoManager.set( "limit", 3 ); + + Y.Assert.areEqual( 3, actions.length, "There must be 3 actions" ); + Y.Assert.areEqual( "Action1", actions[0].get( "label" ), "Label must be Action1" ); + Y.Assert.areEqual( "Action2", actions[1].get( "label" ), "Label must be Action2" ); + Y.Assert.areEqual( "Action3", actions[2].get( "label" ), "Label must be Action3" ); + }, + + testSetLimit4: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo( 4 ); + that.undoManager.set( "limit", 3 ); + + Y.Assert.areEqual( 3, actions.length, "There must be 3 actions" ); + Y.Assert.areEqual( "Action2", actions[0].get( "label" ), "Label must be Action2" ); + Y.Assert.areEqual( "Action3", actions[1].get( "label" ), "Label must be Action3" ); + Y.Assert.areEqual( "Action4", actions[2].get( "label" ), "Label must be Action4" ); + }, + + testSetLimit5: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo( 5 ); + that.undoManager.set( "limit", 3 ); + + Y.Assert.areEqual( 3, actions.length, "There must be 3 actions" ); + Y.Assert.areEqual( "Action2", actions[0].get( "label" ), "Label must be Action2" ); + Y.Assert.areEqual( "Action3", actions[1].get( "label" ), "Label must be Action3" ); + Y.Assert.areEqual( "Action4", actions[2].get( "label" ), "Label must be Action4" ); + }, + + testSetLimit6: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo(1); + that.undoManager.set( "limit", 4 ); + + Y.Assert.areEqual( 4, actions.length, "There must be 4 actions" ); + Y.Assert.areEqual( "Action0", actions[0].get( "label" ), "Label must be Action0" ); + Y.Assert.areEqual( "Action1", actions[1].get( "label" ), "Label must be Action1" ); + Y.Assert.areEqual( "Action2", actions[2].get( "label" ), "Label must be Action2" ); + Y.Assert.areEqual( "Action3", actions[3].get( "label" ), "Label must be Action3" ); + }, + + testSetLimit7: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo(2); + that.undoManager.set( "limit", 4 ); + + Y.Assert.areEqual( 4, actions.length, "There must be 4 actions" ); + Y.Assert.areEqual( "Action0", actions[0].get( "label" ), "Label must be Action0" ); + Y.Assert.areEqual( "Action1", actions[1].get( "label" ), "Label must be Action1" ); + Y.Assert.areEqual( "Action2", actions[2].get( "label" ), "Label must be Action2" ); + Y.Assert.areEqual( "Action3", actions[3].get( "label" ), "Label must be Action3" ); + }, + + testSetLimit8: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo(3); + that.undoManager.set( "limit", 4 ); + + Y.Assert.areEqual( 4, actions.length, "There must be 4 actions" ); + Y.Assert.areEqual( "Action1", actions[0].get( "label" ), "Label must be Action1" ); + Y.Assert.areEqual( "Action2", actions[1].get( "label" ), "Label must be Action2" ); + Y.Assert.areEqual( "Action3", actions[2].get( "label" ), "Label must be Action3" ); + Y.Assert.areEqual( "Action4", actions[3].get( "label" ), "Label must be Action4" ); + }, + + testSetLimit9: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo(4); + that.undoManager.set( "limit", 4 ); + + Y.Assert.areEqual( 4, actions.length, "There must be 4 actions" ); + Y.Assert.areEqual( "Action1", actions[0].get( "label" ), "Label must be Action1" ); + Y.Assert.areEqual( "Action2", actions[1].get( "label" ), "Label must be Action2" ); + Y.Assert.areEqual( "Action3", actions[2].get( "label" ), "Label must be Action3" ); + Y.Assert.areEqual( "Action4", actions[3].get( "label" ), "Label must be Action4" ); + }, + + testSetLimit10: function(){ + var actions = that.undoManager._actions; + + this.prepareLimitTest(); + that.undoManager.processTo(3); + that.undoManager.set( "limit", 2 ); + + Y.Assert.areEqual( 2, actions.length, "There must be 2 actions" ); + Y.Assert.areEqual( "Action2", actions[0].get( "label" ), "Label must be Action2" ); + Y.Assert.areEqual( "Action3", actions[1].get( "label" ), "Label must be Action3" ); + } + }); + + + var testPurgeActions = new Y.Test.Case({ + testPurgeActions : function(){ + var i, undoableAction, maxActions = 10, targetIndex, actions; + + that.undoManager.purgeAll(); + that.undoManager.set( "limit", 0 ); + + for( i = 0; i < maxActions; i++ ){ + undoableAction = new TestUndoableAction({ + "label" : "Action" + i + }); + + that.undoManager.add(undoableAction); + } + + maxActions = parseInt(maxActions/2, 10); + targetIndex = parseInt( maxActions - 1, 10); + + that.undoManager.processTo(targetIndex); + that.undoManager.purgeTo( maxActions ); + + actions = that.undoManager._actions; + Y.Assert.areEqual( maxActions, actions.length, "There must be ", maxActions, " actions" ); + Y.Assert.areEqual( targetIndex, that.undoManager.get( "undoIndex" ), "Undo index must be " + targetIndex ); + + + that.undoManager.purgeTo( maxActions ); // must do nothing + Y.Assert.areEqual( maxActions, actions.length, "There must be ", maxActions, " actions" ); + Y.Assert.areEqual( targetIndex, that.undoManager.get( "undoIndex" ), "Undo index must be " + targetIndex ); + + maxActions -= 1; + that.undoManager.purgeTo( maxActions ); + Y.Assert.areEqual( maxActions, actions.length, "There must be ", maxActions, " actions" ); + Y.Assert.areEqual( targetIndex, that.undoManager.get( "undoIndex" ), "Undo index must be " + targetIndex ); + + + that.undoManager.undo(); + Y.Assert.areEqual( maxActions, actions.length, "There must be ", maxActions, " actions" ); + Y.Assert.areEqual( targetIndex - 1, that.undoManager.get( "undoIndex" ), "Undo index must be " + targetIndex - 1 ); + + + that.undoManager.redo(); + Y.Assert.areEqual( maxActions, actions.length, "There must be ", maxActions, " actions" ); + Y.Assert.areEqual( targetIndex, that.undoManager.get( "undoIndex" ), "Undo index must be " + targetIndex ); + } + }); + + Y.Test.Runner.add(testSynchronousActions); + Y.Test.Runner.add(testSynchronousActionsLimit); + Y.Test.Runner.add(testPurgeActions); + + console = new Y.Console({ + verbose : false, + printTimeout: 0, + newestOnTop : false, + + entryTemplate: '
'+
+                '{label}'+
+                '{message}'+
+        '
' + }).render(); + + Y.Test.Runner.run(); +}); \ No newline at end of file