Last active
August 29, 2015 14:04
-
-
Save drewbrokke/cc00050d8fa931ae4628 to your computer and use it in GitHub Desktop.
aui-undo-redo.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
YUI.add('aui-undo-redo', function (A, NAME) { | |
/** | |
* The Undo/Redo Component | |
* | |
* @module aui-undo-redo | |
*/ | |
/** | |
* Fired after a redo has finished running. | |
* | |
* @event afterRedo | |
*/ | |
/** | |
* Fired after an undo has finished running. | |
* | |
* @event afterUndo | |
*/ | |
/** | |
* Fired right before a redo is run. | |
* | |
* @event beforeRedo | |
* @preventable _defBeforeActionFn | |
*/ | |
/** | |
* Fired right before an undo is run. | |
* | |
* @event beforeUndo | |
* @preventable _defBeforeActionFn | |
*/ | |
A.UndoRedo = A.Base.create('undo-redo', A.Base, [], { | |
ACTION_TYPE_REDO: 'redo', | |
ACTION_TYPE_UNDO: 'undo', | |
AFTER_REDO: 'afterRedo', | |
AFTER_UNDO: 'afterUndo', | |
BEFORE_REDO: 'beforeRedo', | |
BEFORE_UNDO: 'beforeUndo', | |
EVENT_PREFIX_AFTER: 'after', | |
EVENT_PREFIX_BEFORE: 'before', | |
/** | |
* This index points to the last state that was executed. Calling undo() | |
* will undo the state this index points to. | |
* | |
* @property _currentStateIndex | |
* @type {Number} | |
* @protected | |
*/ | |
_currentStateIndex: -1, | |
/** | |
* List of pending actions. | |
* | |
* @property _pendingActions | |
* @type {Array} | |
* @protected | |
*/ | |
_pendingActions: null, | |
/** | |
* List of states containing `undo` and `redo` action methods added by the | |
* user through the `add` method. | |
* | |
* @property _states | |
* @type {Array} | |
* @protected | |
*/ | |
_states: null, | |
/** | |
* Constructor for the Undo/Redo component. | |
* | |
* @method initializer | |
* @protected | |
*/ | |
initializer: function() { | |
this.clearHistory(); | |
this.publish({ | |
afterRedo: {}, | |
afterUndo: {}, | |
beforeRedo: { | |
defaultFn: this._defBeforeActionFn, | |
preventedFn: this._prevBeforeActionFn | |
}, | |
beforeUndo: { | |
defaultFn: this._defBeforeActionFn, | |
preventedFn: this._prevBeforeActionFn | |
} | |
}); | |
this.after('maxUndoDepthChange', this._removeStatesBeyondMaxDepth); | |
}, | |
/** | |
* Adds a state to the stack and makes it the current state. Note that all | |
* states that could be redone will be removed from the stack after this. | |
* Valid states are objects that have at least 2 functions: undo and redo. | |
* These functions can return promises, in which case any subsequent calls | |
* will be queued, waiting for all pending promises to end. | |
* | |
* @method add | |
* @param {Object | Function} state Object that contains `undo` and `redo` | |
* action methods (and, optionally, a 'merge' method). | |
*/ | |
add: function(state) { | |
if (!state.undo || !state.redo) { | |
throw new Error('Invalid state. States used in UndoRedo need to ' + | |
'have both the \'undo\' and the \'redo\' functions defined'); | |
} | |
if (this._currentStateIndex < this._states.length - 1) { | |
// First remove all states after the current one, since | |
// those can't be redone anymore now that a new state was added. | |
this._states = this._states.slice(0, this._currentStateIndex + 1); | |
} | |
if (this._tryMerge(state)) { | |
// We shouldn't add the state again if it was already merged. | |
return; | |
} | |
this._states.push(state); | |
this._currentStateIndex++; | |
this._removeStatesBeyondMaxDepth(); | |
}, | |
bindShortcuts: function(node) { | |
A.one(node).on('keypress', this._handleKeypress, this); | |
}, | |
/** | |
* Checks if it's possible to redo an action. | |
* | |
* @method canRedo | |
* @return {Boolean} | |
*/ | |
canRedo: function() { | |
return this._currentStateIndex < this._states.length - 1 && !this._shouldIgnoreNewActions(); | |
}, | |
/** | |
* Checks if it's possible to undo an action. | |
* | |
* @method canUndo | |
* @return {Boolean} | |
*/ | |
canUndo: function() { | |
return this._currentStateIndex >= 0 && !this._shouldIgnoreNewActions(); | |
}, | |
/** | |
* Resets the stack, clearing all states and pending actions. | |
* | |
* @method clearHistory | |
*/ | |
clearHistory: function() { | |
this._states = []; | |
this._pendingActions = []; | |
this._currentStateIndex = -1; | |
}, | |
/** | |
* Checks if either an undo or a redo action is currently in progress. | |
* | |
* @method isActionInProgress | |
* @return {Boolean} | |
*/ | |
isActionInProgress: function() { | |
return this._pendingActions.length > 0; | |
}, | |
/** | |
* Redoes the next state. | |
* | |
* @method redo | |
* @return {Boolean} Returns false if there was no state to be redone and | |
* true, otherwise. | |
*/ | |
redo: function() { | |
if (!this.canRedo()) { | |
return false; | |
} | |
this._currentStateIndex++; | |
this._runAction({ | |
state: this._states[this._currentStateIndex], | |
type: this.ACTION_TYPE_REDO, | |
undoIndex: this._currentStateIndex - 1 | |
}); | |
return true; | |
}, | |
/** | |
* Returns the state that will be redone when calling redo(). | |
* | |
* @method redoPeek | |
* @return {Boolean} | |
*/ | |
redoPeek: function() { | |
return this._states[this._currentStateIndex + 1]; | |
}, | |
/** | |
* Undoes the last state. | |
* | |
* @method undo | |
* @return {Boolean} Returns false if there was no state to be undone and | |
* true, otherwise. | |
*/ | |
undo: function() { | |
if (!this.canUndo()) { | |
return false; | |
} | |
this._runAction({ | |
state: this._states[this._currentStateIndex], | |
type: this.ACTION_TYPE_UNDO, | |
undoIndex: this._currentStateIndex | |
}); | |
this._currentStateIndex--; | |
return true; | |
}, | |
/** | |
* Returns the state that will be undone when calling undo(). | |
* | |
* @method undoPeek | |
* @return {Boolean} | |
*/ | |
undoPeek: function() { | |
return this._states[this._currentStateIndex]; | |
}, | |
/** | |
* Executes right after an action finishes running. | |
* | |
* @method _afterAction | |
* @param {Object} action Object containing the `state` and action `type` | |
* (undo or redo) to be executed. | |
* @protected | |
*/ | |
_afterAction: function(action) { | |
this.fire(this._makeEventName(this.EVENT_PREFIX_AFTER, action.type)); | |
this._pendingActions.shift(); | |
this._removeStatesBeyondMaxDepth(); | |
this._runNextPendingAction(); | |
}, | |
/** | |
* This is the default function for the beforeUndo and beforeRedo events. | |
* | |
* @method _defBeforeActionFn | |
* @param {EventFacade} event | |
* @protected | |
*/ | |
_defBeforeActionFn: function() { | |
var action = this._pendingActions[0], | |
result = action.state[action.type](); | |
if (A.Promise.isPromise(result)) { | |
result.then(A.bind(this._afterAction, this, action)); | |
} | |
else { | |
this._afterAction(action); | |
} | |
}, | |
_handleKeypress: function(event) { | |
var keyCode = event.keyCode; | |
if (event.ctrlKey) { | |
if (keyCode === 26) { | |
this.undo(); | |
} | |
else if (keyCode === 9) { | |
this.redo(); | |
} | |
} | |
}, | |
/** | |
* Constructs the event's name based on its prefix and the action type | |
* related to it. | |
* | |
* @method _makeEventName | |
* @return {String} Returns the camel case version of `prefix` plus | |
* `actionType`. | |
* @protected | |
*/ | |
_makeEventName: function(prefix, actionType) { | |
return prefix + actionType.substring(0, 1).toUpperCase() + actionType.substring(1); | |
}, | |
/** | |
* This function runs when a beforeUndo or beforeRedo event is prevented. | |
* | |
* @method _prevBeforeActionFn | |
* @param {EventFacade} event | |
* @protected | |
*/ | |
_prevBeforeActionFn: function() { | |
var action = this._pendingActions[0]; | |
this._currentStateIndex = action.undoIndex; | |
this._pendingActions = []; | |
this._removeStatesBeyondMaxDepth(); | |
}, | |
/** | |
* Removes states in the stack if it's over the max depth limit. | |
* | |
* @method _removeStatesBeyondMaxDepth | |
* @protected | |
*/ | |
_removeStatesBeyondMaxDepth: function() { | |
var extraCount = this._currentStateIndex + 1 - this.get('maxUndoDepth'); | |
// We should ignore this call if there are pending actions, as we may need | |
// to roll back to a state due to the user preventing one of them. | |
if (extraCount > 0 && !this._pendingActions.length) { | |
this._states = this._states.slice(extraCount); | |
this._currentStateIndex -= extraCount; | |
} | |
}, | |
/** | |
* Executes the given action (which can be either undo or redo). | |
* | |
* @method _runAction | |
* @param {Object} action Object containing the `state` and action `type` | |
* (undo or redo) to be executed. | |
* @protected | |
*/ | |
_runAction: function(action) { | |
this._pendingActions.push(action); | |
if (this._pendingActions.length === 1) { | |
this._runNextPendingAction(); | |
} | |
}, | |
/** | |
* Executes the next pending action, if one exists. | |
* | |
* @method _runNextPendingAction | |
* @protected | |
*/ | |
_runNextPendingAction: function() { | |
var action = this._pendingActions[0]; | |
if (!action) { | |
return; | |
} | |
this.fire(this._makeEventName(this.EVENT_PREFIX_BEFORE, action.type)); | |
}, | |
/** | |
* Checks if new actions (calls to undo/redo) should be ignored. Actions | |
* should only be ignored if the `queueable` attribute is false and there | |
* is currently an action in progress. | |
* | |
* @method _shouldIgnoreNewActions | |
* @return {Boolean} | |
* @protected | |
*/ | |
_shouldIgnoreNewActions: function() { | |
return !this.get('queueable') && this.isActionInProgress(); | |
}, | |
/** | |
* Tries to merge the given state to one at the current position in the | |
* stack. | |
* | |
* @method _tryMerge | |
* @return {Boolean} Returns true if the merge happened and false otherwise. | |
* @protected | |
*/ | |
_tryMerge: function(state) { | |
if (this._currentStateIndex >= 0 && A.Lang.isFunction(state.merge)) { | |
return state.merge(this.undoPeek()); | |
} | |
return false; | |
} | |
}, { | |
ATTRS: { | |
/** | |
* Limits the states stack size. Useful for memory optimization. | |
* | |
* @attribute maxUndoDepth | |
* @default 100 | |
* @type {Number} | |
*/ | |
maxUndoDepth: { | |
validator: function(depth) { | |
return depth >= 1; | |
}, | |
value: 100 | |
}, | |
/** | |
* Defines how this module will behave when the user calls undo | |
* or redo while an action is still in progress. If false, these | |
* calls will be ignored. If true, they will be queued, running | |
* in order as soon as the pending action finishes. | |
* | |
* @attribute queueable | |
* @default false | |
* @type {Boolean} | |
*/ | |
queueable: { | |
validator: A.Lang.isBoolean, | |
value: false | |
} | |
} | |
}); | |
}, '3.0.0pr2', {"requires": ["base", "base-build", "promise"]}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title></title> | |
<link rel="stylesheet" href="../../build/aui-css/css/bootstrap.css"> | |
<script src="../../build/aui/aui.js"></script> | |
<style> | |
body { | |
margin-left: 10px; | |
} | |
#buttons-container { | |
margin-bottom: 10px; | |
} | |
#redbox-container { | |
border: 1px solid black; | |
height: 300px; | |
width: 500px; | |
} | |
#redbox { | |
background-color: red; | |
height: 100px; | |
width: 100px; | |
} | |
.hidden { | |
display: none; | |
} | |
.async-options { | |
margin-bottom: 10px; | |
} | |
</style> | |
</head> | |
<body class="yui3-skin-sam"> | |
<h1>AlloyUI - UndoRedo</h1> | |
<div id="buttons-container"> | |
<div class="async-options"> | |
<span class="btn btn-primary sync-button">Run Async Example</span> | |
<span class="queueable" style="display: none"><input type="checkbox"> Queueable</span> | |
</div> | |
<span class="btn btn-default undo-button">Undo</span> | |
<span class="btn btn-default redo-button">Redo</span> | |
<span class="loading-indicator hidden">Running...</span> | |
</div> | |
<div id="redbox-container"> | |
<div id="redbox"></div> | |
</div> | |
<script> | |
YUI({filter: 'raw'}).use('aui-undo-redo', 'dd-drag', 'dd-constrain', function(Y) { | |
var box = Y.one('#redbox'), | |
dd = new Y.DD.Drag({node: '#redbox'}), | |
undoRedo = new Y.UndoRedo(), | |
previousXY = box.getXY(), | |
async = false; | |
dd.plug(Y.Plugin.DDConstrained, {constrain2node: '#redbox-container'}); | |
dd.on('drag:end', function(event) { | |
var undoXY = previousXY, | |
redoXY = box.getXY(); | |
undoRedo.add({ | |
undo: function() { | |
return async ? boxSetXYPromise(undoXY) : boxSetXY(undoXY); | |
}, | |
redo: function() { | |
return async ? boxSetXYPromise(redoXY) : boxSetXY(redoXY); | |
} | |
}); | |
previousXY = redoXY; | |
}); | |
Y.one('.undo-button').on('click', function() { | |
undoRedo.undo(); | |
}); | |
Y.one('.redo-button').on('click', function() { | |
undoRedo.redo(); | |
}); | |
Y.one('.sync-button').on('click', function() { | |
if (async) { | |
this.set('text', 'Run Async Example'); | |
} else { | |
this.set('text', 'Run Sync Example'); | |
} | |
async = !async; | |
Y.one('.queueable').toggleView(); | |
}); | |
Y.one('input[type=checkbox]').on('change', function() { | |
undoRedo.set('queueable', !undoRedo.get('queueable')); | |
}); | |
var x = Y.one('body'); | |
undoRedo.bindShortcuts(x); | |
function boxSetXY(xy) { | |
box.setXY(xy); | |
previousXY = xy; | |
} | |
function boxSetXYPromise(xy) { | |
return new Y.Promise(function(resolve) { | |
Y.one('.loading-indicator').removeClass('hidden'); | |
setTimeout(function() { | |
boxSetXY(xy); | |
resolve(); | |
Y.one('.loading-indicator').addClass('hidden'); | |
}, 1000); | |
}); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment