@pixerate/editor 0.6.1 → 0.8.0
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/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 +322 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +317 -0
- package/dist/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
|
@@ -33,11 +33,13 @@ __export(src_exports, {
|
|
|
33
33
|
ColorHighlighter: () => ColorHighlighter,
|
|
34
34
|
DataSourceBinder: () => DataSourceBinder,
|
|
35
35
|
DependencyGraph: () => DependencyGraph,
|
|
36
|
+
DirtyTracker: () => DirtyTracker,
|
|
36
37
|
EditorController: () => EditorController,
|
|
37
38
|
FontSize: () => FontSize,
|
|
38
39
|
FormulaEvaluator: () => FormulaEvaluator,
|
|
39
40
|
FormulaParser: () => FormulaParser,
|
|
40
41
|
GradientText: () => GradientText,
|
|
42
|
+
HistoryManager: () => HistoryManager,
|
|
41
43
|
INSTRUCTION_REGEX: () => INSTRUCTION_REGEX,
|
|
42
44
|
Image: () => Image2,
|
|
43
45
|
LoadingNode: () => LoadingNode,
|
|
@@ -50,9 +52,12 @@ __export(src_exports, {
|
|
|
50
52
|
TemplateSuggestions: () => TemplateSuggestions,
|
|
51
53
|
VARIABLE_REGEX: () => VARIABLE_REGEX,
|
|
52
54
|
colNameToIndex: () => colNameToIndex,
|
|
55
|
+
createDirtyTracker: () => createDirtyTracker,
|
|
53
56
|
createEditor: () => createEditor,
|
|
57
|
+
createHistoryManager: () => createHistoryManager,
|
|
54
58
|
createMentionExtension: () => createMentionExtension,
|
|
55
59
|
createRichTextPreset: () => createRichTextPreset,
|
|
60
|
+
defaultIsEqual: () => defaultIsEqual,
|
|
56
61
|
escapeHtml: () => escapeHtml,
|
|
57
62
|
extractCellDependencies: () => extractCellDependencies,
|
|
58
63
|
extractInstructionNames: () => extractInstructionNames,
|
|
@@ -2443,6 +2448,7 @@ var EditorController = class {
|
|
|
2443
2448
|
onMarkdownChange;
|
|
2444
2449
|
onTokensChange;
|
|
2445
2450
|
onSelectionChange;
|
|
2451
|
+
baselineContent;
|
|
2446
2452
|
constructor(options = {}) {
|
|
2447
2453
|
const {
|
|
2448
2454
|
element,
|
|
@@ -2480,6 +2486,7 @@ var EditorController = class {
|
|
|
2480
2486
|
if (plainTextMode) {
|
|
2481
2487
|
this.enforcePlainTextClipboard();
|
|
2482
2488
|
}
|
|
2489
|
+
this.baselineContent = this.getCurrentContentString();
|
|
2483
2490
|
}
|
|
2484
2491
|
enforcePlainTextClipboard() {
|
|
2485
2492
|
this.editor.setOptions({
|
|
@@ -2553,6 +2560,23 @@ var EditorController = class {
|
|
|
2553
2560
|
replaceRange(from, to, text) {
|
|
2554
2561
|
this.editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, text).run();
|
|
2555
2562
|
}
|
|
2563
|
+
getCurrentContentString() {
|
|
2564
|
+
if (this.plainTextMode) return this.getPlainText();
|
|
2565
|
+
if (this.markdownMode) return this.getMarkdown();
|
|
2566
|
+
return this.editor.getHTML();
|
|
2567
|
+
}
|
|
2568
|
+
isDirty() {
|
|
2569
|
+
return this.getCurrentContentString() !== this.baselineContent;
|
|
2570
|
+
}
|
|
2571
|
+
setCheckpoint(content) {
|
|
2572
|
+
this.baselineContent = content !== void 0 ? content : this.getCurrentContentString();
|
|
2573
|
+
}
|
|
2574
|
+
resetDirty() {
|
|
2575
|
+
this.setCheckpoint();
|
|
2576
|
+
}
|
|
2577
|
+
getBaselineContent() {
|
|
2578
|
+
return this.baselineContent;
|
|
2579
|
+
}
|
|
2556
2580
|
destroy() {
|
|
2557
2581
|
this.editor.destroy();
|
|
2558
2582
|
}
|
|
@@ -3835,16 +3859,311 @@ var SpreadsheetController = class {
|
|
|
3835
3859
|
this.onDocumentChange?.(this.document);
|
|
3836
3860
|
}
|
|
3837
3861
|
};
|
|
3862
|
+
|
|
3863
|
+
// src/dirty.ts
|
|
3864
|
+
function defaultIsEqual(a, b) {
|
|
3865
|
+
if (Object.is(a, b)) return true;
|
|
3866
|
+
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
|
|
3867
|
+
return false;
|
|
3868
|
+
}
|
|
3869
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
3870
|
+
if (a.length !== b.length) return false;
|
|
3871
|
+
for (let i = 0; i < a.length; i++) {
|
|
3872
|
+
if (!defaultIsEqual(a[i], b[i])) return false;
|
|
3873
|
+
}
|
|
3874
|
+
return true;
|
|
3875
|
+
}
|
|
3876
|
+
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
3877
|
+
return false;
|
|
3878
|
+
}
|
|
3879
|
+
const keysA = Object.keys(a);
|
|
3880
|
+
const keysB = Object.keys(b);
|
|
3881
|
+
if (keysA.length !== keysB.length) return false;
|
|
3882
|
+
for (const key of keysA) {
|
|
3883
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
3884
|
+
if (!defaultIsEqual(a[key], b[key])) return false;
|
|
3885
|
+
}
|
|
3886
|
+
return true;
|
|
3887
|
+
}
|
|
3888
|
+
var DirtyTracker = class {
|
|
3889
|
+
baseline;
|
|
3890
|
+
isEqual;
|
|
3891
|
+
constructor(initialBaseline, options) {
|
|
3892
|
+
this.baseline = initialBaseline;
|
|
3893
|
+
if (typeof options === "function") {
|
|
3894
|
+
this.isEqual = options;
|
|
3895
|
+
} else {
|
|
3896
|
+
this.isEqual = options?.isEqual ?? defaultIsEqual;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* Returns current baseline value.
|
|
3901
|
+
*/
|
|
3902
|
+
getBaseline() {
|
|
3903
|
+
return this.baseline;
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* Sets a new baseline value without triggering any dirty state.
|
|
3907
|
+
*/
|
|
3908
|
+
setBaseline(value) {
|
|
3909
|
+
this.baseline = value;
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3912
|
+
* Checks whether the given value differs from the baseline.
|
|
3913
|
+
*/
|
|
3914
|
+
isDirty(current) {
|
|
3915
|
+
return !this.isEqual(this.baseline, current);
|
|
3916
|
+
}
|
|
3917
|
+
/**
|
|
3918
|
+
* Resets the baseline to the provided current value (or clears dirty status).
|
|
3919
|
+
*/
|
|
3920
|
+
reset(current) {
|
|
3921
|
+
this.baseline = current;
|
|
3922
|
+
}
|
|
3923
|
+
};
|
|
3924
|
+
function createDirtyTracker(initialBaseline, options) {
|
|
3925
|
+
return new DirtyTracker(initialBaseline, options);
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
// src/history/HistoryManager.ts
|
|
3929
|
+
var HistoryManager = class {
|
|
3930
|
+
undoStack = [];
|
|
3931
|
+
redoStack = [];
|
|
3932
|
+
maxDepth;
|
|
3933
|
+
isApplying = false;
|
|
3934
|
+
batchDepth = 0;
|
|
3935
|
+
currentBatch = null;
|
|
3936
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3937
|
+
onStateChange;
|
|
3938
|
+
cachedState;
|
|
3939
|
+
constructor(options = {}) {
|
|
3940
|
+
this.maxDepth = options.maxDepth ?? 50;
|
|
3941
|
+
this.onStateChange = options.onStateChange;
|
|
3942
|
+
this.cachedState = this.computeState();
|
|
3943
|
+
}
|
|
3944
|
+
computeState() {
|
|
3945
|
+
const canUndo = this.undoStack.length > 0;
|
|
3946
|
+
const canRedo = this.redoStack.length > 0;
|
|
3947
|
+
return {
|
|
3948
|
+
canUndo,
|
|
3949
|
+
canRedo,
|
|
3950
|
+
undoName: canUndo ? this.undoStack[this.undoStack.length - 1].name : "",
|
|
3951
|
+
redoName: canRedo ? this.redoStack[this.redoStack.length - 1].name : "",
|
|
3952
|
+
undoCount: this.undoStack.length,
|
|
3953
|
+
redoCount: this.redoStack.length
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
/**
|
|
3957
|
+
* Returns a snapshot of the current history state.
|
|
3958
|
+
* Cached to maintain referential equality across renders for useSyncExternalStore.
|
|
3959
|
+
*/
|
|
3960
|
+
getState() {
|
|
3961
|
+
return this.cachedState;
|
|
3962
|
+
}
|
|
3963
|
+
notify() {
|
|
3964
|
+
this.cachedState = this.computeState();
|
|
3965
|
+
const state = this.cachedState;
|
|
3966
|
+
this.onStateChange?.(state);
|
|
3967
|
+
this.listeners.forEach((listener) => {
|
|
3968
|
+
try {
|
|
3969
|
+
listener(state);
|
|
3970
|
+
} catch (err) {
|
|
3971
|
+
console.error("[HistoryManager] Error in state listener:", err);
|
|
3972
|
+
}
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Executes a command and pushes it onto the undo stack.
|
|
3977
|
+
* Clears the redo stack.
|
|
3978
|
+
*/
|
|
3979
|
+
execute(command) {
|
|
3980
|
+
if (this.isApplying) {
|
|
3981
|
+
command.execute();
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
if (this.batchDepth > 0 && this.currentBatch) {
|
|
3985
|
+
command.execute();
|
|
3986
|
+
this.currentBatch.push({
|
|
3987
|
+
...command,
|
|
3988
|
+
timestamp: command.timestamp ?? Date.now()
|
|
3989
|
+
});
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
command.execute();
|
|
3993
|
+
this.undoStack.push({
|
|
3994
|
+
...command,
|
|
3995
|
+
timestamp: command.timestamp ?? Date.now()
|
|
3996
|
+
});
|
|
3997
|
+
if (this.undoStack.length > this.maxDepth) {
|
|
3998
|
+
this.undoStack.shift();
|
|
3999
|
+
}
|
|
4000
|
+
this.redoStack = [];
|
|
4001
|
+
this.notify();
|
|
4002
|
+
}
|
|
4003
|
+
/**
|
|
4004
|
+
* Executes a command, coalescing it with the previous command if they share
|
|
4005
|
+
* the same `tag` and occurred within `windowMs` (default: 300ms).
|
|
4006
|
+
* Useful for high-frequency interactions like slider scrubs or node drags.
|
|
4007
|
+
*/
|
|
4008
|
+
executeMerged(command, options = {}) {
|
|
4009
|
+
const windowMs = options.windowMs ?? 300;
|
|
4010
|
+
const now = Date.now();
|
|
4011
|
+
if (command.tag && this.undoStack.length > 0 && this.batchDepth === 0 && !this.isApplying) {
|
|
4012
|
+
const top = this.undoStack[this.undoStack.length - 1];
|
|
4013
|
+
const elapsed = top.timestamp ? now - top.timestamp : Infinity;
|
|
4014
|
+
if (top.tag === command.tag && elapsed <= windowMs) {
|
|
4015
|
+
command.execute();
|
|
4016
|
+
const initialUndo = top.undo;
|
|
4017
|
+
this.undoStack[this.undoStack.length - 1] = {
|
|
4018
|
+
name: command.name,
|
|
4019
|
+
tag: command.tag,
|
|
4020
|
+
timestamp: now,
|
|
4021
|
+
execute: command.execute,
|
|
4022
|
+
undo: initialUndo,
|
|
4023
|
+
redo: command.redo ?? command.execute
|
|
4024
|
+
};
|
|
4025
|
+
this.redoStack = [];
|
|
4026
|
+
this.notify();
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
this.execute({ ...command, timestamp: now });
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Batches multiple commands executed within `fn` into a single atomic undo entry.
|
|
4034
|
+
*/
|
|
4035
|
+
batch(name, fn) {
|
|
4036
|
+
if (this.isApplying) {
|
|
4037
|
+
fn();
|
|
4038
|
+
return;
|
|
4039
|
+
}
|
|
4040
|
+
if (this.batchDepth > 0) {
|
|
4041
|
+
fn();
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
this.batchDepth++;
|
|
4045
|
+
this.currentBatch = [];
|
|
4046
|
+
try {
|
|
4047
|
+
fn();
|
|
4048
|
+
} finally {
|
|
4049
|
+
this.batchDepth--;
|
|
4050
|
+
const batchCommands = this.currentBatch;
|
|
4051
|
+
this.currentBatch = null;
|
|
4052
|
+
if (batchCommands && batchCommands.length > 0) {
|
|
4053
|
+
const compoundCommand = {
|
|
4054
|
+
name,
|
|
4055
|
+
timestamp: Date.now(),
|
|
4056
|
+
execute: () => {
|
|
4057
|
+
for (let i = 0; i < batchCommands.length; i++) {
|
|
4058
|
+
(batchCommands[i].redo ?? batchCommands[i].execute)();
|
|
4059
|
+
}
|
|
4060
|
+
},
|
|
4061
|
+
undo: () => {
|
|
4062
|
+
for (let i = batchCommands.length - 1; i >= 0; i--) {
|
|
4063
|
+
batchCommands[i].undo();
|
|
4064
|
+
}
|
|
4065
|
+
},
|
|
4066
|
+
redo: () => {
|
|
4067
|
+
for (let i = 0; i < batchCommands.length; i++) {
|
|
4068
|
+
(batchCommands[i].redo ?? batchCommands[i].execute)();
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
};
|
|
4072
|
+
this.undoStack.push(compoundCommand);
|
|
4073
|
+
if (this.undoStack.length > this.maxDepth) {
|
|
4074
|
+
this.undoStack.shift();
|
|
4075
|
+
}
|
|
4076
|
+
this.redoStack = [];
|
|
4077
|
+
this.notify();
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* Reverts the most recent command on the undo stack.
|
|
4083
|
+
* Returns true if a command was undone, false otherwise.
|
|
4084
|
+
*/
|
|
4085
|
+
undo() {
|
|
4086
|
+
if (this.undoStack.length === 0 || this.isApplying || this.batchDepth > 0) {
|
|
4087
|
+
return false;
|
|
4088
|
+
}
|
|
4089
|
+
this.isApplying = true;
|
|
4090
|
+
try {
|
|
4091
|
+
const command = this.undoStack.pop();
|
|
4092
|
+
command.undo();
|
|
4093
|
+
this.redoStack.push(command);
|
|
4094
|
+
return true;
|
|
4095
|
+
} finally {
|
|
4096
|
+
this.isApplying = false;
|
|
4097
|
+
this.notify();
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
/**
|
|
4101
|
+
* Re-applies the most recent undone command on the redo stack.
|
|
4102
|
+
* Returns true if a command was redone, false otherwise.
|
|
4103
|
+
*/
|
|
4104
|
+
redo() {
|
|
4105
|
+
if (this.redoStack.length === 0 || this.isApplying || this.batchDepth > 0) {
|
|
4106
|
+
return false;
|
|
4107
|
+
}
|
|
4108
|
+
this.isApplying = true;
|
|
4109
|
+
try {
|
|
4110
|
+
const command = this.redoStack.pop();
|
|
4111
|
+
(command.redo ?? command.execute)();
|
|
4112
|
+
this.undoStack.push(command);
|
|
4113
|
+
return true;
|
|
4114
|
+
} finally {
|
|
4115
|
+
this.isApplying = false;
|
|
4116
|
+
this.notify();
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
/**
|
|
4120
|
+
* Clears both undo and redo stacks.
|
|
4121
|
+
*/
|
|
4122
|
+
clear() {
|
|
4123
|
+
this.undoStack = [];
|
|
4124
|
+
this.redoStack = [];
|
|
4125
|
+
this.notify();
|
|
4126
|
+
}
|
|
4127
|
+
/**
|
|
4128
|
+
* Subscribes to history state changes.
|
|
4129
|
+
* The listener is immediately invoked with the current state.
|
|
4130
|
+
* Returns an unsubscribe function.
|
|
4131
|
+
*/
|
|
4132
|
+
subscribe(listener) {
|
|
4133
|
+
this.listeners.add(listener);
|
|
4134
|
+
listener(this.getState());
|
|
4135
|
+
return () => {
|
|
4136
|
+
this.listeners.delete(listener);
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
/**
|
|
4140
|
+
* Utility helper to create a Command from before/after state snapshots.
|
|
4141
|
+
*/
|
|
4142
|
+
static createSnapshotCommand(options) {
|
|
4143
|
+
return {
|
|
4144
|
+
name: options.name,
|
|
4145
|
+
tag: options.tag,
|
|
4146
|
+
execute: () => options.apply(options.after),
|
|
4147
|
+
undo: () => options.apply(options.before),
|
|
4148
|
+
redo: () => options.apply(options.after)
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
};
|
|
4152
|
+
function createHistoryManager(options = {}) {
|
|
4153
|
+
return new HistoryManager(options);
|
|
4154
|
+
}
|
|
3838
4155
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3839
4156
|
0 && (module.exports = {
|
|
3840
4157
|
ColorHighlighter,
|
|
3841
4158
|
DataSourceBinder,
|
|
3842
4159
|
DependencyGraph,
|
|
4160
|
+
DirtyTracker,
|
|
3843
4161
|
EditorController,
|
|
3844
4162
|
FontSize,
|
|
3845
4163
|
FormulaEvaluator,
|
|
3846
4164
|
FormulaParser,
|
|
3847
4165
|
GradientText,
|
|
4166
|
+
HistoryManager,
|
|
3848
4167
|
INSTRUCTION_REGEX,
|
|
3849
4168
|
Image,
|
|
3850
4169
|
LoadingNode,
|
|
@@ -3857,9 +4176,12 @@ var SpreadsheetController = class {
|
|
|
3857
4176
|
TemplateSuggestions,
|
|
3858
4177
|
VARIABLE_REGEX,
|
|
3859
4178
|
colNameToIndex,
|
|
4179
|
+
createDirtyTracker,
|
|
3860
4180
|
createEditor,
|
|
4181
|
+
createHistoryManager,
|
|
3861
4182
|
createMentionExtension,
|
|
3862
4183
|
createRichTextPreset,
|
|
4184
|
+
defaultIsEqual,
|
|
3863
4185
|
escapeHtml,
|
|
3864
4186
|
extractCellDependencies,
|
|
3865
4187
|
extractInstructionNames,
|