UNPKG

immutable-undo

Version:

Data structure for and Undo/Redo stack, using ImmutableJS

245 lines (193 loc) 8.02 kB
'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;