@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
package/dist/index.d.cts
CHANGED
|
@@ -6,6 +6,7 @@ export { ExtractedMention, escapeHtml, extractMentionsFromDoc, getEditorMarkdown
|
|
|
6
6
|
import { Editor, EditorOptions, Content, Extensions } from '@tiptap/core';
|
|
7
7
|
export { Content, Editor, EditorOptions, Extensions } from '@tiptap/core';
|
|
8
8
|
export { ASTNode, BinaryOpNode, CellCoordinate, CellData, CellDep, CellFormat, CellRange, CellRefNode, ColumnBinding, ColumnType, DataSourceBinder, DependencyGraph, EvaluationContext, FormulaEvaluator, FormulaParser, FormulaToken, FormulaTokenType, FunctionCallNode, LiteralNode, PropertyRefNode, RangeNode, RowType, SpreadsheetColumn, SpreadsheetController, SpreadsheetControllerOptions, SpreadsheetDataSource, SpreadsheetDataSourceField, SpreadsheetDocument, SpreadsheetRow, UnaryOpNode, colNameToIndex, extractCellDependencies, indexToColName, parseCellReference, parseFormula, parseRange, tokenizeFormula } from './spreadsheet/index.cjs';
|
|
9
|
+
export { Command, HistoryManager, HistoryManagerOptions, HistoryState, MergedExecutionOptions, SnapshotCommandOptions, createHistoryManager } from './history/index.cjs';
|
|
9
10
|
export { MarkdownOptions } from 'tiptap-markdown';
|
|
10
11
|
import '@tiptap/pm/view';
|
|
11
12
|
import '@tiptap/pm/model';
|
|
@@ -40,6 +41,7 @@ declare class EditorController {
|
|
|
40
41
|
private readonly onMarkdownChange?;
|
|
41
42
|
private readonly onTokensChange?;
|
|
42
43
|
private readonly onSelectionChange?;
|
|
44
|
+
private baselineContent;
|
|
43
45
|
constructor(options?: EditorControllerOptions);
|
|
44
46
|
private enforcePlainTextClipboard;
|
|
45
47
|
private handleUpdate;
|
|
@@ -51,6 +53,11 @@ declare class EditorController {
|
|
|
51
53
|
insertText(text: string): void;
|
|
52
54
|
insertTextAt(pos: number, text: string): void;
|
|
53
55
|
replaceRange(from: number, to: number, text: string): void;
|
|
56
|
+
getCurrentContentString(): string;
|
|
57
|
+
isDirty(): boolean;
|
|
58
|
+
setCheckpoint(content?: string): void;
|
|
59
|
+
resetDirty(): void;
|
|
60
|
+
getBaselineContent(): string;
|
|
54
61
|
destroy(): void;
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
@@ -58,4 +65,41 @@ declare class EditorController {
|
|
|
58
65
|
*/
|
|
59
66
|
declare function createEditor(options?: EditorControllerOptions): EditorController;
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Compares two values for structural equality.
|
|
70
|
+
* Handles primitives, objects, arrays, and null/undefined.
|
|
71
|
+
*/
|
|
72
|
+
declare function defaultIsEqual<T>(a: T, b: T): boolean;
|
|
73
|
+
interface DirtyTrackerOptions<T> {
|
|
74
|
+
isEqual?: (a: T, b: T) => boolean;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Tracks a baseline value and determines if the current state is dirty (modified).
|
|
78
|
+
*/
|
|
79
|
+
declare class DirtyTracker<T = unknown> {
|
|
80
|
+
private baseline;
|
|
81
|
+
private readonly isEqual;
|
|
82
|
+
constructor(initialBaseline: T, options?: DirtyTrackerOptions<T> | ((a: T, b: T) => boolean));
|
|
83
|
+
/**
|
|
84
|
+
* Returns current baseline value.
|
|
85
|
+
*/
|
|
86
|
+
getBaseline(): T;
|
|
87
|
+
/**
|
|
88
|
+
* Sets a new baseline value without triggering any dirty state.
|
|
89
|
+
*/
|
|
90
|
+
setBaseline(value: T): void;
|
|
91
|
+
/**
|
|
92
|
+
* Checks whether the given value differs from the baseline.
|
|
93
|
+
*/
|
|
94
|
+
isDirty(current: T): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Resets the baseline to the provided current value (or clears dirty status).
|
|
97
|
+
*/
|
|
98
|
+
reset(current: T): void;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Creates a DirtyTracker instance.
|
|
102
|
+
*/
|
|
103
|
+
declare function createDirtyTracker<T>(initialBaseline: T, options?: DirtyTrackerOptions<T> | ((a: T, b: T) => boolean)): DirtyTracker<T>;
|
|
104
|
+
|
|
105
|
+
export { DirtyTracker, type DirtyTrackerOptions, EditorController, type EditorControllerOptions, Token, createDirtyTracker, createEditor, defaultIsEqual };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { ExtractedMention, escapeHtml, extractMentionsFromDoc, getEditorMarkdown
|
|
|
6
6
|
import { Editor, EditorOptions, Content, Extensions } from '@tiptap/core';
|
|
7
7
|
export { Content, Editor, EditorOptions, Extensions } from '@tiptap/core';
|
|
8
8
|
export { ASTNode, BinaryOpNode, CellCoordinate, CellData, CellDep, CellFormat, CellRange, CellRefNode, ColumnBinding, ColumnType, DataSourceBinder, DependencyGraph, EvaluationContext, FormulaEvaluator, FormulaParser, FormulaToken, FormulaTokenType, FunctionCallNode, LiteralNode, PropertyRefNode, RangeNode, RowType, SpreadsheetColumn, SpreadsheetController, SpreadsheetControllerOptions, SpreadsheetDataSource, SpreadsheetDataSourceField, SpreadsheetDocument, SpreadsheetRow, UnaryOpNode, colNameToIndex, extractCellDependencies, indexToColName, parseCellReference, parseFormula, parseRange, tokenizeFormula } from './spreadsheet/index.js';
|
|
9
|
+
export { Command, HistoryManager, HistoryManagerOptions, HistoryState, MergedExecutionOptions, SnapshotCommandOptions, createHistoryManager } from './history/index.js';
|
|
9
10
|
export { MarkdownOptions } from 'tiptap-markdown';
|
|
10
11
|
import '@tiptap/pm/view';
|
|
11
12
|
import '@tiptap/pm/model';
|
|
@@ -40,6 +41,7 @@ declare class EditorController {
|
|
|
40
41
|
private readonly onMarkdownChange?;
|
|
41
42
|
private readonly onTokensChange?;
|
|
42
43
|
private readonly onSelectionChange?;
|
|
44
|
+
private baselineContent;
|
|
43
45
|
constructor(options?: EditorControllerOptions);
|
|
44
46
|
private enforcePlainTextClipboard;
|
|
45
47
|
private handleUpdate;
|
|
@@ -51,6 +53,11 @@ declare class EditorController {
|
|
|
51
53
|
insertText(text: string): void;
|
|
52
54
|
insertTextAt(pos: number, text: string): void;
|
|
53
55
|
replaceRange(from: number, to: number, text: string): void;
|
|
56
|
+
getCurrentContentString(): string;
|
|
57
|
+
isDirty(): boolean;
|
|
58
|
+
setCheckpoint(content?: string): void;
|
|
59
|
+
resetDirty(): void;
|
|
60
|
+
getBaselineContent(): string;
|
|
54
61
|
destroy(): void;
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
@@ -58,4 +65,41 @@ declare class EditorController {
|
|
|
58
65
|
*/
|
|
59
66
|
declare function createEditor(options?: EditorControllerOptions): EditorController;
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Compares two values for structural equality.
|
|
70
|
+
* Handles primitives, objects, arrays, and null/undefined.
|
|
71
|
+
*/
|
|
72
|
+
declare function defaultIsEqual<T>(a: T, b: T): boolean;
|
|
73
|
+
interface DirtyTrackerOptions<T> {
|
|
74
|
+
isEqual?: (a: T, b: T) => boolean;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Tracks a baseline value and determines if the current state is dirty (modified).
|
|
78
|
+
*/
|
|
79
|
+
declare class DirtyTracker<T = unknown> {
|
|
80
|
+
private baseline;
|
|
81
|
+
private readonly isEqual;
|
|
82
|
+
constructor(initialBaseline: T, options?: DirtyTrackerOptions<T> | ((a: T, b: T) => boolean));
|
|
83
|
+
/**
|
|
84
|
+
* Returns current baseline value.
|
|
85
|
+
*/
|
|
86
|
+
getBaseline(): T;
|
|
87
|
+
/**
|
|
88
|
+
* Sets a new baseline value without triggering any dirty state.
|
|
89
|
+
*/
|
|
90
|
+
setBaseline(value: T): void;
|
|
91
|
+
/**
|
|
92
|
+
* Checks whether the given value differs from the baseline.
|
|
93
|
+
*/
|
|
94
|
+
isDirty(current: T): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Resets the baseline to the provided current value (or clears dirty status).
|
|
97
|
+
*/
|
|
98
|
+
reset(current: T): void;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Creates a DirtyTracker instance.
|
|
102
|
+
*/
|
|
103
|
+
declare function createDirtyTracker<T>(initialBaseline: T, options?: DirtyTrackerOptions<T> | ((a: T, b: T) => boolean)): DirtyTracker<T>;
|
|
104
|
+
|
|
105
|
+
export { DirtyTracker, type DirtyTrackerOptions, EditorController, type EditorControllerOptions, Token, createDirtyTracker, createEditor, defaultIsEqual };
|
package/dist/index.js
CHANGED
|
@@ -2358,6 +2358,7 @@ var EditorController = class {
|
|
|
2358
2358
|
onMarkdownChange;
|
|
2359
2359
|
onTokensChange;
|
|
2360
2360
|
onSelectionChange;
|
|
2361
|
+
baselineContent;
|
|
2361
2362
|
constructor(options = {}) {
|
|
2362
2363
|
const {
|
|
2363
2364
|
element,
|
|
@@ -2395,6 +2396,7 @@ var EditorController = class {
|
|
|
2395
2396
|
if (plainTextMode) {
|
|
2396
2397
|
this.enforcePlainTextClipboard();
|
|
2397
2398
|
}
|
|
2399
|
+
this.baselineContent = this.getCurrentContentString();
|
|
2398
2400
|
}
|
|
2399
2401
|
enforcePlainTextClipboard() {
|
|
2400
2402
|
this.editor.setOptions({
|
|
@@ -2468,6 +2470,23 @@ var EditorController = class {
|
|
|
2468
2470
|
replaceRange(from, to, text) {
|
|
2469
2471
|
this.editor.chain().focus().deleteRange({ from, to }).insertContentAt(from, text).run();
|
|
2470
2472
|
}
|
|
2473
|
+
getCurrentContentString() {
|
|
2474
|
+
if (this.plainTextMode) return this.getPlainText();
|
|
2475
|
+
if (this.markdownMode) return this.getMarkdown();
|
|
2476
|
+
return this.editor.getHTML();
|
|
2477
|
+
}
|
|
2478
|
+
isDirty() {
|
|
2479
|
+
return this.getCurrentContentString() !== this.baselineContent;
|
|
2480
|
+
}
|
|
2481
|
+
setCheckpoint(content) {
|
|
2482
|
+
this.baselineContent = content !== void 0 ? content : this.getCurrentContentString();
|
|
2483
|
+
}
|
|
2484
|
+
resetDirty() {
|
|
2485
|
+
this.setCheckpoint();
|
|
2486
|
+
}
|
|
2487
|
+
getBaselineContent() {
|
|
2488
|
+
return this.baselineContent;
|
|
2489
|
+
}
|
|
2471
2490
|
destroy() {
|
|
2472
2491
|
this.editor.destroy();
|
|
2473
2492
|
}
|
|
@@ -3750,15 +3769,310 @@ var SpreadsheetController = class {
|
|
|
3750
3769
|
this.onDocumentChange?.(this.document);
|
|
3751
3770
|
}
|
|
3752
3771
|
};
|
|
3772
|
+
|
|
3773
|
+
// src/dirty.ts
|
|
3774
|
+
function defaultIsEqual(a, b) {
|
|
3775
|
+
if (Object.is(a, b)) return true;
|
|
3776
|
+
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
|
|
3777
|
+
return false;
|
|
3778
|
+
}
|
|
3779
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
3780
|
+
if (a.length !== b.length) return false;
|
|
3781
|
+
for (let i = 0; i < a.length; i++) {
|
|
3782
|
+
if (!defaultIsEqual(a[i], b[i])) return false;
|
|
3783
|
+
}
|
|
3784
|
+
return true;
|
|
3785
|
+
}
|
|
3786
|
+
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
3787
|
+
return false;
|
|
3788
|
+
}
|
|
3789
|
+
const keysA = Object.keys(a);
|
|
3790
|
+
const keysB = Object.keys(b);
|
|
3791
|
+
if (keysA.length !== keysB.length) return false;
|
|
3792
|
+
for (const key of keysA) {
|
|
3793
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
3794
|
+
if (!defaultIsEqual(a[key], b[key])) return false;
|
|
3795
|
+
}
|
|
3796
|
+
return true;
|
|
3797
|
+
}
|
|
3798
|
+
var DirtyTracker = class {
|
|
3799
|
+
baseline;
|
|
3800
|
+
isEqual;
|
|
3801
|
+
constructor(initialBaseline, options) {
|
|
3802
|
+
this.baseline = initialBaseline;
|
|
3803
|
+
if (typeof options === "function") {
|
|
3804
|
+
this.isEqual = options;
|
|
3805
|
+
} else {
|
|
3806
|
+
this.isEqual = options?.isEqual ?? defaultIsEqual;
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
/**
|
|
3810
|
+
* Returns current baseline value.
|
|
3811
|
+
*/
|
|
3812
|
+
getBaseline() {
|
|
3813
|
+
return this.baseline;
|
|
3814
|
+
}
|
|
3815
|
+
/**
|
|
3816
|
+
* Sets a new baseline value without triggering any dirty state.
|
|
3817
|
+
*/
|
|
3818
|
+
setBaseline(value) {
|
|
3819
|
+
this.baseline = value;
|
|
3820
|
+
}
|
|
3821
|
+
/**
|
|
3822
|
+
* Checks whether the given value differs from the baseline.
|
|
3823
|
+
*/
|
|
3824
|
+
isDirty(current) {
|
|
3825
|
+
return !this.isEqual(this.baseline, current);
|
|
3826
|
+
}
|
|
3827
|
+
/**
|
|
3828
|
+
* Resets the baseline to the provided current value (or clears dirty status).
|
|
3829
|
+
*/
|
|
3830
|
+
reset(current) {
|
|
3831
|
+
this.baseline = current;
|
|
3832
|
+
}
|
|
3833
|
+
};
|
|
3834
|
+
function createDirtyTracker(initialBaseline, options) {
|
|
3835
|
+
return new DirtyTracker(initialBaseline, options);
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
// src/history/HistoryManager.ts
|
|
3839
|
+
var HistoryManager = class {
|
|
3840
|
+
undoStack = [];
|
|
3841
|
+
redoStack = [];
|
|
3842
|
+
maxDepth;
|
|
3843
|
+
isApplying = false;
|
|
3844
|
+
batchDepth = 0;
|
|
3845
|
+
currentBatch = null;
|
|
3846
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3847
|
+
onStateChange;
|
|
3848
|
+
cachedState;
|
|
3849
|
+
constructor(options = {}) {
|
|
3850
|
+
this.maxDepth = options.maxDepth ?? 50;
|
|
3851
|
+
this.onStateChange = options.onStateChange;
|
|
3852
|
+
this.cachedState = this.computeState();
|
|
3853
|
+
}
|
|
3854
|
+
computeState() {
|
|
3855
|
+
const canUndo = this.undoStack.length > 0;
|
|
3856
|
+
const canRedo = this.redoStack.length > 0;
|
|
3857
|
+
return {
|
|
3858
|
+
canUndo,
|
|
3859
|
+
canRedo,
|
|
3860
|
+
undoName: canUndo ? this.undoStack[this.undoStack.length - 1].name : "",
|
|
3861
|
+
redoName: canRedo ? this.redoStack[this.redoStack.length - 1].name : "",
|
|
3862
|
+
undoCount: this.undoStack.length,
|
|
3863
|
+
redoCount: this.redoStack.length
|
|
3864
|
+
};
|
|
3865
|
+
}
|
|
3866
|
+
/**
|
|
3867
|
+
* Returns a snapshot of the current history state.
|
|
3868
|
+
* Cached to maintain referential equality across renders for useSyncExternalStore.
|
|
3869
|
+
*/
|
|
3870
|
+
getState() {
|
|
3871
|
+
return this.cachedState;
|
|
3872
|
+
}
|
|
3873
|
+
notify() {
|
|
3874
|
+
this.cachedState = this.computeState();
|
|
3875
|
+
const state = this.cachedState;
|
|
3876
|
+
this.onStateChange?.(state);
|
|
3877
|
+
this.listeners.forEach((listener) => {
|
|
3878
|
+
try {
|
|
3879
|
+
listener(state);
|
|
3880
|
+
} catch (err) {
|
|
3881
|
+
console.error("[HistoryManager] Error in state listener:", err);
|
|
3882
|
+
}
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
/**
|
|
3886
|
+
* Executes a command and pushes it onto the undo stack.
|
|
3887
|
+
* Clears the redo stack.
|
|
3888
|
+
*/
|
|
3889
|
+
execute(command) {
|
|
3890
|
+
if (this.isApplying) {
|
|
3891
|
+
command.execute();
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
if (this.batchDepth > 0 && this.currentBatch) {
|
|
3895
|
+
command.execute();
|
|
3896
|
+
this.currentBatch.push({
|
|
3897
|
+
...command,
|
|
3898
|
+
timestamp: command.timestamp ?? Date.now()
|
|
3899
|
+
});
|
|
3900
|
+
return;
|
|
3901
|
+
}
|
|
3902
|
+
command.execute();
|
|
3903
|
+
this.undoStack.push({
|
|
3904
|
+
...command,
|
|
3905
|
+
timestamp: command.timestamp ?? Date.now()
|
|
3906
|
+
});
|
|
3907
|
+
if (this.undoStack.length > this.maxDepth) {
|
|
3908
|
+
this.undoStack.shift();
|
|
3909
|
+
}
|
|
3910
|
+
this.redoStack = [];
|
|
3911
|
+
this.notify();
|
|
3912
|
+
}
|
|
3913
|
+
/**
|
|
3914
|
+
* Executes a command, coalescing it with the previous command if they share
|
|
3915
|
+
* the same `tag` and occurred within `windowMs` (default: 300ms).
|
|
3916
|
+
* Useful for high-frequency interactions like slider scrubs or node drags.
|
|
3917
|
+
*/
|
|
3918
|
+
executeMerged(command, options = {}) {
|
|
3919
|
+
const windowMs = options.windowMs ?? 300;
|
|
3920
|
+
const now = Date.now();
|
|
3921
|
+
if (command.tag && this.undoStack.length > 0 && this.batchDepth === 0 && !this.isApplying) {
|
|
3922
|
+
const top = this.undoStack[this.undoStack.length - 1];
|
|
3923
|
+
const elapsed = top.timestamp ? now - top.timestamp : Infinity;
|
|
3924
|
+
if (top.tag === command.tag && elapsed <= windowMs) {
|
|
3925
|
+
command.execute();
|
|
3926
|
+
const initialUndo = top.undo;
|
|
3927
|
+
this.undoStack[this.undoStack.length - 1] = {
|
|
3928
|
+
name: command.name,
|
|
3929
|
+
tag: command.tag,
|
|
3930
|
+
timestamp: now,
|
|
3931
|
+
execute: command.execute,
|
|
3932
|
+
undo: initialUndo,
|
|
3933
|
+
redo: command.redo ?? command.execute
|
|
3934
|
+
};
|
|
3935
|
+
this.redoStack = [];
|
|
3936
|
+
this.notify();
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
this.execute({ ...command, timestamp: now });
|
|
3941
|
+
}
|
|
3942
|
+
/**
|
|
3943
|
+
* Batches multiple commands executed within `fn` into a single atomic undo entry.
|
|
3944
|
+
*/
|
|
3945
|
+
batch(name, fn) {
|
|
3946
|
+
if (this.isApplying) {
|
|
3947
|
+
fn();
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
if (this.batchDepth > 0) {
|
|
3951
|
+
fn();
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
this.batchDepth++;
|
|
3955
|
+
this.currentBatch = [];
|
|
3956
|
+
try {
|
|
3957
|
+
fn();
|
|
3958
|
+
} finally {
|
|
3959
|
+
this.batchDepth--;
|
|
3960
|
+
const batchCommands = this.currentBatch;
|
|
3961
|
+
this.currentBatch = null;
|
|
3962
|
+
if (batchCommands && batchCommands.length > 0) {
|
|
3963
|
+
const compoundCommand = {
|
|
3964
|
+
name,
|
|
3965
|
+
timestamp: Date.now(),
|
|
3966
|
+
execute: () => {
|
|
3967
|
+
for (let i = 0; i < batchCommands.length; i++) {
|
|
3968
|
+
(batchCommands[i].redo ?? batchCommands[i].execute)();
|
|
3969
|
+
}
|
|
3970
|
+
},
|
|
3971
|
+
undo: () => {
|
|
3972
|
+
for (let i = batchCommands.length - 1; i >= 0; i--) {
|
|
3973
|
+
batchCommands[i].undo();
|
|
3974
|
+
}
|
|
3975
|
+
},
|
|
3976
|
+
redo: () => {
|
|
3977
|
+
for (let i = 0; i < batchCommands.length; i++) {
|
|
3978
|
+
(batchCommands[i].redo ?? batchCommands[i].execute)();
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
};
|
|
3982
|
+
this.undoStack.push(compoundCommand);
|
|
3983
|
+
if (this.undoStack.length > this.maxDepth) {
|
|
3984
|
+
this.undoStack.shift();
|
|
3985
|
+
}
|
|
3986
|
+
this.redoStack = [];
|
|
3987
|
+
this.notify();
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Reverts the most recent command on the undo stack.
|
|
3993
|
+
* Returns true if a command was undone, false otherwise.
|
|
3994
|
+
*/
|
|
3995
|
+
undo() {
|
|
3996
|
+
if (this.undoStack.length === 0 || this.isApplying || this.batchDepth > 0) {
|
|
3997
|
+
return false;
|
|
3998
|
+
}
|
|
3999
|
+
this.isApplying = true;
|
|
4000
|
+
try {
|
|
4001
|
+
const command = this.undoStack.pop();
|
|
4002
|
+
command.undo();
|
|
4003
|
+
this.redoStack.push(command);
|
|
4004
|
+
return true;
|
|
4005
|
+
} finally {
|
|
4006
|
+
this.isApplying = false;
|
|
4007
|
+
this.notify();
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
/**
|
|
4011
|
+
* Re-applies the most recent undone command on the redo stack.
|
|
4012
|
+
* Returns true if a command was redone, false otherwise.
|
|
4013
|
+
*/
|
|
4014
|
+
redo() {
|
|
4015
|
+
if (this.redoStack.length === 0 || this.isApplying || this.batchDepth > 0) {
|
|
4016
|
+
return false;
|
|
4017
|
+
}
|
|
4018
|
+
this.isApplying = true;
|
|
4019
|
+
try {
|
|
4020
|
+
const command = this.redoStack.pop();
|
|
4021
|
+
(command.redo ?? command.execute)();
|
|
4022
|
+
this.undoStack.push(command);
|
|
4023
|
+
return true;
|
|
4024
|
+
} finally {
|
|
4025
|
+
this.isApplying = false;
|
|
4026
|
+
this.notify();
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
/**
|
|
4030
|
+
* Clears both undo and redo stacks.
|
|
4031
|
+
*/
|
|
4032
|
+
clear() {
|
|
4033
|
+
this.undoStack = [];
|
|
4034
|
+
this.redoStack = [];
|
|
4035
|
+
this.notify();
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Subscribes to history state changes.
|
|
4039
|
+
* The listener is immediately invoked with the current state.
|
|
4040
|
+
* Returns an unsubscribe function.
|
|
4041
|
+
*/
|
|
4042
|
+
subscribe(listener) {
|
|
4043
|
+
this.listeners.add(listener);
|
|
4044
|
+
listener(this.getState());
|
|
4045
|
+
return () => {
|
|
4046
|
+
this.listeners.delete(listener);
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
/**
|
|
4050
|
+
* Utility helper to create a Command from before/after state snapshots.
|
|
4051
|
+
*/
|
|
4052
|
+
static createSnapshotCommand(options) {
|
|
4053
|
+
return {
|
|
4054
|
+
name: options.name,
|
|
4055
|
+
tag: options.tag,
|
|
4056
|
+
execute: () => options.apply(options.after),
|
|
4057
|
+
undo: () => options.apply(options.before),
|
|
4058
|
+
redo: () => options.apply(options.after)
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
function createHistoryManager(options = {}) {
|
|
4063
|
+
return new HistoryManager(options);
|
|
4064
|
+
}
|
|
3753
4065
|
export {
|
|
3754
4066
|
ColorHighlighter,
|
|
3755
4067
|
DataSourceBinder,
|
|
3756
4068
|
DependencyGraph,
|
|
4069
|
+
DirtyTracker,
|
|
3757
4070
|
EditorController,
|
|
3758
4071
|
FontSize,
|
|
3759
4072
|
FormulaEvaluator,
|
|
3760
4073
|
FormulaParser,
|
|
3761
4074
|
GradientText,
|
|
4075
|
+
HistoryManager,
|
|
3762
4076
|
INSTRUCTION_REGEX,
|
|
3763
4077
|
Image2 as Image,
|
|
3764
4078
|
LoadingNode,
|
|
@@ -3771,9 +4085,12 @@ export {
|
|
|
3771
4085
|
TemplateSuggestions,
|
|
3772
4086
|
VARIABLE_REGEX,
|
|
3773
4087
|
colNameToIndex,
|
|
4088
|
+
createDirtyTracker,
|
|
3774
4089
|
createEditor,
|
|
4090
|
+
createHistoryManager,
|
|
3775
4091
|
createMentionExtension,
|
|
3776
4092
|
createRichTextPreset,
|
|
4093
|
+
defaultIsEqual,
|
|
3777
4094
|
escapeHtml,
|
|
3778
4095
|
extractCellDependencies,
|
|
3779
4096
|
extractInstructionNames,
|