Undoable - Observable Array

An example of using the Undoable extension with an observable array.

by Areson

HTML

<script src="https://raw.github.com/SteveSanderson/knockout/master/build/output/knockout-latest.debug.js"></script>
<script src="https://raw.github.com/SteveSanderson/knockout.mapping/master/build/output/knockout.mapping-latest.debug.js"></script>
<label>Value: <input id="valueBox" type="text" data-bind="value: observe().value"/></label>
<button id="addButton">Add Value</button>
<button data-bind="click: function(){observe.undo()}">Undo</button>

<hr/>
<h3>Array Data</h3>
<table data-bind="foreach: observe">
    <tr><td data-bind="text: $data"></td></tr>
</table>

<hr/>
<h3>Available Snapshots</h3>
<table data-bind="foreach: observe.undoStack()">
    <tr data-bind="click: $parent.observe.undo, style: {color: isCurrentUndoLevel()?'green':'grey'}">
        <td data-bind="text: name"></td>
    </tr>
</table>

JavaScript

ko.extenders.undoable= function(target, options) {
    var _trackChanges = ko.observable(true);
    var _stackSize = options.stackSize || 0;
    var _undoData = {};
    _undoData.undoStack = ko.observableArray([]);
    
    //Setup our stack items
    var StackItem = function(index, data) {
        var item = {
            data: data,
            index: index
        };
        
        item.isCurrentUndoLevel = ko.computed(function() {
            var undoStack = _undoData.undoStack();
            var stackLength = undoStack.length;
            var firstIndex = (stackLength > 0)?undoStack[0].index:0;
            var temp = (stackLength > 0 && this.index - firstIndex == undoStack[stackLength - 1].index);
            return(stackLength > 0 && this.index - firstIndex == undoStack[stackLength - 1].index);
        }, item);
         
        return(item);                
    }
    
    _undoData.isDirty = ko.computed(function() {
        if(_trackChanges())
        {
            var undoStack = _undoData.undoStack;
            var stackLength = undoStack().length;
            var firstIndex = (stackLength > 0)?undoStack()[0].index:0;
            
            undoStack.push(new StackItem(stackLength + firstIndex, ko.toJSON(target)));
            
            if(_stackSize > 0 && stackLength >= _stackSize)
                undoStack.splice(0, stackLength - _stackSize + 1);
        }
    });
    
    //Setup a dependent observable to track
    //the list of snapshots
    target.undoStack = ko.computed({
        read: function() {
            return _undoData.undoStack();
        },
        write: function() {}
    });
    
    //This is the function that controls how the 
    //snapshots get mapped back to the observable.
    //The data that is passed is JSON.
    var _undoFunction = function(snapshot, target) {
        if($.isPlainObject(target()))
            ko.mapping.fromJSON(snapshot.data, null, target);
        else
            target($.parseJSON(snapshot.data));
...