@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/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
- export { EditorController, type EditorControllerOptions, Token, createEditor };
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
- export { EditorController, type EditorControllerOptions, Token, createEditor };
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,