You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

457 lines
17 KiB

3 years ago
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