@pixerate/editor 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extensions/index.cjs +1 -0
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.js +1 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/history/index.cjs +260 -0
- package/dist/history/index.cjs.map +1 -0
- package/dist/history/index.d.cts +109 -0
- package/dist/history/index.d.ts +109 -0
- package/dist/history/index.js +232 -0
- package/dist/history/index.js.map +1 -0
- package/dist/index.cjs +240 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -1
- package/dist/serializers/index.cjs +7 -0
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.js +7 -0
- package/dist/serializers/index.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// src/history/HistoryManager.ts
|
|
2
|
+
var HistoryManager = class {
|
|
3
|
+
undoStack = [];
|
|
4
|
+
redoStack = [];
|
|
5
|
+
maxDepth;
|
|
6
|
+
isApplying = false;
|
|
7
|
+
batchDepth = 0;
|
|
8
|
+
currentBatch = null;
|
|
9
|
+
listeners = /* @__PURE__ */ new Set();
|
|
10
|
+
onStateChange;
|
|
11
|
+
cachedState;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.maxDepth = options.maxDepth ?? 50;
|
|
14
|
+
this.onStateChange = options.onStateChange;
|
|
15
|
+
this.cachedState = this.computeState();
|
|
16
|
+
}
|
|
17
|
+
computeState() {
|
|
18
|
+
const canUndo = this.undoStack.length > 0;
|
|
19
|
+
const canRedo = this.redoStack.length > 0;
|
|
20
|
+
return {
|
|
21
|
+
canUndo,
|
|
22
|
+
canRedo,
|
|
23
|
+
undoName: canUndo ? this.undoStack[this.undoStack.length - 1].name : "",
|
|
24
|
+
redoName: canRedo ? this.redoStack[this.redoStack.length - 1].name : "",
|
|
25
|
+
undoCount: this.undoStack.length,
|
|
26
|
+
redoCount: this.redoStack.length
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Returns a snapshot of the current history state.
|
|
31
|
+
* Cached to maintain referential equality across renders for useSyncExternalStore.
|
|
32
|
+
*/
|
|
33
|
+
getState() {
|
|
34
|
+
return this.cachedState;
|
|
35
|
+
}
|
|
36
|
+
notify() {
|
|
37
|
+
this.cachedState = this.computeState();
|
|
38
|
+
const state = this.cachedState;
|
|
39
|
+
this.onStateChange?.(state);
|
|
40
|
+
this.listeners.forEach((listener) => {
|
|
41
|
+
try {
|
|
42
|
+
listener(state);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error("[HistoryManager] Error in state listener:", err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Executes a command and pushes it onto the undo stack.
|
|
50
|
+
* Clears the redo stack.
|
|
51
|
+
*/
|
|
52
|
+
execute(command) {
|
|
53
|
+
if (this.isApplying) {
|
|
54
|
+
command.execute();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (this.batchDepth > 0 && this.currentBatch) {
|
|
58
|
+
command.execute();
|
|
59
|
+
this.currentBatch.push({
|
|
60
|
+
...command,
|
|
61
|
+
timestamp: command.timestamp ?? Date.now()
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
command.execute();
|
|
66
|
+
this.undoStack.push({
|
|
67
|
+
...command,
|
|
68
|
+
timestamp: command.timestamp ?? Date.now()
|
|
69
|
+
});
|
|
70
|
+
if (this.undoStack.length > this.maxDepth) {
|
|
71
|
+
this.undoStack.shift();
|
|
72
|
+
}
|
|
73
|
+
this.redoStack = [];
|
|
74
|
+
this.notify();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Executes a command, coalescing it with the previous command if they share
|
|
78
|
+
* the same `tag` and occurred within `windowMs` (default: 300ms).
|
|
79
|
+
* Useful for high-frequency interactions like slider scrubs or node drags.
|
|
80
|
+
*/
|
|
81
|
+
executeMerged(command, options = {}) {
|
|
82
|
+
const windowMs = options.windowMs ?? 300;
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (command.tag && this.undoStack.length > 0 && this.batchDepth === 0 && !this.isApplying) {
|
|
85
|
+
const top = this.undoStack[this.undoStack.length - 1];
|
|
86
|
+
const elapsed = top.timestamp ? now - top.timestamp : Infinity;
|
|
87
|
+
if (top.tag === command.tag && elapsed <= windowMs) {
|
|
88
|
+
command.execute();
|
|
89
|
+
const initialUndo = top.undo;
|
|
90
|
+
this.undoStack[this.undoStack.length - 1] = {
|
|
91
|
+
name: command.name,
|
|
92
|
+
tag: command.tag,
|
|
93
|
+
timestamp: now,
|
|
94
|
+
execute: command.execute,
|
|
95
|
+
undo: initialUndo,
|
|
96
|
+
redo: command.redo ?? command.execute
|
|
97
|
+
};
|
|
98
|
+
this.redoStack = [];
|
|
99
|
+
this.notify();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
this.execute({ ...command, timestamp: now });
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Batches multiple commands executed within `fn` into a single atomic undo entry.
|
|
107
|
+
*/
|
|
108
|
+
batch(name, fn) {
|
|
109
|
+
if (this.isApplying) {
|
|
110
|
+
fn();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (this.batchDepth > 0) {
|
|
114
|
+
fn();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.batchDepth++;
|
|
118
|
+
this.currentBatch = [];
|
|
119
|
+
try {
|
|
120
|
+
fn();
|
|
121
|
+
} finally {
|
|
122
|
+
this.batchDepth--;
|
|
123
|
+
const batchCommands = this.currentBatch;
|
|
124
|
+
this.currentBatch = null;
|
|
125
|
+
if (batchCommands && batchCommands.length > 0) {
|
|
126
|
+
const compoundCommand = {
|
|
127
|
+
name,
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
execute: () => {
|
|
130
|
+
for (let i = 0; i < batchCommands.length; i++) {
|
|
131
|
+
(batchCommands[i].redo ?? batchCommands[i].execute)();
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
undo: () => {
|
|
135
|
+
for (let i = batchCommands.length - 1; i >= 0; i--) {
|
|
136
|
+
batchCommands[i].undo();
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
redo: () => {
|
|
140
|
+
for (let i = 0; i < batchCommands.length; i++) {
|
|
141
|
+
(batchCommands[i].redo ?? batchCommands[i].execute)();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
this.undoStack.push(compoundCommand);
|
|
146
|
+
if (this.undoStack.length > this.maxDepth) {
|
|
147
|
+
this.undoStack.shift();
|
|
148
|
+
}
|
|
149
|
+
this.redoStack = [];
|
|
150
|
+
this.notify();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Reverts the most recent command on the undo stack.
|
|
156
|
+
* Returns true if a command was undone, false otherwise.
|
|
157
|
+
*/
|
|
158
|
+
undo() {
|
|
159
|
+
if (this.undoStack.length === 0 || this.isApplying || this.batchDepth > 0) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
this.isApplying = true;
|
|
163
|
+
try {
|
|
164
|
+
const command = this.undoStack.pop();
|
|
165
|
+
command.undo();
|
|
166
|
+
this.redoStack.push(command);
|
|
167
|
+
return true;
|
|
168
|
+
} finally {
|
|
169
|
+
this.isApplying = false;
|
|
170
|
+
this.notify();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Re-applies the most recent undone command on the redo stack.
|
|
175
|
+
* Returns true if a command was redone, false otherwise.
|
|
176
|
+
*/
|
|
177
|
+
redo() {
|
|
178
|
+
if (this.redoStack.length === 0 || this.isApplying || this.batchDepth > 0) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
this.isApplying = true;
|
|
182
|
+
try {
|
|
183
|
+
const command = this.redoStack.pop();
|
|
184
|
+
(command.redo ?? command.execute)();
|
|
185
|
+
this.undoStack.push(command);
|
|
186
|
+
return true;
|
|
187
|
+
} finally {
|
|
188
|
+
this.isApplying = false;
|
|
189
|
+
this.notify();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Clears both undo and redo stacks.
|
|
194
|
+
*/
|
|
195
|
+
clear() {
|
|
196
|
+
this.undoStack = [];
|
|
197
|
+
this.redoStack = [];
|
|
198
|
+
this.notify();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Subscribes to history state changes.
|
|
202
|
+
* The listener is immediately invoked with the current state.
|
|
203
|
+
* Returns an unsubscribe function.
|
|
204
|
+
*/
|
|
205
|
+
subscribe(listener) {
|
|
206
|
+
this.listeners.add(listener);
|
|
207
|
+
listener(this.getState());
|
|
208
|
+
return () => {
|
|
209
|
+
this.listeners.delete(listener);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Utility helper to create a Command from before/after state snapshots.
|
|
214
|
+
*/
|
|
215
|
+
static createSnapshotCommand(options) {
|
|
216
|
+
return {
|
|
217
|
+
name: options.name,
|
|
218
|
+
tag: options.tag,
|
|
219
|
+
execute: () => options.apply(options.after),
|
|
220
|
+
undo: () => options.apply(options.before),
|
|
221
|
+
redo: () => options.apply(options.after)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
function createHistoryManager(options = {}) {
|
|
226
|
+
return new HistoryManager(options);
|
|
227
|
+
}
|
|
228
|
+
export {
|
|
229
|
+
HistoryManager,
|
|
230
|
+
createHistoryManager
|
|
231
|
+
};
|
|
232
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/history/HistoryManager.ts"],"sourcesContent":["import {\n Command,\n HistoryManagerOptions,\n HistoryState,\n MergedExecutionOptions,\n SnapshotCommandOptions,\n} from \"./types\";\n\n/**\n * UI-agnostic History and Undo/Redo manager supporting command closures,\n * snapshot diffs, transaction batching, and high-frequency event coalescing.\n */\nexport class HistoryManager {\n private undoStack: Command[] = [];\n private redoStack: Command[] = [];\n private readonly maxDepth: number;\n private isApplying = false;\n private batchDepth = 0;\n private currentBatch: Command[] | null = null;\n private listeners: Set<(state: HistoryState) => void> = new Set();\n private onStateChange?: (state: HistoryState) => void;\n private cachedState: HistoryState;\n\n constructor(options: HistoryManagerOptions = {}) {\n this.maxDepth = options.maxDepth ?? 50;\n this.onStateChange = options.onStateChange;\n this.cachedState = this.computeState();\n }\n\n private computeState(): HistoryState {\n const canUndo = this.undoStack.length > 0;\n const canRedo = this.redoStack.length > 0;\n return {\n canUndo,\n canRedo,\n undoName: canUndo ? this.undoStack[this.undoStack.length - 1].name : \"\",\n redoName: canRedo ? this.redoStack[this.redoStack.length - 1].name : \"\",\n undoCount: this.undoStack.length,\n redoCount: this.redoStack.length,\n };\n }\n\n /**\n * Returns a snapshot of the current history state.\n * Cached to maintain referential equality across renders for useSyncExternalStore.\n */\n public getState(): HistoryState {\n return this.cachedState;\n }\n\n private notify(): void {\n this.cachedState = this.computeState();\n const state = this.cachedState;\n this.onStateChange?.(state);\n this.listeners.forEach((listener) => {\n try {\n listener(state);\n } catch (err) {\n console.error(\"[HistoryManager] Error in state listener:\", err);\n }\n });\n }\n\n /**\n * Executes a command and pushes it onto the undo stack.\n * Clears the redo stack.\n */\n public execute(command: Command): void {\n if (this.isApplying) {\n command.execute();\n return;\n }\n\n if (this.batchDepth > 0 && this.currentBatch) {\n command.execute();\n this.currentBatch.push({\n ...command,\n timestamp: command.timestamp ?? Date.now(),\n });\n return;\n }\n\n command.execute();\n\n this.undoStack.push({\n ...command,\n timestamp: command.timestamp ?? Date.now(),\n });\n\n if (this.undoStack.length > this.maxDepth) {\n this.undoStack.shift();\n }\n\n this.redoStack = [];\n this.notify();\n }\n\n /**\n * Executes a command, coalescing it with the previous command if they share\n * the same `tag` and occurred within `windowMs` (default: 300ms).\n * Useful for high-frequency interactions like slider scrubs or node drags.\n */\n public executeMerged(\n command: Command,\n options: MergedExecutionOptions = {},\n ): void {\n const windowMs = options.windowMs ?? 300;\n const now = Date.now();\n\n if (\n command.tag &&\n this.undoStack.length > 0 &&\n this.batchDepth === 0 &&\n !this.isApplying\n ) {\n const top = this.undoStack[this.undoStack.length - 1];\n const elapsed = top.timestamp ? now - top.timestamp : Infinity;\n\n if (top.tag === command.tag && elapsed <= windowMs) {\n command.execute();\n // Replace top with merged command keeping the original initial undo\n const initialUndo = top.undo;\n this.undoStack[this.undoStack.length - 1] = {\n name: command.name,\n tag: command.tag,\n timestamp: now,\n execute: command.execute,\n undo: initialUndo,\n redo: command.redo ?? command.execute,\n };\n this.redoStack = [];\n this.notify();\n return;\n }\n }\n\n this.execute({ ...command, timestamp: now });\n }\n\n /**\n * Batches multiple commands executed within `fn` into a single atomic undo entry.\n */\n public batch(name: string, fn: () => void): void {\n if (this.isApplying) {\n fn();\n return;\n }\n\n if (this.batchDepth > 0) {\n // Already inside a batch; execute directly\n fn();\n return;\n }\n\n this.batchDepth++;\n this.currentBatch = [];\n\n try {\n fn();\n } finally {\n this.batchDepth--;\n const batchCommands = this.currentBatch;\n this.currentBatch = null;\n\n if (batchCommands && batchCommands.length > 0) {\n const compoundCommand: Command = {\n name,\n timestamp: Date.now(),\n execute: () => {\n for (let i = 0; i < batchCommands.length; i++) {\n (batchCommands[i].redo ?? batchCommands[i].execute)();\n }\n },\n undo: () => {\n for (let i = batchCommands.length - 1; i >= 0; i--) {\n batchCommands[i].undo();\n }\n },\n redo: () => {\n for (let i = 0; i < batchCommands.length; i++) {\n (batchCommands[i].redo ?? batchCommands[i].execute)();\n }\n },\n };\n\n this.undoStack.push(compoundCommand);\n if (this.undoStack.length > this.maxDepth) {\n this.undoStack.shift();\n }\n this.redoStack = [];\n this.notify();\n }\n }\n }\n\n /**\n * Reverts the most recent command on the undo stack.\n * Returns true if a command was undone, false otherwise.\n */\n public undo(): boolean {\n if (this.undoStack.length === 0 || this.isApplying || this.batchDepth > 0) {\n return false;\n }\n\n this.isApplying = true;\n try {\n const command = this.undoStack.pop()!;\n command.undo();\n this.redoStack.push(command);\n return true;\n } finally {\n this.isApplying = false;\n this.notify();\n }\n }\n\n /**\n * Re-applies the most recent undone command on the redo stack.\n * Returns true if a command was redone, false otherwise.\n */\n public redo(): boolean {\n if (this.redoStack.length === 0 || this.isApplying || this.batchDepth > 0) {\n return false;\n }\n\n this.isApplying = true;\n try {\n const command = this.redoStack.pop()!;\n (command.redo ?? command.execute)();\n this.undoStack.push(command);\n return true;\n } finally {\n this.isApplying = false;\n this.notify();\n }\n }\n\n /**\n * Clears both undo and redo stacks.\n */\n public clear(): void {\n this.undoStack = [];\n this.redoStack = [];\n this.notify();\n }\n\n /**\n * Subscribes to history state changes.\n * The listener is immediately invoked with the current state.\n * Returns an unsubscribe function.\n */\n public subscribe(listener: (state: HistoryState) => void): () => void {\n this.listeners.add(listener);\n listener(this.getState());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Utility helper to create a Command from before/after state snapshots.\n */\n public static createSnapshotCommand<T>(\n options: SnapshotCommandOptions<T>,\n ): Command {\n return {\n name: options.name,\n tag: options.tag,\n execute: () => options.apply(options.after),\n undo: () => options.apply(options.before),\n redo: () => options.apply(options.after),\n };\n }\n}\n\n/**\n * Creates and initializes a new HistoryManager instance.\n */\nexport function createHistoryManager(\n options: HistoryManagerOptions = {},\n): HistoryManager {\n return new HistoryManager(options);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -39,6 +39,7 @@ __export(src_exports, {
|
|
|
39
39
|
FormulaEvaluator: () => FormulaEvaluator,
|
|
40
40
|
FormulaParser: () => FormulaParser,
|
|
41
41
|
GradientText: () => GradientText,
|
|
42
|
+
HistoryManager: () => HistoryManager,
|
|
42
43
|
INSTRUCTION_REGEX: () => INSTRUCTION_REGEX,
|
|
43
44
|
Image: () => Image2,
|
|
44
45
|
LoadingNode: () => LoadingNode,
|
|
@@ -53,6 +54,7 @@ __export(src_exports, {
|
|
|
53
54
|
colNameToIndex: () => colNameToIndex,
|
|
54
55
|
createDirtyTracker: () => createDirtyTracker,
|
|
55
56
|
createEditor: () => createEditor,
|
|
57
|
+
createHistoryManager: () => createHistoryManager,
|
|
56
58
|
createMentionExtension: () => createMentionExtension,
|
|
57
59
|
createRichTextPreset: () => createRichTextPreset,
|
|
58
60
|
defaultIsEqual: () => defaultIsEqual,
|
|
@@ -2203,6 +2205,7 @@ function createRichTextPreset(options = {}) {
|
|
|
2203
2205
|
openOnClick: openLinksOnClick,
|
|
2204
2206
|
autolink: true,
|
|
2205
2207
|
defaultProtocol: "https",
|
|
2208
|
+
protocols: ["file", "vscode", "cursor"],
|
|
2206
2209
|
HTMLAttributes: {
|
|
2207
2210
|
target: "_blank",
|
|
2208
2211
|
rel: "noopener noreferrer",
|
|
@@ -2423,6 +2426,13 @@ var md2 = new import_markdown_it2.default({
|
|
|
2423
2426
|
linkify: true,
|
|
2424
2427
|
breaks: true
|
|
2425
2428
|
}).use(import_markdown_it_task_lists2.default);
|
|
2429
|
+
var defaultValidateLink = md2.validateLink;
|
|
2430
|
+
md2.validateLink = (url) => {
|
|
2431
|
+
if (url.startsWith("file://") || url.startsWith("vscode://") || url.startsWith("cursor://")) {
|
|
2432
|
+
return true;
|
|
2433
|
+
}
|
|
2434
|
+
return defaultValidateLink.call(md2, url);
|
|
2435
|
+
};
|
|
2426
2436
|
function markdownToTipTapHtml(markdown) {
|
|
2427
2437
|
if (!markdown) return "<p></p>";
|
|
2428
2438
|
return md2.render(markdown);
|
|
@@ -3922,6 +3932,234 @@ var DirtyTracker = class {
|
|
|
3922
3932
|
function createDirtyTracker(initialBaseline, options) {
|
|
3923
3933
|
return new DirtyTracker(initialBaseline, options);
|
|
3924
3934
|
}
|
|
3935
|
+
|
|
3936
|
+
// src/history/HistoryManager.ts
|
|
3937
|
+
var HistoryManager = class {
|
|
3938
|
+
undoStack = [];
|
|
3939
|
+
redoStack = [];
|
|
3940
|
+
maxDepth;
|
|
3941
|
+
isApplying = false;
|
|
3942
|
+
batchDepth = 0;
|
|
3943
|
+
currentBatch = null;
|
|
3944
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3945
|
+
onStateChange;
|
|
3946
|
+
cachedState;
|
|
3947
|
+
constructor(options = {}) {
|
|
3948
|
+
this.maxDepth = options.maxDepth ?? 50;
|
|
3949
|
+
this.onStateChange = options.onStateChange;
|
|
3950
|
+
this.cachedState = this.computeState();
|
|
3951
|
+
}
|
|
3952
|
+
computeState() {
|
|
3953
|
+
const canUndo = this.undoStack.length > 0;
|
|
3954
|
+
const canRedo = this.redoStack.length > 0;
|
|
3955
|
+
return {
|
|
3956
|
+
canUndo,
|
|
3957
|
+
canRedo,
|
|
3958
|
+
undoName: canUndo ? this.undoStack[this.undoStack.length - 1].name : "",
|
|
3959
|
+
redoName: canRedo ? this.redoStack[this.redoStack.length - 1].name : "",
|
|
3960
|
+
undoCount: this.undoStack.length,
|
|
3961
|
+
redoCount: this.redoStack.length
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
/**
|
|
3965
|
+
* Returns a snapshot of the current history state.
|
|
3966
|
+
* Cached to maintain referential equality across renders for useSyncExternalStore.
|
|
3967
|
+
*/
|
|
3968
|
+
getState() {
|
|
3969
|
+
return this.cachedState;
|
|
3970
|
+
}
|
|
3971
|
+
notify() {
|
|
3972
|
+
this.cachedState = this.computeState();
|
|
3973
|
+
const state = this.cachedState;
|
|
3974
|
+
this.onStateChange?.(state);
|
|
3975
|
+
this.listeners.forEach((listener) => {
|
|
3976
|
+
try {
|
|
3977
|
+
listener(state);
|
|
3978
|
+
} catch (err) {
|
|
3979
|
+
console.error("[HistoryManager] Error in state listener:", err);
|
|
3980
|
+
}
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
/**
|
|
3984
|
+
* Executes a command and pushes it onto the undo stack.
|
|
3985
|
+
* Clears the redo stack.
|
|
3986
|
+
*/
|
|
3987
|
+
execute(command) {
|
|
3988
|
+
if (this.isApplying) {
|
|
3989
|
+
command.execute();
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
if (this.batchDepth > 0 && this.currentBatch) {
|
|
3993
|
+
command.execute();
|
|
3994
|
+
this.currentBatch.push({
|
|
3995
|
+
...command,
|
|
3996
|
+
timestamp: command.timestamp ?? Date.now()
|
|
3997
|
+
});
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
command.execute();
|
|
4001
|
+
this.undoStack.push({
|
|
4002
|
+
...command,
|
|
4003
|
+
timestamp: command.timestamp ?? Date.now()
|
|
4004
|
+
});
|
|
4005
|
+
if (this.undoStack.length > this.maxDepth) {
|
|
4006
|
+
this.undoStack.shift();
|
|
4007
|
+
}
|
|
4008
|
+
this.redoStack = [];
|
|
4009
|
+
this.notify();
|
|
4010
|
+
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Executes a command, coalescing it with the previous command if they share
|
|
4013
|
+
* the same `tag` and occurred within `windowMs` (default: 300ms).
|
|
4014
|
+
* Useful for high-frequency interactions like slider scrubs or node drags.
|
|
4015
|
+
*/
|
|
4016
|
+
executeMerged(command, options = {}) {
|
|
4017
|
+
const windowMs = options.windowMs ?? 300;
|
|
4018
|
+
const now = Date.now();
|
|
4019
|
+
if (command.tag && this.undoStack.length > 0 && this.batchDepth === 0 && !this.isApplying) {
|
|
4020
|
+
const top = this.undoStack[this.undoStack.length - 1];
|
|
4021
|
+
const elapsed = top.timestamp ? now - top.timestamp : Infinity;
|
|
4022
|
+
if (top.tag === command.tag && elapsed <= windowMs) {
|
|
4023
|
+
command.execute();
|
|
4024
|
+
const initialUndo = top.undo;
|
|
4025
|
+
this.undoStack[this.undoStack.length - 1] = {
|
|
4026
|
+
name: command.name,
|
|
4027
|
+
tag: command.tag,
|
|
4028
|
+
timestamp: now,
|
|
4029
|
+
execute: command.execute,
|
|
4030
|
+
undo: initialUndo,
|
|
4031
|
+
redo: command.redo ?? command.execute
|
|
4032
|
+
};
|
|
4033
|
+
this.redoStack = [];
|
|
4034
|
+
this.notify();
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
this.execute({ ...command, timestamp: now });
|
|
4039
|
+
}
|
|
4040
|
+
/**
|
|
4041
|
+
* Batches multiple commands executed within `fn` into a single atomic undo entry.
|
|
4042
|
+
*/
|
|
4043
|
+
batch(name, fn) {
|
|
4044
|
+
if (this.isApplying) {
|
|
4045
|
+
fn();
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
if (this.batchDepth > 0) {
|
|
4049
|
+
fn();
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
this.batchDepth++;
|
|
4053
|
+
this.currentBatch = [];
|
|
4054
|
+
try {
|
|
4055
|
+
fn();
|
|
4056
|
+
} finally {
|
|
4057
|
+
this.batchDepth--;
|
|
4058
|
+
const batchCommands = this.currentBatch;
|
|
4059
|
+
this.currentBatch = null;
|
|
4060
|
+
if (batchCommands && batchCommands.length > 0) {
|
|
4061
|
+
const compoundCommand = {
|
|
4062
|
+
name,
|
|
4063
|
+
timestamp: Date.now(),
|
|
4064
|
+
execute: () => {
|
|
4065
|
+
for (let i = 0; i < batchCommands.length; i++) {
|
|
4066
|
+
(batchCommands[i].redo ?? batchCommands[i].execute)();
|
|
4067
|
+
}
|
|
4068
|
+
},
|
|
4069
|
+
undo: () => {
|
|
4070
|
+
for (let i = batchCommands.length - 1; i >= 0; i--) {
|
|
4071
|
+
batchCommands[i].undo();
|
|
4072
|
+
}
|
|
4073
|
+
},
|
|
4074
|
+
redo: () => {
|
|
4075
|
+
for (let i = 0; i < batchCommands.length; i++) {
|
|
4076
|
+
(batchCommands[i].redo ?? batchCommands[i].execute)();
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
this.undoStack.push(compoundCommand);
|
|
4081
|
+
if (this.undoStack.length > this.maxDepth) {
|
|
4082
|
+
this.undoStack.shift();
|
|
4083
|
+
}
|
|
4084
|
+
this.redoStack = [];
|
|
4085
|
+
this.notify();
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
/**
|
|
4090
|
+
* Reverts the most recent command on the undo stack.
|
|
4091
|
+
* Returns true if a command was undone, false otherwise.
|
|
4092
|
+
*/
|
|
4093
|
+
undo() {
|
|
4094
|
+
if (this.undoStack.length === 0 || this.isApplying || this.batchDepth > 0) {
|
|
4095
|
+
return false;
|
|
4096
|
+
}
|
|
4097
|
+
this.isApplying = true;
|
|
4098
|
+
try {
|
|
4099
|
+
const command = this.undoStack.pop();
|
|
4100
|
+
command.undo();
|
|
4101
|
+
this.redoStack.push(command);
|
|
4102
|
+
return true;
|
|
4103
|
+
} finally {
|
|
4104
|
+
this.isApplying = false;
|
|
4105
|
+
this.notify();
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
/**
|
|
4109
|
+
* Re-applies the most recent undone command on the redo stack.
|
|
4110
|
+
* Returns true if a command was redone, false otherwise.
|
|
4111
|
+
*/
|
|
4112
|
+
redo() {
|
|
4113
|
+
if (this.redoStack.length === 0 || this.isApplying || this.batchDepth > 0) {
|
|
4114
|
+
return false;
|
|
4115
|
+
}
|
|
4116
|
+
this.isApplying = true;
|
|
4117
|
+
try {
|
|
4118
|
+
const command = this.redoStack.pop();
|
|
4119
|
+
(command.redo ?? command.execute)();
|
|
4120
|
+
this.undoStack.push(command);
|
|
4121
|
+
return true;
|
|
4122
|
+
} finally {
|
|
4123
|
+
this.isApplying = false;
|
|
4124
|
+
this.notify();
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
/**
|
|
4128
|
+
* Clears both undo and redo stacks.
|
|
4129
|
+
*/
|
|
4130
|
+
clear() {
|
|
4131
|
+
this.undoStack = [];
|
|
4132
|
+
this.redoStack = [];
|
|
4133
|
+
this.notify();
|
|
4134
|
+
}
|
|
4135
|
+
/**
|
|
4136
|
+
* Subscribes to history state changes.
|
|
4137
|
+
* The listener is immediately invoked with the current state.
|
|
4138
|
+
* Returns an unsubscribe function.
|
|
4139
|
+
*/
|
|
4140
|
+
subscribe(listener) {
|
|
4141
|
+
this.listeners.add(listener);
|
|
4142
|
+
listener(this.getState());
|
|
4143
|
+
return () => {
|
|
4144
|
+
this.listeners.delete(listener);
|
|
4145
|
+
};
|
|
4146
|
+
}
|
|
4147
|
+
/**
|
|
4148
|
+
* Utility helper to create a Command from before/after state snapshots.
|
|
4149
|
+
*/
|
|
4150
|
+
static createSnapshotCommand(options) {
|
|
4151
|
+
return {
|
|
4152
|
+
name: options.name,
|
|
4153
|
+
tag: options.tag,
|
|
4154
|
+
execute: () => options.apply(options.after),
|
|
4155
|
+
undo: () => options.apply(options.before),
|
|
4156
|
+
redo: () => options.apply(options.after)
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4159
|
+
};
|
|
4160
|
+
function createHistoryManager(options = {}) {
|
|
4161
|
+
return new HistoryManager(options);
|
|
4162
|
+
}
|
|
3925
4163
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3926
4164
|
0 && (module.exports = {
|
|
3927
4165
|
ColorHighlighter,
|
|
@@ -3933,6 +4171,7 @@ function createDirtyTracker(initialBaseline, options) {
|
|
|
3933
4171
|
FormulaEvaluator,
|
|
3934
4172
|
FormulaParser,
|
|
3935
4173
|
GradientText,
|
|
4174
|
+
HistoryManager,
|
|
3936
4175
|
INSTRUCTION_REGEX,
|
|
3937
4176
|
Image,
|
|
3938
4177
|
LoadingNode,
|
|
@@ -3947,6 +4186,7 @@ function createDirtyTracker(initialBaseline, options) {
|
|
|
3947
4186
|
colNameToIndex,
|
|
3948
4187
|
createDirtyTracker,
|
|
3949
4188
|
createEditor,
|
|
4189
|
+
createHistoryManager,
|
|
3950
4190
|
createMentionExtension,
|
|
3951
4191
|
createRichTextPreset,
|
|
3952
4192
|
defaultIsEqual,
|