slate-history
Version:
An operation-based history implementation for Slate editors.
598 lines (587 loc) • 18.1 kB
JavaScript
import { isObject, Operation, Editor, Range, Path, Point, Transforms } from 'slate';
// eslint-disable-next-line no-redeclare
const History = {
/**
* Check if a value is a `History` object.
*/
isHistory(value) {
return isObject(value) && Array.isArray(value.redos) && Array.isArray(value.undos) && (value.redos.length === 0 || Operation.isOperationList(value.redos[0].operations)) && (value.undos.length === 0 || Operation.isOperationList(value.undos[0].operations));
}
};
/**
* Weakmaps for attaching state to the editor.
*/
const HISTORY = new WeakMap();
const SAVING = new WeakMap();
const MERGING = new WeakMap();
const SPLITTING_ONCE = new WeakMap();
// eslint-disable-next-line no-redeclare
const HistoryEditor = {
/**
* Check if a value is a `HistoryEditor` object.
*/
isHistoryEditor(value) {
return History.isHistory(value.history) && Editor.isEditor(value);
},
/**
* Get the merge flag's current value.
*/
isMerging(editor) {
return MERGING.get(editor);
},
/**
* Get the splitting once flag's current value.
*/
isSplittingOnce(editor) {
return SPLITTING_ONCE.get(editor);
},
setSplittingOnce(editor, value) {
SPLITTING_ONCE.set(editor, value);
},
/**
* Get the saving flag's current value.
*/
isSaving(editor) {
return SAVING.get(editor);
},
/**
* Redo to the previous saved state.
*/
redo(editor) {
editor.redo();
},
/**
* Undo to the previous saved state.
*/
undo(editor) {
editor.undo();
},
/**
* Apply a series of changes inside a synchronous `fn`, These operations will
* be merged into the previous history.
*/
withMerging(editor, fn) {
const prev = HistoryEditor.isMerging(editor);
MERGING.set(editor, true);
try {
fn();
} finally {
MERGING.set(editor, prev);
}
},
/**
* Apply a series of changes inside a synchronous `fn`, ensuring that the first
* operation starts a new batch in the history. Subsequent operations will be
* merged as usual.
*/
withNewBatch(editor, fn) {
const prevMerging = HistoryEditor.isMerging(editor);
const prevSplitting = HistoryEditor.isSplittingOnce(editor);
MERGING.set(editor, true);
SPLITTING_ONCE.set(editor, true);
try {
fn();
} finally {
MERGING.set(editor, prevMerging);
if (!prevSplitting) {
SPLITTING_ONCE.delete(editor);
}
}
},
/**
* Apply a series of changes inside a synchronous `fn`, without merging any of
* the new operations into previous save point in the history.
*/
withoutMerging(editor, fn) {
const prev = HistoryEditor.isMerging(editor);
MERGING.set(editor, false);
try {
fn();
} finally {
MERGING.set(editor, prev);
}
},
/**
* Apply a series of changes inside a synchronous `fn`, without saving any of
* their operations into the history.
*/
withoutSaving(editor, fn) {
const prev = HistoryEditor.isSaving(editor);
SAVING.set(editor, false);
try {
fn();
} finally {
SAVING.set(editor, prev);
}
}
};
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r);
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function toPropertyKey(t) {
var i = toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : i + "";
}
function _defineProperty(e, r, t) {
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: true,
configurable: true,
writable: true
}) : e[r] = t, e;
}
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
const isChildOf = (path, parent) => path.length === parent.length + 1 && Path.isParent(parent, path);
const mapPath = (path, by) => Path.transform(path, by);
const mapSlot = (path, by) => {
if ((by.type === 'remove_node' || by.type === 'merge_node') && Path.equals(by.path, path)) {
return path;
}
if (by.type === 'move_node' && Path.equals(by.path, path)) {
return Path.transform(path, {
type: 'insert_node',
path: by.newPath,
node: {
text: ''
}
});
}
return mapPath(path, by);
};
const mapOffset = ({
path,
offset
}, by, side) => {
if (by.type === 'set_selection' || by.type === 'set_node') return {
path,
offset
};
if (by.type === 'insert_node' || by.type === 'remove_node' || by.type === 'split_node' || by.type === 'merge_node') {
if (isChildOf(by.path, path)) {
const index = by.path[by.path.length - 1];
if (by.type === 'insert_node' || by.type === 'split_node') {
const at = by.type === 'split_node' ? index + 1 : index;
if (at < offset || at === offset && side === 'after') offset++;
} else if (by.type === 'remove_node') {
if (index < offset) offset--;
} else if (index < offset) {
offset--;
} else if (index === offset) {
return null;
}
const mapped = mapPath(path, by);
return mapped && {
path: mapped,
offset
};
}
}
if (by.type === 'move_node') {
const from = isChildOf(by.path, path);
const to = Path.transform(by.path, by);
const into = to !== null && isChildOf(to, path);
if (from || into) return null;
}
const point = Point.transform({
path,
offset
}, by, {
affinity: side === 'after' ? 'forward' : 'backward'
});
return point && {
path: point.path,
offset: point.offset
};
};
const transformRemoveText = (op, by) => {
const end = op.offset + op.text.length;
if (by.type !== 'set_selection' && Path.equals(by.path, op.path)) {
if (by.type === 'insert_text' && by.offset > op.offset && by.offset < end) {
const split = by.offset - op.offset;
return [_objectSpread(_objectSpread({}, op), {}, {
text: op.text.slice(0, split)
}), _objectSpread(_objectSpread({}, op), {}, {
offset: op.offset + by.text.length,
text: op.text.slice(split)
})];
}
if (by.type === 'remove_text') {
const removedStart = by.offset;
const removedEnd = by.offset + by.text.length;
let text = '';
for (let i = op.offset; i < end; i++) {
if (i < removedStart || i >= removedEnd) text += op.text[i - op.offset];
}
if (!text) return [];
const offset = op.offset - Math.max(0, Math.min(op.offset, removedEnd) - removedStart);
return [_objectSpread(_objectSpread({}, op), {}, {
offset,
text
})];
}
if (by.type === 'split_node' && by.position > op.offset && by.position < end) {
const split = by.position - op.offset;
return [_objectSpread(_objectSpread({}, op), {}, {
text: op.text.slice(0, split)
}), _objectSpread(_objectSpread({}, op), {}, {
path: Path.next(op.path),
offset: 0,
text: op.text.slice(split)
})];
}
}
const start = mapOffset({
path: op.path,
offset: op.offset
}, by, 'after');
return start ? [_objectSpread(_objectSpread({}, op), {}, {
path: start.path,
offset: start.offset
})] : [];
};
const unchanged = (op, next) => {
const a = op;
const b = next;
return a.text === b.text && a.offset === b.offset && a.position === b.position && (a.path === b.path || Path.equals(a.path, b.path)) && (a.newPath === b.newPath || Path.equals(a.newPath, b.newPath));
};
const transformOperation = (op, by, side) => {
const result = transform(op, by, side);
if ((result === null || result === void 0 ? void 0 : result.length) === 1 && unchanged(op, result[0])) return [op];
return result;
};
const transform = (op, by, side) => {
switch (op.type) {
case 'set_selection':
return [op];
case 'set_node':
case 'remove_node':
{
const path = mapPath(op.path, by);
return path ? [_objectSpread(_objectSpread({}, op), {}, {
path
})] : [];
}
case 'insert_node':
{
if (by.type === 'insert_node' && Path.equals(by.path, op.path) && side === 'before') {
return [op];
}
const path = mapSlot(op.path, by);
return path ? [_objectSpread(_objectSpread({}, op), {}, {
path
})] : [];
}
case 'move_node':
{
const path = mapPath(op.path, by);
if (!path) return [];
const newPath = mapSlot(op.newPath, by);
return newPath ? [_objectSpread(_objectSpread({}, op), {}, {
path,
newPath
})] : null;
}
case 'insert_text':
{
const at = mapOffset({
path: op.path,
offset: op.offset
}, by, side);
return at ? [_objectSpread(_objectSpread({}, op), {}, {
path: at.path,
offset: at.offset
})] : [];
}
case 'remove_text':
return transformRemoveText(op, by);
case 'split_node':
{
if (!mapPath(op.path, by)) return [];
const at = mapOffset({
path: op.path,
offset: op.position
}, by, 'before');
return at ? [_objectSpread(_objectSpread({}, op), {}, {
path: at.path,
position: at.offset
})] : null;
}
case 'merge_node':
{
const path = mapPath(op.path, by);
if (!path) return null;
const previous = mapOffset({
path: Path.previous(op.path),
offset: op.position
}, by, 'after');
if (!previous || !Path.equals(previous.path, Path.previous(path))) {
return null;
}
return [_objectSpread(_objectSpread({}, op), {}, {
path,
position: previous.offset
})];
}
}
};
const opposite = side => side === 'after' ? 'before' : 'after';
const transformOperations = (ops, by, side) => {
if (ops.length === 0 || by.length === 0) return [ops, by];
if (ops.length > 1) {
const first = transformOperations([ops[0]], by, side);
if (!first) return null;
const rest = transformOperations(ops.slice(1), first[1], side);
if (!rest) return null;
return [first[0].concat(rest[0]), rest[1]];
}
if (by.length > 1) {
const first = transformOperations(ops, [by[0]], side);
if (!first) return null;
const rest = transformOperations(first[0], by.slice(1), side);
if (!rest) return null;
return [rest[0], first[1].concat(rest[1])];
}
const op = transformOperation(ops[0], by[0], side);
const other = transformOperation(by[0], ops[0], opposite(side));
return op && other ? [op, other] : null;
};
const transformRange = (range, ops) => {
let current = range;
for (const op of ops) {
if (!current) return null;
current = Range.transform(current, op, {
affinity: 'inward'
});
}
return current;
};
const same = (a, b) => a.length === b.length && a.every((op, index) => op === b[index]);
const rebaseRedos = (redos, op) => {
let unsaved = [op];
for (let i = redos.length - 1; i >= 0 && unsaved.length; i--) {
const batch = redos[i];
const result = transformOperations(batch.operations, unsaved, 'after');
if (!result || !result[0].length) {
redos.splice(0, i + 1);
return;
}
batch.selectionBefore = transformRange(batch.selectionBefore, unsaved);
if (!same(result[0], batch.operations)) batch.operations = result[0];
unsaved = result[1];
}
};
const rebaseUndos = (undos, op) => {
let unsaved = [op];
for (let i = undos.length - 1; i >= 0 && unsaved.length; i--) {
const batch = undos[i];
const inverse = batch.operations.map(Operation.inverse).reverse();
const result = transformOperations(inverse, unsaved, 'after');
if (!result) {
unsaved = batch.operations.concat(unsaved);
undos.splice(i, 1);
continue;
}
unsaved = result[1];
if (!result[0].length) {
undos.splice(i, 1);
continue;
}
if (!same(result[0], inverse)) {
batch.operations = result[0].map(Operation.inverse).reverse();
}
batch.selectionBefore = transformRange(batch.selectionBefore, unsaved);
}
};
const rebaseHistory = (history, op) => {
rebaseRedos(history.redos, op);
rebaseUndos(history.undos, op);
};
/**
* The `withHistory` plugin keeps track of the operation history of a Slate
* editor as operations are applied to it, using undo and redo stacks.
*
* If you are using TypeScript, you must extend Slate's CustomTypes to use
* this plugin.
*
* See https://docs.slatejs.org/concepts/11-typescript to learn how.
*/
const withHistory = editor => {
const e = editor;
const {
apply,
normalize,
setChildren
} = e;
let isSettled = true;
let isReplaying = false;
e.history = {
undos: [],
redos: []
};
e.redo = () => {
const {
history
} = e;
const {
redos
} = history;
if (redos.length > 0) {
const batch = redos[redos.length - 1];
if (batch.selectionBefore) {
Transforms.select(e, batch.selectionBefore);
}
isReplaying = true;
try {
HistoryEditor.withoutSaving(e, () => {
Editor.withoutNormalizing(e, () => {
for (const op of batch.operations) {
e.apply(op);
}
});
});
} finally {
isReplaying = false;
}
history.redos.pop();
e.writeHistory('undos', batch);
}
};
e.undo = () => {
const {
history
} = e;
const {
undos
} = history;
if (undos.length > 0) {
const batch = undos[undos.length - 1];
isReplaying = true;
try {
HistoryEditor.withoutSaving(e, () => {
Editor.withoutNormalizing(e, () => {
const inverseOps = batch.operations.map(Operation.inverse).reverse();
for (const op of inverseOps) {
e.apply(op);
}
if (batch.selectionBefore) {
Transforms.select(e, batch.selectionBefore);
}
});
});
} finally {
isReplaying = false;
}
e.writeHistory('redos', batch);
history.undos.pop();
}
};
e.apply = op => {
const {
operations,
history
} = e;
const {
undos
} = history;
const lastBatch = undos[undos.length - 1];
const lastOp = lastBatch && lastBatch.operations[lastBatch.operations.length - 1];
let save = HistoryEditor.isSaving(e);
let merge = HistoryEditor.isMerging(e);
if (save == null) {
save = shouldSave(op);
}
if (save) {
if (merge == null) {
if (lastBatch == null) {
merge = false;
} else if (operations.includes(lastOp)) {
merge = true;
} else {
merge = shouldMerge(op, lastOp);
}
}
if (!merge && !isSettled && lastBatch && operations.includes(lastOp)) {
merge = true;
}
if (HistoryEditor.isSplittingOnce(e)) {
merge = false;
HistoryEditor.setSplittingOnce(e, undefined);
}
if (lastBatch && merge) {
lastBatch.operations.push(op);
} else {
const batch = {
operations: [op],
selectionBefore: e.selection
};
e.writeHistory('undos', batch);
}
while (undos.length > 100) {
undos.shift();
}
history.redos = [];
} else if (!isReplaying && op.type !== 'set_selection') {
rebaseHistory(history, op);
}
isSettled = false;
apply(op);
};
e.normalize = options => {
normalize(options);
if (Editor.isNormalizing(e)) {
isSettled = true;
}
};
e.setChildren = children => {
const previous = e.history.undos[e.history.undos.length - 1];
const count = previous === null || previous === void 0 ? void 0 : previous.operations.length;
HistoryEditor.withNewBatch(e, () => setChildren(children));
const current = e.history.undos[e.history.undos.length - 1];
if (current !== previous || (current === null || current === void 0 ? void 0 : current.operations.length) !== count) {
HistoryEditor.setSplittingOnce(e, true);
}
};
e.writeHistory = (stack, batch) => {
e.history[stack].push(batch);
};
return e;
};
/**
* Check whether to merge an operation into the previous operation.
*/
const shouldMerge = (op, prev) => {
if (prev && op.type === 'insert_text' && prev.type === 'insert_text' && op.offset === prev.offset + prev.text.length && Path.equals(op.path, prev.path)) {
return true;
}
if (prev && op.type === 'remove_text' && prev.type === 'remove_text' && op.offset + op.text.length === prev.offset && Path.equals(op.path, prev.path)) {
return true;
}
return false;
};
/**
* Check whether an operation needs to be saved to the history.
*/
const shouldSave = (op, prev) => {
if (op.type === 'set_selection') {
return false;
}
return true;
};
export { HISTORY, History, HistoryEditor, MERGING, SAVING, SPLITTING_ONCE, withHistory };
//# sourceMappingURL=index.es.js.map