@tldraw/editor
Version:
A tiny little drawing app (editor).
300 lines (299 loc) • 9.16 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var HistoryManager_exports = {};
__export(HistoryManager_exports, {
HistoryManager: () => HistoryManager
});
module.exports = __toCommonJS(HistoryManager_exports);
var import_state = require("@tldraw/state");
var import_store = require("@tldraw/store");
var import_utils = require("@tldraw/utils");
var import_Stack = require("./Stack");
var HistoryRecorderState = /* @__PURE__ */ ((HistoryRecorderState2) => {
HistoryRecorderState2["Recording"] = "recording";
HistoryRecorderState2["RecordingPreserveRedoStack"] = "recordingPreserveRedoStack";
HistoryRecorderState2["Paused"] = "paused";
return HistoryRecorderState2;
})(HistoryRecorderState || {});
class HistoryManager {
store;
dispose;
state = "recording" /* Recording */;
pendingDiff = new PendingDiff();
stacks = (0, import_state.atom)(
"HistoryManager.stacks",
{
undos: (0, import_Stack.stack)(),
redos: (0, import_Stack.stack)()
},
{
isEqual: (a, b) => a.undos === b.undos && a.redos === b.redos
}
);
annotateError;
constructor(opts) {
this.store = opts.store;
this.annotateError = opts.annotateError ?? import_utils.noop;
this.dispose = this.store.addHistoryInterceptor((entry, source) => {
if (source !== "user") return;
switch (this.state) {
case "recording" /* Recording */:
this.pendingDiff.apply(entry.changes);
this.stacks.update(({ undos }) => ({ undos, redos: (0, import_Stack.stack)() }));
break;
case "recordingPreserveRedoStack" /* RecordingPreserveRedoStack */:
this.pendingDiff.apply(entry.changes);
break;
case "paused" /* Paused */:
break;
default:
(0, import_utils.exhaustiveSwitchError)(this.state);
}
});
}
flushPendingDiff() {
if (this.pendingDiff.isEmpty()) return;
const diff = this.pendingDiff.clear();
this.stacks.update(({ undos, redos }) => ({
undos: undos.push({ type: "diff", diff }),
redos
}));
}
getNumUndos() {
return this.stacks.get().undos.length + (this.pendingDiff.isEmpty() ? 0 : 1);
}
getNumRedos() {
return this.stacks.get().redos.length;
}
/** @internal */
_isInBatch = false;
batch(fn, opts) {
const previousState = this.state;
if (previousState !== "paused" /* Paused */ && opts?.history) {
this.state = modeToState[opts.history];
}
try {
if (this._isInBatch) {
(0, import_state.transact)(fn);
return this;
}
this._isInBatch = true;
try {
(0, import_state.transact)(fn);
} catch (error) {
this.annotateError(error);
throw error;
} finally {
this._isInBatch = false;
}
return this;
} finally {
this.state = previousState;
}
}
// History
_undo({ pushToRedoStack, toMark = void 0 }) {
const previousState = this.state;
this.state = "paused" /* Paused */;
try {
let { undos, redos } = this.stacks.get();
const pendingDiff = this.pendingDiff.clear();
const isPendingDiffEmpty = (0, import_store.isRecordsDiffEmpty)(pendingDiff);
const diffToUndo = (0, import_store.reverseRecordsDiff)(pendingDiff);
if (pushToRedoStack && !isPendingDiffEmpty) {
redos = redos.push({ type: "diff", diff: pendingDiff });
}
let didFindMark = false;
if (isPendingDiffEmpty) {
while (undos.head?.type === "stop") {
const mark = undos.head;
undos = undos.tail;
if (pushToRedoStack) {
redos = redos.push(mark);
}
if (mark.id === toMark) {
didFindMark = true;
break;
}
}
}
if (!didFindMark) {
loop: while (undos.head) {
const undo = undos.head;
undos = undos.tail;
if (pushToRedoStack) {
redos = redos.push(undo);
}
switch (undo.type) {
case "diff":
(0, import_store.squashRecordDiffsMutable)(diffToUndo, [(0, import_store.reverseRecordsDiff)(undo.diff)]);
break;
case "stop":
if (!toMark) break loop;
if (undo.id === toMark) {
didFindMark = true;
break loop;
}
break;
default:
(0, import_utils.exhaustiveSwitchError)(undo);
}
}
}
if (!didFindMark && toMark) {
return this;
}
this.store.applyDiff(diffToUndo, { ignoreEphemeralKeys: true });
this.store.ensureStoreIsUsable();
this.stacks.set({ undos, redos });
} finally {
this.state = previousState;
}
return this;
}
undo() {
this._undo({ pushToRedoStack: true });
return this;
}
redo() {
const previousState = this.state;
this.state = "paused" /* Paused */;
try {
this.flushPendingDiff();
let { undos, redos } = this.stacks.get();
if (redos.length === 0) {
return this;
}
while (redos.head?.type === "stop") {
undos = undos.push(redos.head);
redos = redos.tail;
}
const diffToRedo = (0, import_store.createEmptyRecordsDiff)();
while (redos.head) {
const redo = redos.head;
undos = undos.push(redo);
redos = redos.tail;
if (redo.type === "diff") {
(0, import_store.squashRecordDiffsMutable)(diffToRedo, [redo.diff]);
} else {
break;
}
}
this.store.applyDiff(diffToRedo, { ignoreEphemeralKeys: true });
this.store.ensureStoreIsUsable();
this.stacks.set({ undos, redos });
} finally {
this.state = previousState;
}
return this;
}
bail() {
this._undo({ pushToRedoStack: false });
return this;
}
bailToMark(id) {
this._undo({ pushToRedoStack: false, toMark: id });
return this;
}
squashToMark(id) {
let top = this.stacks.get().undos;
const popped = [];
while (top.head && !(top.head.type === "stop" && top.head.id === id)) {
if (top.head.type === "diff") {
popped.push(top.head.diff);
}
top = top.tail;
}
if (!top.head || top.head?.id !== id) {
console.error("Could not find mark to squash to: ", id);
return this;
}
if (popped.length === 0) {
return this;
}
const diff = (0, import_store.createEmptyRecordsDiff)();
(0, import_store.squashRecordDiffsMutable)(diff, popped.reverse());
this.stacks.update(({ redos }) => ({
undos: top.push({
type: "diff",
diff
}),
redos
}));
return this;
}
/** @internal */
_mark(id) {
(0, import_state.transact)(() => {
this.flushPendingDiff();
this.stacks.update(({ undos, redos }) => ({ undos: undos.push({ type: "stop", id }), redos }));
});
}
clear() {
this.stacks.set({ undos: (0, import_Stack.stack)(), redos: (0, import_Stack.stack)() });
this.pendingDiff.clear();
}
/** @internal */
getMarkIdMatching(idSubstring) {
let top = this.stacks.get().undos;
while (top.head) {
if (top.head.type === "stop" && top.head.id.includes(idSubstring)) {
return top.head.id;
}
top = top.tail;
}
return null;
}
/** @internal */
debug() {
const { undos, redos } = this.stacks.get();
return {
undos: undos.toArray(),
redos: redos.toArray(),
pendingDiff: this.pendingDiff.debug(),
state: this.state
};
}
}
const modeToState = {
record: "recording" /* Recording */,
"record-preserveRedoStack": "recordingPreserveRedoStack" /* RecordingPreserveRedoStack */,
ignore: "paused" /* Paused */
};
class PendingDiff {
diff = (0, import_store.createEmptyRecordsDiff)();
isEmptyAtom = (0, import_state.atom)("PendingDiff.isEmpty", true);
clear() {
const diff = this.diff;
this.diff = (0, import_store.createEmptyRecordsDiff)();
this.isEmptyAtom.set(true);
return diff;
}
isEmpty() {
return this.isEmptyAtom.get();
}
apply(diff) {
(0, import_store.squashRecordDiffsMutable)(this.diff, [diff]);
this.isEmptyAtom.set((0, import_store.isRecordsDiffEmpty)(this.diff));
}
debug() {
return { diff: this.diff, isEmpty: this.isEmpty() };
}
}
//# sourceMappingURL=HistoryManager.js.map