UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

341 lines (340 loc) • 9.7 kB
"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"); const HistoryRecorderState = { Recording: "recording", RecordingPreserveRedoStack: "recordingPreserveRedoStack", Paused: "paused" }; class HistoryManager { store; dispose; state = HistoryRecorderState.Recording; pendingDiff = new PendingDiff(); stacks = (0, import_state.atom)( "HistoryManager.stacks", { undos: stack(), redos: 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 HistoryRecorderState.Recording: this.pendingDiff.apply(entry.changes); this.stacks.update(({ undos }) => ({ undos, redos: stack() })); break; case HistoryRecorderState.RecordingPreserveRedoStack: this.pendingDiff.apply(entry.changes); break; case HistoryRecorderState.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 !== HistoryRecorderState.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 = HistoryRecorderState.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) { this.pendingDiff.restore(pendingDiff); 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 = HistoryRecorderState.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) { if (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: stack(), redos: 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: stackToArray(undos), redos: stackToArray(redos), pendingDiff: this.pendingDiff.debug(), state: this.state }; } } const modeToState = { record: HistoryRecorderState.Recording, "record-preserveRedoStack": HistoryRecorderState.RecordingPreserveRedoStack, ignore: HistoryRecorderState.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; } restore(diff) { this.diff = diff; this.isEmptyAtom.set((0, import_store.isRecordsDiffEmpty)(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() }; } } function stack() { return EMPTY_STACK_ITEM; } class EmptyStackItem { length = 0; head = null; tail = this; push(head) { return new StackItem(head, this); } } const EMPTY_STACK_ITEM = new EmptyStackItem(); class StackItem { constructor(head, tail) { this.head = head; this.tail = tail; this.length = tail.length + 1; } head; tail; length; push(head) { return new StackItem(head, this); } } function stackToArray(stack2) { if (!stack2.length) { return import_state.EMPTY_ARRAY; } const arr = []; while (stack2.length) { arr.push(stack2.head); stack2 = stack2.tail; } return arr; } //# sourceMappingURL=HistoryManager.js.map