immutable-undo
Version:
Data structure for and Undo/Redo stack, using ImmutableJS
245 lines (193 loc) • 8.02 kB
JavaScript
'use strict';
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var _require = require('immutable'),
Record = _require.Record,
Stack = _require.Stack,
List = _require.List;
var linear = require('./linear');
// Default maximum number of undo
var MAX_UNDOS = 500;
/*
type Snapshot<T> = {
value: T,
merged: number=1 // Number of snapshots virtually contained in this one,
// including itself. It increases each time a snapshot is merged // with this one.
}
*/
/**
* Default properties.
*/
var DEFAULTS = {
// The previous states. Last is the closest to current (most
// recent)
undos: new List(), // List<Snapshot>
// The next states. Top is the closest to current (oldest)
redos: new Stack(), // Stack<Snapshot>
// Remember the current merged count. For SMOOTH strategy
merged: 1,
maxUndos: MAX_UNDOS,
strategy: lru
};
/**
* Data structure for an History of state, with undo/redo.
*/
var History = function (_ref) {
_inherits(History, _ref);
function History() {
_classCallCheck(this, History);
return _possibleConstructorReturn(this, (History.__proto__ || Object.getPrototypeOf(History)).apply(this, arguments));
}
_createClass(History, [{
key: 'push',
/**
* Push a new state, and clear all the next states.
* @param {Any} state The new state
* @return {History}
*/
value: function push(state) {
var newHistory = this.merge({
undos: this.undos.push(snapshot(state, this.merged)),
redos: new Stack(),
merged: 1
});
return newHistory.prune();
}
/**
* Go back to previous state. Return itself if no previous state.
* @param {Any} current The current state
* @return {History}
*/
}, {
key: 'undo',
value: function undo(current) {
if (!this.canUndo) return this;
return this.merge({
undos: this.undos.pop(),
redos: this.redos.push(snapshot(current, this.merged)),
merged: this.undos.last().merged
});
}
/**
* Go to next state. Return itself if no next state
* @param {Any} current The current state
* @return {History}
*/
}, {
key: 'redo',
value: function redo(current) {
if (!this.canRedo) return this;
return this.merge({
undos: this.undos.push(snapshot(current, this.merged)),
redos: this.redos.pop(),
merged: this.redos.first().merged
});
}
/**
* Prune undo/redo using the defined strategy,
* after pushing a value on a valid History.
* @return {History}
*/
}, {
key: 'prune',
value: function prune() {
if (this.undos.size <= this.maxUndos) {
return this;
} else {
return this.strategy(this);
}
}
}, {
key: 'canUndo',
get: function get() {
return !this.undos.isEmpty();
}
}, {
key: 'canRedo',
get: function get() {
return !this.redos.isEmpty();
}
/**
* @return {Any?} the previous state
*/
}, {
key: 'previous',
get: function get() {
return this.undos.last().value;
}
/**
* @return {Any?} the next state
*/
}, {
key: 'next',
get: function get() {
return this.redos.first().value;
}
}], [{
key: 'create',
/**
* @param {Any} initial The initial state
* @return {History}
*/
value: function create() {
var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return new History(opts);
}
}]);
return History;
}(new Record(DEFAULTS));
// Strategies
History.lru = lru;
History.smooth = smooth;
function lru(history) {
return history.set('undos', history.undos.shift());
}
function smooth(history) {
var maxMerge = history.undos.first().merged;
var minMerge = history.undos.last().merged;
var size = history.undos.count();
var curve = linear({
x0: 0,
y0: minMerge
}, {
x1: size - 1,
y1: maxMerge
});
// Find the first undo that was not merged enough
// compared to the rest of the history.
var receivingIndex = history.undos.findIndex(function (_ref2, index) {
var merged = _ref2.merged;
return merged < curve(size - 1 - index);
});
if (receivingIndex === -1) {
// Fallback to the oldest possible
receivingIndex = 0; // but always keep the initial state
}
var mergedIndex = receivingIndex + 1;
var _history$undos$slice$ = history.undos.slice(receivingIndex, mergedIndex + 1).toArray(),
_history$undos$slice$2 = _slicedToArray(_history$undos$slice$, 2),
receivingSnapshot = _history$undos$slice$2[0],
mergedSnapshot // always defined since
// the comparison to curve(x) is strict
= _history$undos$slice$2[1];
return history.set('undos', history.undos.set(receivingIndex, incrementSnapshot(receivingSnapshot, mergedSnapshot.merged)).delete(mergedIndex));
}
/**
* Make a snapshot of a value.
*/
function snapshot(value) {
var merged = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
return { value: value, merged: merged };
}
/**
* Returns the given snapshot with increment merge count
*/
function incrementSnapshot(_ref3, amount) {
var value = _ref3.value,
merged = _ref3.merged;
return { value: value, merged: merged + amount };
}
module.exports = History;