code: function toEditable(observable, getRollbackValue) {
  	var rollbackValues = [],
  		cancelledValue;
  
  	getRollbackValue = getRollbackValue || function (observable) { return observable(); };
  
  	function canEdit() {
  		return !observable.isEditing() && (!observable.isEditable || ko.unwrap(observable.isEditable()));
  	}
  
  	//a flag to indicate if the field is being edited
  	observable.isEditing = ko.observable(false);
  
  	//start an edit
  	observable.beginEdit = function () {
  		if (!canEdit()) { return; }
  		cancelledValue = undefined;
  
  		rollbackValues.push(getRollbackValue(observable));
  
  		observable.isEditing(true);
  	};
  
  	//end (commit) an edit
  	observable.endEdit = function () {
  		if (!observable.isEditing()) { return; }
  
  		observable.isEditing(false);
  	};
  
  	//cancel and roll-back an edit
  	observable.cancelEdit = function () {
  		if (!observable.isEditing() || !rollbackValues.length) { return; }
  
  		cancelledValue = getRollbackValue(observable);
  
  		observable(rollbackValues.pop());
  
  		observable.isEditing(false);
  	};
  
  	//undo a set of cancelled changes
  	observable.undoCancel = function () {
  		if (cancelledValue === undefined) { return; }
  
  		var value = cancelledValue; //grab value here as beginEdit will clear it
  		observable.beginEdit();
  		observable(value);
  		cancelledValue = undefined;
  	};
  
  	//roll-back to historical committed values
  	observable.rollback = function () {
  		if (rollbackValues.length) {
  			cancelledValue = undefined;
  			observable(rollbackValues.pop());
  		}
  	};
  
  	observable.isDirty = ko.computed(function () {
  		return observable.isEditing() &&
  			JSON.stringify(rollbackValues[rollbackValues.length-1]) !== JSON.stringify(ko.unwrap(observable));
  	}).extend({ throttle: 1});
  
  	return observable;
  }
docs: /*global ko:false