/**
* 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 <code>Y.UndoableAction</code> has been added to list
*
* @event actionAdded
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt>action</dt>
* <dd>An <code>Y.UndoableAction</code> added to the list</dd>
* </dl>
*/
this.publish( ACTIONADDED );
/**
* Signals an <code>Y.UndoableAction</code> has been merged with another one
*
* @event actionMerged
* @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
* <dl>
* <dt><code>Y.UndoableAction</code> action</dt>
* <dd>The action, accepted merge</dd>
* <dt><code>Y.UndoableAction</code> mergedAction</dt>
* <dd>The merged action</dd>
* </dl>
*/
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:
* <dl>
* <dt>action</dt>
* <dd>An <code>Y.UndoableAction</code> canceled</dd>
* <dt>index</dt>
* <dd>The index of the action in the list</dd>
* </dl>
*/
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. <code>UndoManager</code> 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:
* <dl>
* <dt>action</dt>
* <dd>An <code>Y.UndoableAction</code> undone</dd>
* <dt>index</dt>
* <dd>The index of the action in the list</dd>
* </dl>
*/
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:
* <dl>
* <dt>action</dt>
* <dd>An <code>Y.UndoableAction</code> redone</dd>
* <dt>index</dt>
* <dd>The index of the action in the list</dd>
* </dl>
*/
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.<br>
* Removes and cancels all actions from the current action index till the end of the list.
* Tries to merge the current action with the <code>newAction</code>, passed as parameter. If <code>currentAction.merge(newAction)</code> returns false, UndoManager places the <code>newAction</code> at the end of the list.<br>
* Fires <code>actionAdded</code> event if action has been added to the list, or <code>actionMerged</code> if <code>newAction</code> 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, {
action : newAction
});
} else {
this.fire( ACTIONMERGED, {
'action' : curAction,
'mergedAction' : newAction
});
}
return true;
},
/**
* Removes actions from the list if their number exceedes the <code>limit</code>
*
* @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 <code>_limitActions</code> in order to keep the number of actions in the list according to the <code>limit</code>.
*
* @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 <code>undo</code> method.
* If <code>asyncProcessing</code> property of the action is true, UndoManager waits until action fires <code>undoFinished</code> 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 <code>redo</code> method.
* If <code>asyncProcessing</code> property of the action is true, UndoManager waits until action fires <code>redoFinished</code> 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 <code>label</code> 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 <code>label</code> 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 <code>newIndex</code> passed.
*
* @method processTo
* @param newIndex The new value of <code>undoIndex</code>
*/
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 <code>newIndex</code>. In case of asynchronous action, waits until action fires <code>redoFinished</code> event.
*
* @method _redoTo
* @protected
* @param newIndex The new value of <code>undoIndex</code>
*/
_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 <code>newIndex</code>. In case of asynchronous action, waits until action fires <code>undoFinished</code> event.
*
* @method _undoTo
* @protected
* @param newIndex The new value of <code>undoIndex</code>
*/
_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 <code>actionUndone</code> event. Checks if <code>newIndex</code> is less than current index. If true, invokes _undoTo again, or fires <code>undoFinished</code> 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 <code>undoIndex</code>
*/
_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': action
});
}
},
/**
* Handles the completion of redo method of asynchronous action.
* Fires <code>actionRedone</code> event. Checks if <code>newIndex</code> is bigger than current index. If true, invokes _redoTo again, or fires <code>redoFinished</code> 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 <code>undoIndex</code>
*/
_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': action
});
}
}
});
Y.UndoManager = UndoManager;
}());