import RopeSequence from 'rope-sequence'; import { Mapping } from 'prosemirror-transform'; import { PluginKey, Plugin } from 'prosemirror-state'; // ProseMirror's history isn't simply a way to roll back to a previous // state, because ProseMirror supports applying changes without adding // them to the history (for example during collaboration). // // To this end, each 'Branch' (one for the undo history and one for // the redo history) keeps an array of 'Items', which can optionally // hold a step (an actual undoable change), and always hold a position // map (which is needed to move changes below them to apply to the // current document). // // An item that has both a step and a selection bookmark is the start // of an 'event' — a group of changes that will be undone or redone at // once. (It stores only the bookmark, since that way we don't have to // provide a document until the selection is actually applied, which // is useful when compressing.) // Used to schedule history compression var max_empty_items = 500; var Branch = function Branch(items, eventCount) { this.items = items; this.eventCount = eventCount; }; // : (EditorState, bool) → ?{transform: Transform, selection: ?SelectionBookmark, remaining: Branch} // Pop the latest event off the branch's history and apply it // to a document transform. Branch.prototype.popEvent = function popEvent (state, preserveItems) { var this$1$1 = this; if (this.eventCount == 0) { return null } var end = this.items.length; for (;; end--) { var next = this.items.get(end - 1); if (next.selection) { --end; break } } var remap, mapFrom; if (preserveItems) { remap = this.remapping(end, this.items.length); mapFrom = remap.maps.length; } var transform = state.tr; var selection, remaining; var addAfter = [], addBefore = []; this.items.forEach(function (item, i) { if (!item.step) { if (!remap) { remap = this$1$1.remapping(end, i + 1); mapFrom = remap.maps.length; } mapFrom--; addBefore.push(item); return } if (remap) { addBefore.push(new Item(item.map)); var step = item.step.map(remap.slice(mapFrom)), map; if (step && transform.maybeStep(step).doc) { map = transform.mapping.maps[transform.mapping.maps.length - 1]; addAfter.push(new Item(map, null, null, addAfter.length + addBefore.length)); } mapFrom--; if (map) { remap.appendMap(map, mapFrom); } } else { transform.maybeStep(item.step); } if (item.selection) { selection = remap ? item.selection.map(remap.slice(mapFrom)) : item.selection; remaining = new Branch(this$1$1.items.slice(0, end).append(addBefore.reverse().concat(addAfter)), this$1$1.eventCount - 1); return false } }, this.items.length, 0); return {remaining: remaining, transform: transform, selection: selection} }; // : (Transform, ?SelectionBookmark, Object) → Branch // Create a new branch with the given transform added. Branch.prototype.addTransform = function addTransform (transform, selection, histOptions, preserveItems) { var newItems = [], eventCount = this.eventCount; var oldItems = this.items, lastItem = !preserveItems && oldItems.length ? oldItems.get(oldItems.length - 1) : null; for (var i = 0; i < transform.steps.length; i++) { var step = transform.steps[i].invert(transform.docs[i]); var item = new Item(transform.mapping.maps[i], step, selection), merged = (void 0); if (merged = lastItem && lastItem.merge(item)) { item = merged; if (i) { newItems.pop(); } else { oldItems = oldItems.slice(0, oldItems.length - 1); } } newItems.push(item); if (selection) { eventCount++; selection = null; } if (!preserveItems) { lastItem = item; } } var overflow = eventCount - histOptions.depth; if (overflow > DEPTH_OVERFLOW) { oldItems = cutOffEvents(oldItems, overflow); eventCount -= overflow; } return new Branch(oldItems.append(newItems), eventCount) }; Branch.prototype.remapping = function remapping (from, to) { var maps = new Mapping; this.items.forEach(function (item, i) { var mirrorPos = item.mirrorOffset != null && i - item.mirrorOffset >= from ? maps.maps.length - item.mirrorOffset : null; maps.appendMap(item.map, mirrorPos); }, from, to); return maps }; Branch.prototype.addMaps = function addMaps (array) { if (this.eventCount == 0) { return this } return new Branch(this.items.append(array.map(function (map) { return new Item(map); })), this.eventCount) }; // : (Transform, number) // When the collab module receives remote changes, the history has // to know about those, so that it can adjust the steps that were // rebased on top of the remote changes, and include the position // maps for the remote changes in its array of items. Branch.prototype.rebased = function rebased (rebasedTransform, rebasedCount) { if (!this.eventCount) { return this } var rebasedItems = [], start = Math.max(0, this.items.length - rebasedCount); var mapping = rebasedTransform.mapping; var newUntil = rebasedTransform.steps.length; var eventCount = this.eventCount; this.items.forEach(function (item) { if (item.selection) { eventCount--; } }, start); var iRebased = rebasedCount; this.items.forEach(function (item) { var pos = mapping.getMirror(--iRebased); if (pos == null) { return } newUntil = Math.min(newUntil, pos); var map = mapping.maps[pos]; if (item.step) { var step = rebasedTransform.steps[pos].invert(rebasedTransform.docs[pos]); var selection = item.selection && item.selection.map(mapping.slice(iRebased + 1, pos)); if (selection) { eventCount++; } rebasedItems.push(new Item(map, step, selection)); } else { rebasedItems.push(new Item(map)); } }, start); var newMaps = []; for (var i = rebasedCount; i < newUntil; i++) { newMaps.push(new Item(mapping.maps[i])); } var items = this.items.slice(0, start).append(newMaps).append(rebasedItems); var branch = new Branch(items, eventCount); if (branch.emptyItemCount() > max_empty_items) { branch = branch.compress(this.items.length - rebasedItems.length); } return branch }; Branch.prototype.emptyItemCount = function emptyItemCount () { var count = 0; this.items.forEach(function (item) { if (!item.step) { count++; } }); return count }; // Compressing a branch means rewriting it to push the air (map-only // items) out. During collaboration, these naturally accumulate // because each remote change adds one. The `upto` argument is used // to ensure that only the items below a given level are compressed, // because `rebased` relies on a clean, untouched set of items in // order to associate old items with rebased steps. Branch.prototype.compress = function compress (upto) { if ( upto === void 0 ) upto = this.items.length; var remap = this.remapping(0, upto), mapFrom = remap.maps.length; var items = [], events = 0; this.items.forEach(function (item, i) { if (i >= upto) { items.push(item); if (item.selection) { events++; } } else if (item.step) { var step = item.step.map(remap.slice(mapFrom)), map = step && step.getMap(); mapFrom--; if (map) { remap.appendMap(map, mapFrom); } if (step) { var selection = item.selection && item.selection.map(remap.slice(mapFrom)); if (selection) { events++; } var newItem = new Item(map.invert(), step, selection), merged, last = items.length - 1; if (merged = items.length && items[last].merge(newItem)) { items[last] = merged; } else { items.push(newItem); } } } else if (item.map) { mapFrom--; } }, this.items.length, 0); return new Branch(RopeSequence.from(items.reverse()), events) }; Branch.empty = new Branch(RopeSequence.empty, 0); function cutOffEvents(items, n) { var cutPoint; items.forEach(function (item, i) { if (item.selection && (n-- == 0)) { cutPoint = i; return false } }); return items.slice(cutPoint) } var Item = function Item(map, step, selection, mirrorOffset) { // The (forward) step map for this item. this.map = map; // The inverted step this.step = step; // If this is non-null, this item is the start of a group, and // this selection is the starting selection for the group (the one // that was active before the first step was applied) this.selection = selection; // If this item is the inverse of a previous mapping on the stack, // this points at the inverse's offset this.mirrorOffset = mirrorOffset; }; Item.prototype.merge = function merge (other) { if (this.step && other.step && !other.selection) { var step = other.step.merge(this.step); if (step) { return new Item(step.getMap().invert(), step, this.selection) } } }; // The value of the state field that tracks undo/redo history for that // state. Will be stored in the plugin state when the history plugin // is active. var HistoryState = function HistoryState(done, undone, prevRanges, prevTime) { this.done = done; this.undone = undone; this.prevRanges = prevRanges; this.prevTime = prevTime; }; var DEPTH_OVERFLOW = 20; // : (HistoryState, EditorState, Transaction, Object) // Record a transformation in undo history. function applyTransaction(history, state, tr, options) { var historyTr = tr.getMeta(historyKey), rebased; if (historyTr) { return historyTr.historyState } if (tr.getMeta(closeHistoryKey)) { history = new HistoryState(history.done, history.undone, null, 0); } var appended = tr.getMeta("appendedTransaction"); if (tr.steps.length == 0) { return history } else if (appended && appended.getMeta(historyKey)) { if (appended.getMeta(historyKey).redo) { return new HistoryState(history.done.addTransform(tr, null, options, mustPreserveItems(state)), history.undone, rangesFor(tr.mapping.maps[tr.steps.length - 1]), history.prevTime) } else { return new HistoryState(history.done, history.undone.addTransform(tr, null, options, mustPreserveItems(state)), null, history.prevTime) } } else if (tr.getMeta("addToHistory") !== false && !(appended && appended.getMeta("addToHistory") === false)) { // Group transforms that occur in quick succession into one event. var newGroup = history.prevTime == 0 || !appended && (history.prevTime < (tr.time || 0) - options.newGroupDelay || !isAdjacentTo(tr, history.prevRanges)); var prevRanges = appended ? mapRanges(history.prevRanges, tr.mapping) : rangesFor(tr.mapping.maps[tr.steps.length - 1]); return new HistoryState(history.done.addTransform(tr, newGroup ? state.selection.getBookmark() : null, options, mustPreserveItems(state)), Branch.empty, prevRanges, tr.time) } else if (rebased = tr.getMeta("rebased")) { // Used by the collab module to tell the history that some of its // content has been rebased. return new HistoryState(history.done.rebased(tr, rebased), history.undone.rebased(tr, rebased), mapRanges(history.prevRanges, tr.mapping), history.prevTime) } else { return new HistoryState(history.done.addMaps(tr.mapping.maps), history.undone.addMaps(tr.mapping.maps), mapRanges(history.prevRanges, tr.mapping), history.prevTime) } } function isAdjacentTo(transform, prevRanges) { if (!prevRanges) { return false } if (!transform.docChanged) { return true } var adjacent = false; transform.mapping.maps[0].forEach(function (start, end) { for (var i = 0; i < prevRanges.length; i += 2) { if (start <= prevRanges[i + 1] && end >= prevRanges[i]) { adjacent = true; } } }); return adjacent } function rangesFor(map) { var result = []; map.forEach(function (_from, _to, from, to) { return result.push(from, to); }); return result } function mapRanges(ranges, mapping) { if (!ranges) { return null } var result = []; for (var i = 0; i < ranges.length; i += 2) { var from = mapping.map(ranges[i], 1), to = mapping.map(ranges[i + 1], -1); if (from <= to) { result.push(from, to); } } return result } // : (HistoryState, EditorState, (tr: Transaction), bool) // Apply the latest event from one branch to the document and shift the event // onto the other branch. function histTransaction(history, state, dispatch, redo) { var preserveItems = mustPreserveItems(state), histOptions = historyKey.get(state).spec.config; var pop = (redo ? history.undone : history.done).popEvent(state, preserveItems); if (!pop) { return } var selection = pop.selection.resolve(pop.transform.doc); var added = (redo ? history.done : history.undone).addTransform(pop.transform, state.selection.getBookmark(), histOptions, preserveItems); var newHist = new HistoryState(redo ? added : pop.remaining, redo ? pop.remaining : added, null, 0); dispatch(pop.transform.setSelection(selection).setMeta(historyKey, {redo: redo, historyState: newHist}).scrollIntoView()); } var cachedPreserveItems = false, cachedPreserveItemsPlugins = null; // Check whether any plugin in the given state has a // `historyPreserveItems` property in its spec, in which case we must // preserve steps exactly as they came in, so that they can be // rebased. function mustPreserveItems(state) { var plugins = state.plugins; if (cachedPreserveItemsPlugins != plugins) { cachedPreserveItems = false; cachedPreserveItemsPlugins = plugins; for (var i = 0; i < plugins.length; i++) { if (plugins[i].spec.historyPreserveItems) { cachedPreserveItems = true; break } } } return cachedPreserveItems } // :: (Transaction) → Transaction // Set a flag on the given transaction that will prevent further steps // from being appended to an existing history event (so that they // require a separate undo command to undo). function closeHistory(tr) { return tr.setMeta(closeHistoryKey, true) } var historyKey = new PluginKey("history"); var closeHistoryKey = new PluginKey("closeHistory"); // :: (?Object) → Plugin // Returns a plugin that enables the undo history for an editor. The // plugin will track undo and redo stacks, which can be used with the // [`undo`](#history.undo) and [`redo`](#history.redo) commands. // // You can set an `"addToHistory"` [metadata // property](#state.Transaction.setMeta) of `false` on a transaction // to prevent it from being rolled back by undo. // // config::- // Supports the following configuration options: // // depth:: ?number // The amount of history events that are collected before the // oldest events are discarded. Defaults to 100. // // newGroupDelay:: ?number // The delay between changes after which a new group should be // started. Defaults to 500 (milliseconds). Note that when changes // aren't adjacent, a new group is always started. function history(config) { config = {depth: config && config.depth || 100, newGroupDelay: config && config.newGroupDelay || 500}; return new Plugin({ key: historyKey, state: { init: function init() { return new HistoryState(Branch.empty, Branch.empty, null, 0) }, apply: function apply(tr, hist, state) { return applyTransaction(hist, state, tr, config) } }, config: config, props: { handleDOMEvents: { beforeinput: function beforeinput(view, e) { var command = e.inputType == "historyUndo" ? undo : e.inputType == "historyRedo" ? redo : null; if (!command) { return false } e.preventDefault(); return command(view.state, view.dispatch) } } } }) } // :: (EditorState, ?(tr: Transaction)) → bool // A command function that undoes the last change, if any. function undo(state, dispatch) { var hist = historyKey.getState(state); if (!hist || hist.done.eventCount == 0) { return false } if (dispatch) { histTransaction(hist, state, dispatch, false); } return true } // :: (EditorState, ?(tr: Transaction)) → bool // A command function that redoes the last undone change, if any. function redo(state, dispatch) { var hist = historyKey.getState(state); if (!hist || hist.undone.eventCount == 0) { return false } if (dispatch) { histTransaction(hist, state, dispatch, true); } return true } // :: (EditorState) → number // The amount of undoable events available in a given state. function undoDepth(state) { var hist = historyKey.getState(state); return hist ? hist.done.eventCount : 0 } // :: (EditorState) → number // The amount of redoable events available in a given editor state. function redoDepth(state) { var hist = historyKey.getState(state); return hist ? hist.undone.eventCount : 0 } export { HistoryState, closeHistory, history, redo, redoDepth, undo, undoDepth }; //# sourceMappingURL=index.es.js.map