@anth0nycodes/fabric-history 0.1.4 → 0.3.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/README.md CHANGED
@@ -6,7 +6,9 @@ A library built on top of fabric.js that provides undo/redo history management f
6
6
 
7
7
  - Undo/Redo functionality for fabric.js canvases
8
8
  - Automatic history tracking for object additions, removals, and modifications
9
+ - Multi-selection batching: operations on multiple selected objects are recorded as a single history entry
9
10
  - Support for path creation and erasing events
11
+ - Custom events for history state changes
10
12
  - Easy integration with existing fabric.js projects
11
13
  - Support for fabric.js v6 and v7
12
14
 
@@ -57,6 +59,28 @@ await canvas.undo();
57
59
  await canvas.redo();
58
60
  ```
59
61
 
62
+ ### Using with `@erase2d/fabric`
63
+
64
+ To enable history tracking for erasing operations, use the `setEraserBrush` method with an `EraserBrush` from [`@erase2d/fabric`](https://github.com/erase2d/fabric). This ensures that erasing actions trigger the `erasing:end` event required for history tracking.
65
+
66
+ ```typescript
67
+ import { CanvasWithHistory } from "@anth0nycodes/fabric-history";
68
+ import { EraserBrush } from "@erase2d/fabric";
69
+
70
+ const canvas = new CanvasWithHistory("my-canvas", {
71
+ width: 800,
72
+ height: 600,
73
+ });
74
+
75
+ // Create and set the eraser brush
76
+ const eraser = new EraserBrush(canvas);
77
+ eraser.width = 20;
78
+ canvas.setEraserBrush(eraser);
79
+
80
+ // Enable drawing mode to use the eraser
81
+ canvas.isDrawingMode = true;
82
+ ```
83
+
60
84
  ## API
61
85
 
62
86
  ### `CanvasWithHistory`
@@ -65,13 +89,16 @@ Extends fabric.js `Canvas` class with history management capabilities.
65
89
 
66
90
  #### Methods
67
91
 
68
- | Method | Returns | Description |
69
- | ----------- | --------------- | ----------------------------------------------- |
70
- | `undo()` | `Promise<void>` | Undo the most recent action |
71
- | `redo()` | `Promise<void>` | Redo the most recently undone action |
72
- | `canUndo()` | `boolean` | Check if an undo action is available |
73
- | `canRedo()` | `boolean` | Check if a redo action is available |
74
- | `dispose()` | `void` | Clean up event listeners and dispose the canvas |
92
+ | Method | Returns | Description |
93
+ | ------------------------ | --------------- | ---------------------------------------------------------------------------------------------------- |
94
+ | `undo()` | `Promise<void>` | Undo the most recent action |
95
+ | `redo()` | `Promise<void>` | Redo the most recently undone action |
96
+ | `canUndo()` | `boolean` | Check if an undo action is available |
97
+ | `canRedo()` | `boolean` | Check if a redo action is available |
98
+ | `setEraserBrush(eraser)` | `void` | Set an `EraserBrush` from `@erase2d/fabric` to enable history tracking for erasing operations |
99
+ | `clearHistory()` | `void` | Clear the undo and redo history stacks |
100
+ | `clearCanvas()` | `void` | Clear the canvas and save the cleared state to history (use this instead of the inherited `clear()`) |
101
+ | `dispose()` | `void` | Clean up event listeners and dispose the canvas |
75
102
 
76
103
  #### Tracked Events
77
104
 
@@ -84,6 +111,29 @@ History is automatically saved when these fabric.js events occur:
84
111
  - `erasing:end` - When an erasing operation completes
85
112
  - `canvas:cleared` - When the canvas is cleared
86
113
 
114
+ #### Custom Events
115
+
116
+ `CanvasWithHistory` fires custom events that you can listen to for history state changes:
117
+
118
+ | Event | Payload | Description |
119
+ | ----------------- | ------------------------------------ | --------------------------------- |
120
+ | `history:append` | `{ json: string, initial: boolean }` | Fired when a state is saved |
121
+ | `history:undo` | `{ lastUndoAction: string }` | Fired when an undo is performed |
122
+ | `history:redo` | `{ lastRedoAction: string }` | Fired when a redo is performed |
123
+ | `history:cleared` | `{}` | Fired when history stacks cleared |
124
+
125
+ **Example:**
126
+
127
+ ```typescript
128
+ canvas.on("history:append", ({ json, initial }) => {
129
+ console.log("State saved:", initial ? "initial" : "action");
130
+ });
131
+
132
+ canvas.on("history:undo", ({ lastUndoAction }) => {
133
+ console.log("Undo performed");
134
+ });
135
+ ```
136
+
87
137
  ## Requirements
88
138
 
89
139
  - fabric.js 6.x or 7.x
@@ -103,9 +153,15 @@ pnpm build
103
153
  # Type check
104
154
  pnpm check
105
155
 
106
- # Run tests
156
+ # Run all tests
107
157
  pnpm test
108
158
 
159
+ # Run integration tests only
160
+ pnpm test:it
161
+
162
+ # Run E2E tests only (uses Playwright)
163
+ pnpm test:e2e
164
+
109
165
  # Run tests with coverage
110
166
  pnpm coverage
111
167
  ```
package/dist/canvas.d.ts CHANGED
@@ -1,8 +1,38 @@
1
- import { Canvas } from "fabric";
1
+ import type { EraserBrush } from "@erase2d/fabric";
2
+ import { Canvas, type TEvent } from "fabric";
3
+ declare module "fabric" {
4
+ interface CanvasEvents {
5
+ "history:append": Partial<TEvent> & {
6
+ /**
7
+ * Serialized canvas state that was saved to history.
8
+ */
9
+ json: string;
10
+ /**
11
+ * Boolean flag indicating whether or not the appended history action is the initial state of the canvas.
12
+ */
13
+ initial: boolean;
14
+ };
15
+ "history:undo": Partial<TEvent> & {
16
+ /**
17
+ * Serialized canvas state that was most recently undone.
18
+ */
19
+ lastUndoAction: string;
20
+ };
21
+ "history:redo": Partial<TEvent> & {
22
+ /**
23
+ * Serialized canvas state that was most recently redone.
24
+ */
25
+ lastRedoAction: string;
26
+ };
27
+ "history:cleared": Partial<TEvent>;
28
+ }
29
+ }
2
30
  export declare class CanvasWithHistory extends Canvas {
3
31
  private _historyUndo;
4
32
  private _historyRedo;
5
- private _isMoving;
33
+ private _selectedObjects;
34
+ private _isMultiSelection;
35
+ private _historyIsMoving;
6
36
  private _historyProcessing;
7
37
  private _historyCurrentState;
8
38
  constructor(...args: ConstructorParameters<typeof Canvas>);
@@ -10,14 +40,34 @@ export declare class CanvasWithHistory extends Canvas {
10
40
  * Binds all relevant fabric event listeners.
11
41
  */
12
42
  private _bindEventListeners;
43
+ /**
44
+ * Stores the multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true.
45
+ *
46
+ * @param options - The options object containing the selected objects.
47
+ */
48
+ private _handleSelectionCreated;
49
+ /**
50
+ * Stores the updated multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true if there are more than 1 objects selected.
51
+ */
52
+ private _handleSelectionUpdated;
53
+ /**
54
+ * Clears the multi-selection state and sets the `_isMultiSelection` flag to false.
55
+ */
56
+ private _handleSelectionCleared;
13
57
  /**
14
58
  * Saves the initial state of the canvas.
15
59
  */
16
- private _saveInitialState;
60
+ private _historySaveInitialState;
61
+ /**
62
+ * Handles object removal events.
63
+ *
64
+ * @param options - The options object containing details about the removed object.
65
+ */
66
+ private _handleObjectRemoved;
17
67
  /**
18
68
  * Starts the movement event listener for objects.
19
69
  */
20
- private _objectMoving;
70
+ private _handleObjectMoving;
21
71
  /**
22
72
  * Handles object modification events, including moving, resizing, rotating,
23
73
  * scaling, and skewing.
@@ -32,7 +82,7 @@ export declare class CanvasWithHistory extends Canvas {
32
82
  */
33
83
  private _historyCurrent;
34
84
  /**
35
- * Records the current canvas, object, or path state into the history stack for undo/redo.
85
+ * Records the current state of the canvas to the history stack if the state has changed since the last recorded state. This method is called after relevant canvas events such as object modifications, additions, and removals.
36
86
  */
37
87
  private _historySaveAction;
38
88
  /**
@@ -58,13 +108,34 @@ export declare class CanvasWithHistory extends Canvas {
58
108
  */
59
109
  private _loadFromHistory;
60
110
  /**
61
- * Clears the history stacks for undo and redo.
111
+ * Debug method to log relevant events to the console. Always remember to remove before pushing once you're done debugging locally!
62
112
  */
63
- private _clearHistory;
113
+ private _historyDebug;
64
114
  /**
65
115
  * Unsubscribes all relevant fabric event listeners.
66
116
  */
67
117
  private _disposeEventListeners;
118
+ /**
119
+ * Sets the provided `EraserBrush` instance as the canvas's eraser brush. This method is necessary to ensure that erasing actions are properly recorded in the history stack, as the default Fabric.js eraser brush does not trigger the relevant events for history tracking.
120
+ *
121
+ * @param eraser - The EraserBrush instance to set as the canvas's eraser brush.
122
+ */
123
+ setEraserBrush(eraser: EraserBrush): void;
124
+ /**
125
+ * Clears the history stacks for undo and redo.
126
+ */
127
+ clearHistory(): void;
128
+ /**
129
+ * Cleans up event listeners and history stacks before disposing of the canvas instance.
130
+ */
68
131
  dispose(): Promise<boolean>;
132
+ /**
133
+ * Clears the canvas and saves the cleared state to history.
134
+ *
135
+ * @remarks
136
+ * When using `CanvasWithHistory`, use this method instead of `clear()`.
137
+ * The inherited `clear()` method does not record to history.
138
+ */
139
+ clearCanvas(): void;
69
140
  }
70
141
  //# sourceMappingURL=canvas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,qBAAa,iBAAkB,SAAQ,MAAM;IAE3C,OAAO,CAAC,YAAY,CAAW;IAC/B,OAAO,CAAC,YAAY,CAAW;IAG/B,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,kBAAkB,CAAU;IAEpC,OAAO,CAAC,oBAAoB,CAAS;gBAEzB,GAAG,IAAI,EAAE,qBAAqB,CAAC,OAAO,MAAM,CAAC;IAazD;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAY3B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAMzB;;OAEG;IACH,OAAO,CAAC,aAAa;IAIrB;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAM7B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAIvB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;OAEG;IACG,IAAI;IAgBV;;OAEG;IACH,OAAO;IAIP;;OAEG;IACG,IAAI;IAcV;;OAEG;IACH,OAAO;IAIP;;;;OAIG;YACW,gBAAgB;IAe9B;;OAEG;IACH,OAAO,CAAC,aAAa;IAKrB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAY9B,OAAO;CAKR"}
1
+ {"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,iBAAiB,CAAC;AACjE,OAAO,EACL,MAAM,EAGN,KAAK,MAAM,EACZ,MAAM,QAAQ,CAAC;AAGhB,OAAO,QAAQ,QAAQ,CAAC;IACtB,UAAU,YAAY;QACpB,gBAAgB,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG;YAClC;;eAEG;YACH,IAAI,EAAE,MAAM,CAAC;YACb;;eAEG;YACH,OAAO,EAAE,OAAO,CAAC;SAClB,CAAC;QACF,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG;YAChC;;eAEG;YACH,cAAc,EAAE,MAAM,CAAC;SACxB,CAAC;QACF,cAAc,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG;YAChC;;eAEG;YACH,cAAc,EAAE,MAAM,CAAC;SACxB,CAAC;QACF,iBAAiB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;KACpC;CACF;AAED,qBAAa,iBAAkB,SAAQ,MAAM;IAE3C,OAAO,CAAC,YAAY,CAAW;IAC/B,OAAO,CAAC,YAAY,CAAW;IAG/B,OAAO,CAAC,gBAAgB,CAAiB;IACzC,OAAO,CAAC,iBAAiB,CAAU;IAGnC,OAAO,CAAC,gBAAgB,CAAU;IAClC,OAAO,CAAC,kBAAkB,CAAU;IAEpC,OAAO,CAAC,oBAAoB,CAAS;gBAEzB,GAAG,IAAI,EAAE,qBAAqB,CAAC,OAAO,MAAM,CAAC;IAezD;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAc3B;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAS/B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAW/B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAK/B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAMhC;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAuB5B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAI3B;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAM7B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAIvB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;OAEG;IACG,IAAI;IAiBV;;OAEG;IACH,OAAO;IAIP;;OAEG;IACG,IAAI;IAeV;;OAEG;IACH,OAAO;IAIP;;;;OAIG;YACW,gBAAgB;IAa9B;;OAEG;IACH,OAAO,CAAC,aAAa;IAwBrB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAc9B;;;;OAIG;IACH,cAAc,CAAC,MAAM,EAAE,WAAW;IAclC;;OAEG;IACH,YAAY;IAMZ;;OAEG;IACH,OAAO;IAMP;;;;;;OAMG;IACH,WAAW;CAMZ"}
package/dist/canvas.js CHANGED
@@ -1,21 +1,26 @@
1
- import { Canvas } from "fabric";
1
+ import { Canvas, } from "fabric";
2
2
  export class CanvasWithHistory extends Canvas {
3
- // History stacks
3
+ // History stack properties
4
4
  _historyUndo;
5
5
  _historyRedo;
6
- // Boolean values to determine whether or not we should save to history
7
- _isMoving;
6
+ // Multi-selection properties
7
+ _selectedObjects;
8
+ _isMultiSelection;
9
+ // Boolean properties to determine whether or not we should save to history
10
+ _historyIsMoving;
8
11
  _historyProcessing;
9
12
  _historyCurrentState;
10
13
  constructor(...args) {
11
14
  super(...args);
12
15
  this._historyUndo = [];
13
16
  this._historyRedo = [];
14
- this._isMoving = false;
17
+ this._selectedObjects = [];
18
+ this._isMultiSelection = false;
19
+ this._historyIsMoving = false;
15
20
  this._historyProcessing = false;
16
21
  this._historyCurrentState = this._historyCurrent();
17
22
  this._bindEventListeners();
18
- this._saveInitialState();
23
+ this._historySaveInitialState();
19
24
  }
20
25
  /**
21
26
  * Binds all relevant fabric event listeners.
@@ -25,25 +30,86 @@ export class CanvasWithHistory extends Canvas {
25
30
  "path:created": this._historySaveAction.bind(this),
26
31
  "erasing:end": this._historySaveAction.bind(this),
27
32
  "object:added": this._historySaveAction.bind(this),
28
- "object:removed": this._historySaveAction.bind(this), // TODO: handle object modification + deletion batching
29
- "object:moving": this._objectMoving.bind(this),
33
+ "object:removed": this._handleObjectRemoved.bind(this),
34
+ "object:moving": this._handleObjectMoving.bind(this),
30
35
  "object:modified": this._handleObjectModified.bind(this),
31
- "canvas:cleared": this._historySaveAction.bind(this),
36
+ "selection:created": this._handleSelectionCreated.bind(this),
37
+ "selection:updated": this._handleSelectionUpdated.bind(this),
38
+ "selection:cleared": this._handleSelectionCleared.bind(this),
32
39
  });
33
40
  }
41
+ /**
42
+ * Stores the multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true.
43
+ *
44
+ * @param options - The options object containing the selected objects.
45
+ */
46
+ _handleSelectionCreated(options) {
47
+ const currentSelectedObjects = options.selected;
48
+ if (currentSelectedObjects.length > 1) {
49
+ this._selectedObjects = currentSelectedObjects;
50
+ this._isMultiSelection = true;
51
+ }
52
+ }
53
+ /**
54
+ * Stores the updated multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true if there are more than 1 objects selected.
55
+ */
56
+ _handleSelectionUpdated() {
57
+ const allSelectedObjects = this.getActiveObjects();
58
+ /*
59
+ Uses reference to `getActiveObjects()` instead of the callback's `options.selected` because `options.selected`
60
+ only contains the objects that are currently selected, it doesn't cover all the previously
61
+ selected objects that are in the current ActiveSelection.
62
+ */
63
+ this._selectedObjects = allSelectedObjects;
64
+ this._isMultiSelection = allSelectedObjects.length > 1;
65
+ }
66
+ /**
67
+ * Clears the multi-selection state and sets the `_isMultiSelection` flag to false.
68
+ */
69
+ _handleSelectionCleared() {
70
+ this._selectedObjects = [];
71
+ this._isMultiSelection = false;
72
+ }
34
73
  /**
35
74
  * Saves the initial state of the canvas.
36
75
  */
37
- _saveInitialState() {
76
+ _historySaveInitialState() {
38
77
  const initialState = this._historyCurrent();
39
78
  this._historyUndo = [initialState];
40
79
  this._historyCurrentState = initialState;
41
80
  }
81
+ /**
82
+ * Handles object removal events.
83
+ *
84
+ * @param options - The options object containing details about the removed object.
85
+ */
86
+ _handleObjectRemoved(options) {
87
+ // handle object:removed events differently based on whether or not the removed object is within an ActiveSelection
88
+ if (
89
+ /*
90
+ !this._historyProcessing is included to prevent unintended recursive behavior
91
+ caused by this.remove() since that also triggers an object:removed event
92
+ */
93
+ !this._historyProcessing &&
94
+ this._isMultiSelection &&
95
+ this._selectedObjects.includes(options.target)) {
96
+ this._historyProcessing = true;
97
+ const objectsToRemove = [...this._selectedObjects];
98
+ this._selectedObjects = [];
99
+ this.remove(...objectsToRemove);
100
+ this.discardActiveObject();
101
+ this._historyProcessing = false;
102
+ this._historySaveAction();
103
+ }
104
+ else {
105
+ this._historySaveAction();
106
+ }
107
+ }
42
108
  /**
43
109
  * Starts the movement event listener for objects.
44
110
  */
45
- _objectMoving() {
46
- this._isMoving = true;
111
+ _handleObjectMoving() {
112
+ this._historyIsMoving = true;
47
113
  }
48
114
  /**
49
115
  * Handles object modification events, including moving, resizing, rotating,
@@ -53,7 +119,7 @@ export class CanvasWithHistory extends Canvas {
53
119
  */
54
120
  _handleObjectModified() {
55
121
  // object:moving -> object:modified - modification is triggered as soon as the movement of an object halts
56
- this._isMoving = false;
122
+ this._historyIsMoving = false;
57
123
  this._historySaveAction();
58
124
  }
59
125
  /**
@@ -65,17 +131,18 @@ export class CanvasWithHistory extends Canvas {
65
131
  return JSON.stringify(this.toDatalessJSON());
66
132
  }
67
133
  /**
68
- * Records the current canvas, object, or path state into the history stack for undo/redo.
134
+ * Records the current state of the canvas to the history stack if the state has changed since the last recorded state. This method is called after relevant canvas events such as object modifications, additions, and removals.
69
135
  */
70
136
  _historySaveAction() {
71
- if (this._historyProcessing || this._isMoving)
137
+ if (this._historyProcessing || this._historyIsMoving)
72
138
  return;
73
139
  const latestJSON = this._historyCurrent();
74
140
  if (this._historyCurrentState === latestJSON)
75
- return;
141
+ return; // skips duplicates
76
142
  this._historyUndo.push(latestJSON);
77
143
  this._historyCurrentState = latestJSON;
78
144
  this._historyRedo = [];
145
+ this.fire("history:append", { json: latestJSON, initial: false });
79
146
  }
80
147
  /**
81
148
  * Undo the most recent action.
@@ -94,6 +161,7 @@ export class CanvasWithHistory extends Canvas {
94
161
  return;
95
162
  this._historyCurrentState = previousState;
96
163
  await this._loadFromHistory(previousState);
164
+ this.fire("history:undo", { lastUndoAction: poppedState });
97
165
  }
98
166
  /**
99
167
  * Checks for whether or not an action can be undone.
@@ -115,6 +183,7 @@ export class CanvasWithHistory extends Canvas {
115
183
  // refresh canvas to load the popped state
116
184
  this._historyCurrentState = poppedState;
117
185
  await this._loadFromHistory(poppedState);
186
+ this.fire("history:redo", { lastRedoAction: poppedState });
118
187
  }
119
188
  /**
120
189
  * Checks for whether or not an action can be redone.
@@ -128,12 +197,11 @@ export class CanvasWithHistory extends Canvas {
128
197
  * @param historyState - The JSON string representing the canvas history state to load.
129
198
  */
130
199
  async _loadFromHistory(historyState) {
131
- this.clear();
132
- this.discardActiveObject();
133
200
  try {
134
- const parsed = JSON.parse(historyState);
135
- await this.loadFromJSON(parsed);
136
- this.renderAll();
201
+ this.clear();
202
+ this.discardActiveObject();
203
+ await this.loadFromJSON(JSON.parse(historyState));
204
+ this.requestRenderAll();
137
205
  }
138
206
  catch (error) {
139
207
  console.error("Error loading from history:", error);
@@ -143,11 +211,27 @@ export class CanvasWithHistory extends Canvas {
143
211
  }
144
212
  }
145
213
  /**
146
- * Clears the history stacks for undo and redo.
214
+ * Debug method to log relevant events to the console. Always remember to remove before pushing once you're done debugging locally!
147
215
  */
148
- _clearHistory() {
149
- this._historyUndo = [];
150
- this._historyRedo = [];
216
+ _historyDebug() {
217
+ const EVENTS = [
218
+ "path:created",
219
+ "erasing:end",
220
+ "object:added",
221
+ "object:removed",
222
+ "object:moving",
223
+ "object:modified",
224
+ "selection:created",
225
+ "selection:cleared",
226
+ "canvas:cleared",
227
+ "history:append",
228
+ "history:undo",
229
+ "history:redo",
230
+ "history:cleared",
231
+ ];
232
+ EVENTS.forEach((e) => {
233
+ this.on(e, () => console.log(`📝 Event triggered: ${e}`));
234
+ });
151
235
  }
152
236
  /**
153
237
  * Unsubscribes all relevant fabric event listeners.
@@ -157,16 +241,59 @@ export class CanvasWithHistory extends Canvas {
157
241
  "path:created": this._historySaveAction.bind(this),
158
242
  "erasing:end": this._historySaveAction.bind(this),
159
243
  "object:added": this._historySaveAction.bind(this),
160
- "object:removed": this._historySaveAction.bind(this),
161
- "object:moving": this._objectMoving.bind(this),
244
+ "object:removed": this._handleObjectRemoved.bind(this),
245
+ "object:moving": this._handleObjectMoving.bind(this),
162
246
  "object:modified": this._handleObjectModified.bind(this),
163
- "canvas:cleared": this._historySaveAction.bind(this),
247
+ "selection:created": this._handleSelectionCreated.bind(this),
248
+ "selection:updated": this._handleSelectionUpdated.bind(this),
249
+ "selection:cleared": this._handleSelectionCleared.bind(this),
164
250
  });
165
251
  }
252
+ /**
253
+ * Sets the provided `EraserBrush` instance as the canvas's eraser brush. This method is necessary to ensure that erasing actions are properly recorded in the history stack, as the default Fabric.js eraser brush does not trigger the relevant events for history tracking.
254
+ *
255
+ * @param eraser - The EraserBrush instance to set as the canvas's eraser brush.
256
+ */
257
+ setEraserBrush(eraser) {
258
+ this.freeDrawingBrush = eraser;
259
+ eraser.on("end", (e) => {
260
+ const { targets: erasedTargets, path } = e.detail;
261
+ this.fire("erasing:end", {
262
+ path,
263
+ targets: erasedTargets,
264
+ subTargets: [],
265
+ drawables: {},
266
+ });
267
+ });
268
+ }
269
+ /**
270
+ * Clears the history stacks for undo and redo.
271
+ */
272
+ clearHistory() {
273
+ this._historySaveInitialState();
274
+ this._historyRedo = [];
275
+ this.fire("history:cleared");
276
+ }
277
+ /**
278
+ * Cleans up event listeners and history stacks before disposing of the canvas instance.
279
+ */
166
280
  dispose() {
167
281
  this._disposeEventListeners();
168
- this._clearHistory();
282
+ this.clearHistory();
169
283
  return super.dispose();
170
284
  }
285
+ /**
286
+ * Clears the canvas and saves the cleared state to history.
287
+ *
288
+ * @remarks
289
+ * When using `CanvasWithHistory`, use this method instead of `clear()`.
290
+ * The inherited `clear()` method does not record to history.
291
+ */
292
+ clearCanvas() {
293
+ this._historyProcessing = true;
294
+ this.clear();
295
+ this._historyProcessing = false;
296
+ this._historySaveAction();
297
+ }
171
298
  }
172
299
  //# sourceMappingURL=canvas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anth0nycodes/fabric-history",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "description": "A library built on top of fabric.js that allows for easy access to canvas history.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,8 +31,11 @@
31
31
  "fabric": ">=6.0.0 <8.0.0"
32
32
  },
33
33
  "devDependencies": {
34
+ "@erase2d/fabric": "^1.2.1",
34
35
  "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
36
+ "@vitest/browser-playwright": "^4.0.18",
35
37
  "@vitest/coverage-v8": "^4.0.18",
38
+ "@vitest/ui": "^4.0.18",
36
39
  "canvas": "^3.2.1",
37
40
  "jsdom": "^28.0.0",
38
41
  "prettier-plugin-tailwindcss": "^0.7.2",
@@ -44,6 +47,8 @@
44
47
  "dev": "tsx src/index.ts",
45
48
  "build": "tsc",
46
49
  "test": "vitest",
50
+ "test:it": "vitest --project integration",
51
+ "test:e2e": "vitest --project e2e",
47
52
  "coverage": "vitest run --coverage",
48
53
  "start": "node dist/index.js",
49
54
  "check": "tsc --noEmit",
package/src/canvas.ts CHANGED
@@ -1,12 +1,51 @@
1
- import { Canvas } from "fabric";
1
+ import type { EraserBrush, ErasingEvent } from "@erase2d/fabric";
2
+ import {
3
+ Canvas,
4
+ type CanvasEvents,
5
+ type FabricObject,
6
+ type TEvent,
7
+ } from "fabric";
8
+
9
+ // Defines custom event listeners for history events
10
+ declare module "fabric" {
11
+ interface CanvasEvents {
12
+ "history:append": Partial<TEvent> & {
13
+ /**
14
+ * Serialized canvas state that was saved to history.
15
+ */
16
+ json: string;
17
+ /**
18
+ * Boolean flag indicating whether or not the appended history action is the initial state of the canvas.
19
+ */
20
+ initial: boolean;
21
+ };
22
+ "history:undo": Partial<TEvent> & {
23
+ /**
24
+ * Serialized canvas state that was most recently undone.
25
+ */
26
+ lastUndoAction: string;
27
+ };
28
+ "history:redo": Partial<TEvent> & {
29
+ /**
30
+ * Serialized canvas state that was most recently redone.
31
+ */
32
+ lastRedoAction: string;
33
+ };
34
+ "history:cleared": Partial<TEvent>;
35
+ }
36
+ }
2
37
 
3
38
  export class CanvasWithHistory extends Canvas {
4
- // History stacks
39
+ // History stack properties
5
40
  private _historyUndo: string[];
6
41
  private _historyRedo: string[];
7
42
 
8
- // Boolean values to determine whether or not we should save to history
9
- private _isMoving: boolean;
43
+ // Multi-selection properties
44
+ private _selectedObjects: FabricObject[];
45
+ private _isMultiSelection: boolean;
46
+
47
+ // Boolean properties to determine whether or not we should save to history
48
+ private _historyIsMoving: boolean;
10
49
  private _historyProcessing: boolean;
11
50
 
12
51
  private _historyCurrentState: string;
@@ -16,12 +55,14 @@ export class CanvasWithHistory extends Canvas {
16
55
 
17
56
  this._historyUndo = [];
18
57
  this._historyRedo = [];
19
- this._isMoving = false;
58
+ this._selectedObjects = [];
59
+ this._isMultiSelection = false;
60
+ this._historyIsMoving = false;
20
61
  this._historyProcessing = false;
21
62
  this._historyCurrentState = this._historyCurrent();
22
63
 
23
64
  this._bindEventListeners();
24
- this._saveInitialState();
65
+ this._historySaveInitialState();
25
66
  }
26
67
 
27
68
  /**
@@ -32,27 +73,93 @@ export class CanvasWithHistory extends Canvas {
32
73
  "path:created": this._historySaveAction.bind(this),
33
74
  "erasing:end": this._historySaveAction.bind(this),
34
75
  "object:added": this._historySaveAction.bind(this),
35
- "object:removed": this._historySaveAction.bind(this), // TODO: handle object modification + deletion batching
36
- "object:moving": this._objectMoving.bind(this),
76
+ "object:removed": this._handleObjectRemoved.bind(this),
77
+ "object:moving": this._handleObjectMoving.bind(this),
37
78
  "object:modified": this._handleObjectModified.bind(this),
38
- "canvas:cleared": this._historySaveAction.bind(this),
79
+ "selection:created": this._handleSelectionCreated.bind(this),
80
+ "selection:updated": this._handleSelectionUpdated.bind(this),
81
+ "selection:cleared": this._handleSelectionCleared.bind(this),
39
82
  });
40
83
  }
41
84
 
85
+ /**
86
+ * Stores the multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true.
87
+ *
88
+ * @param options - The options object containing the selected objects.
89
+ */
90
+ private _handleSelectionCreated(options: { selected: FabricObject[] }) {
91
+ const currentSelectedObjects = options.selected;
92
+
93
+ if (currentSelectedObjects.length > 1) {
94
+ this._selectedObjects = currentSelectedObjects;
95
+ this._isMultiSelection = true;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Stores the updated multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true if there are more than 1 objects selected.
101
+ */
102
+ private _handleSelectionUpdated() {
103
+ const allSelectedObjects = this.getActiveObjects();
104
+ /*
105
+ Uses reference to `getActiveObjects()` instead of the callback's `options.selected` because `options.selected`
106
+ only contains the objects that are currently selected, it doesn't cover all the previously
107
+ selected objects that are in the current ActiveSelection.
108
+ */
109
+ this._selectedObjects = allSelectedObjects;
110
+ this._isMultiSelection = allSelectedObjects.length > 1;
111
+ }
112
+
113
+ /**
114
+ * Clears the multi-selection state and sets the `_isMultiSelection` flag to false.
115
+ */
116
+ private _handleSelectionCleared() {
117
+ this._selectedObjects = [];
118
+ this._isMultiSelection = false;
119
+ }
120
+
42
121
  /**
43
122
  * Saves the initial state of the canvas.
44
123
  */
45
- private _saveInitialState() {
124
+ private _historySaveInitialState() {
46
125
  const initialState = this._historyCurrent();
47
126
  this._historyUndo = [initialState];
48
127
  this._historyCurrentState = initialState;
49
128
  }
50
129
 
130
+ /**
131
+ * Handles object removal events.
132
+ *
133
+ * @param options - The options object containing details about the removed object.
134
+ */
135
+ private _handleObjectRemoved(options: { target: FabricObject }) {
136
+ // handle object:removed events differently based on whether or not the removed object is within an ActiveSelection
137
+ if (
138
+ /*
139
+ !this._historyProcessing is included to prevent unintended recursive behavior
140
+ caused by this.remove() since that also triggers an object:removed event
141
+ */
142
+ !this._historyProcessing &&
143
+ this._isMultiSelection &&
144
+ this._selectedObjects.includes(options.target)
145
+ ) {
146
+ this._historyProcessing = true;
147
+ const objectsToRemove = [...this._selectedObjects];
148
+ this._selectedObjects = [];
149
+ this.remove(...objectsToRemove);
150
+ this.discardActiveObject();
151
+ this._historyProcessing = false;
152
+ this._historySaveAction();
153
+ } else {
154
+ this._historySaveAction();
155
+ }
156
+ }
157
+
51
158
  /**
52
159
  * Starts the movement event listener for objects.
53
160
  */
54
- private _objectMoving() {
55
- this._isMoving = true;
161
+ private _handleObjectMoving() {
162
+ this._historyIsMoving = true;
56
163
  }
57
164
 
58
165
  /**
@@ -63,7 +170,7 @@ export class CanvasWithHistory extends Canvas {
63
170
  */
64
171
  private _handleObjectModified() {
65
172
  // object:moving -> object:modified - modification is triggered as soon as the movement of an object halts
66
- this._isMoving = false;
173
+ this._historyIsMoving = false;
67
174
  this._historySaveAction();
68
175
  }
69
176
 
@@ -77,16 +184,17 @@ export class CanvasWithHistory extends Canvas {
77
184
  }
78
185
 
79
186
  /**
80
- * Records the current canvas, object, or path state into the history stack for undo/redo.
187
+ * Records the current state of the canvas to the history stack if the state has changed since the last recorded state. This method is called after relevant canvas events such as object modifications, additions, and removals.
81
188
  */
82
189
  private _historySaveAction() {
83
- if (this._historyProcessing || this._isMoving) return;
190
+ if (this._historyProcessing || this._historyIsMoving) return;
84
191
  const latestJSON = this._historyCurrent();
85
192
 
86
- if (this._historyCurrentState === latestJSON) return;
193
+ if (this._historyCurrentState === latestJSON) return; // skips duplicates
87
194
  this._historyUndo.push(latestJSON);
88
195
  this._historyCurrentState = latestJSON;
89
196
  this._historyRedo = [];
197
+ this.fire("history:append", { json: latestJSON, initial: false });
90
198
  }
91
199
 
92
200
  /**
@@ -106,6 +214,7 @@ export class CanvasWithHistory extends Canvas {
106
214
  if (!previousState) return;
107
215
  this._historyCurrentState = previousState;
108
216
  await this._loadFromHistory(previousState);
217
+ this.fire("history:undo", { lastUndoAction: poppedState });
109
218
  }
110
219
 
111
220
  /**
@@ -130,6 +239,7 @@ export class CanvasWithHistory extends Canvas {
130
239
  // refresh canvas to load the popped state
131
240
  this._historyCurrentState = poppedState;
132
241
  await this._loadFromHistory(poppedState);
242
+ this.fire("history:redo", { lastRedoAction: poppedState });
133
243
  }
134
244
 
135
245
  /**
@@ -145,13 +255,11 @@ export class CanvasWithHistory extends Canvas {
145
255
  * @param historyState - The JSON string representing the canvas history state to load.
146
256
  */
147
257
  private async _loadFromHistory(historyState: string) {
148
- this.clear();
149
- this.discardActiveObject();
150
-
151
258
  try {
152
- const parsed = JSON.parse(historyState);
153
- await this.loadFromJSON(parsed);
154
- this.renderAll();
259
+ this.clear();
260
+ this.discardActiveObject();
261
+ await this.loadFromJSON(JSON.parse(historyState));
262
+ this.requestRenderAll();
155
263
  } catch (error) {
156
264
  console.error("Error loading from history:", error);
157
265
  } finally {
@@ -160,11 +268,30 @@ export class CanvasWithHistory extends Canvas {
160
268
  }
161
269
 
162
270
  /**
163
- * Clears the history stacks for undo and redo.
271
+ * Debug method to log relevant events to the console. Always remember to remove before pushing once you're done debugging locally!
164
272
  */
165
- private _clearHistory() {
166
- this._historyUndo = [];
167
- this._historyRedo = [];
273
+ private _historyDebug() {
274
+ const EVENTS = [
275
+ "path:created",
276
+ "erasing:end",
277
+ "object:added",
278
+ "object:removed",
279
+ "object:moving",
280
+ "object:modified",
281
+ "selection:created",
282
+ "selection:cleared",
283
+ "canvas:cleared",
284
+ "history:append",
285
+ "history:undo",
286
+ "history:redo",
287
+ "history:cleared",
288
+ ];
289
+
290
+ EVENTS.forEach((e) => {
291
+ this.on(e as keyof CanvasEvents, () =>
292
+ console.log(`📝 Event triggered: ${e}`)
293
+ );
294
+ });
168
295
  }
169
296
 
170
297
  /**
@@ -175,16 +302,63 @@ export class CanvasWithHistory extends Canvas {
175
302
  "path:created": this._historySaveAction.bind(this),
176
303
  "erasing:end": this._historySaveAction.bind(this),
177
304
  "object:added": this._historySaveAction.bind(this),
178
- "object:removed": this._historySaveAction.bind(this),
179
- "object:moving": this._objectMoving.bind(this),
305
+ "object:removed": this._handleObjectRemoved.bind(this),
306
+ "object:moving": this._handleObjectMoving.bind(this),
180
307
  "object:modified": this._handleObjectModified.bind(this),
181
- "canvas:cleared": this._historySaveAction.bind(this),
308
+ "selection:created": this._handleSelectionCreated.bind(this),
309
+ "selection:updated": this._handleSelectionUpdated.bind(this),
310
+ "selection:cleared": this._handleSelectionCleared.bind(this),
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Sets the provided `EraserBrush` instance as the canvas's eraser brush. This method is necessary to ensure that erasing actions are properly recorded in the history stack, as the default Fabric.js eraser brush does not trigger the relevant events for history tracking.
316
+ *
317
+ * @param eraser - The EraserBrush instance to set as the canvas's eraser brush.
318
+ */
319
+ setEraserBrush(eraser: EraserBrush) {
320
+ this.freeDrawingBrush = eraser;
321
+
322
+ eraser.on("end", (e: ErasingEvent<"end">) => {
323
+ const { targets: erasedTargets, path } = e.detail;
324
+ this.fire("erasing:end", {
325
+ path,
326
+ targets: erasedTargets,
327
+ subTargets: [],
328
+ drawables: {},
329
+ });
182
330
  });
183
331
  }
184
332
 
333
+ /**
334
+ * Clears the history stacks for undo and redo.
335
+ */
336
+ clearHistory() {
337
+ this._historySaveInitialState();
338
+ this._historyRedo = [];
339
+ this.fire("history:cleared");
340
+ }
341
+
342
+ /**
343
+ * Cleans up event listeners and history stacks before disposing of the canvas instance.
344
+ */
185
345
  dispose() {
186
346
  this._disposeEventListeners();
187
- this._clearHistory();
347
+ this.clearHistory();
188
348
  return super.dispose();
189
349
  }
350
+
351
+ /**
352
+ * Clears the canvas and saves the cleared state to history.
353
+ *
354
+ * @remarks
355
+ * When using `CanvasWithHistory`, use this method instead of `clear()`.
356
+ * The inherited `clear()` method does not record to history.
357
+ */
358
+ clearCanvas() {
359
+ this._historyProcessing = true;
360
+ this.clear();
361
+ this._historyProcessing = false;
362
+ this._historySaveAction();
363
+ }
190
364
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=canvas.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"canvas.test.d.ts","sourceRoot":"","sources":["../src/canvas.test.ts"],"names":[],"mappings":""}
@@ -1,137 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { Circle, Path, Rect } from "fabric";
3
- import { beforeEach, describe, expect, test } from "vitest";
4
- import { CanvasWithHistory } from "./canvas";
5
- describe("canvas operations with history management", () => {
6
- let canvas;
7
- let circle;
8
- let path;
9
- let rect;
10
- beforeEach(() => {
11
- const canvasEl = document.createElement("canvas");
12
- canvas = new CanvasWithHistory(canvasEl);
13
- circle = new Circle({
14
- radius: 20,
15
- fill: "green",
16
- left: 100,
17
- top: 100,
18
- });
19
- path = new Path("M 0 0 L 100 100 L 0 100 z", {
20
- fill: "",
21
- stroke: "red",
22
- });
23
- rect = new Rect({
24
- left: 100,
25
- top: 100,
26
- fill: "red",
27
- width: 50,
28
- height: 50,
29
- });
30
- });
31
- test("canvas only contains rect and circle after undo", async () => {
32
- canvas.add(rect);
33
- canvas.add(circle);
34
- canvas.add(path);
35
- await canvas.undo();
36
- // After undo: canvas should only contain rect and circle object
37
- expect(canvas.contains(path)).toBe(false);
38
- });
39
- test("canvas only contains circle after undo", async () => {
40
- canvas.add(circle);
41
- canvas.add(rect);
42
- canvas.add(path);
43
- await canvas.undo();
44
- await canvas.undo();
45
- // After undo: canvas should only contain circle object
46
- expect(canvas.contains(rect) && canvas.contains(path)).toBe(false);
47
- });
48
- test("canvas contains nothing after undo", async () => {
49
- canvas.add(path);
50
- await canvas.undo();
51
- // After undo: canvas should have 0 objects
52
- expect(canvas.contains(path)).toBe(false);
53
- });
54
- // Redo tests
55
- test("canvas contains path again after undo then redo", async () => {
56
- canvas.add(rect);
57
- canvas.add(circle);
58
- canvas.add(path);
59
- await canvas.undo();
60
- await canvas.redo();
61
- // After redo: canvas should contain all 3 objects again
62
- expect(canvas.getObjects().length).toBe(3);
63
- });
64
- test("canvas contains rect and path after two undos then one redo", async () => {
65
- canvas.add(rect);
66
- canvas.add(circle);
67
- canvas.add(path);
68
- await canvas.undo();
69
- await canvas.undo();
70
- await canvas.redo();
71
- // After 2 undos and 1 redo: canvas should contain rect and circle
72
- expect(canvas.getObjects().length).toBe(2);
73
- });
74
- test("canvas contains all objects after multiple undos then multiple redos", async () => {
75
- canvas.add(rect);
76
- canvas.add(circle);
77
- canvas.add(path);
78
- await canvas.undo();
79
- await canvas.undo();
80
- await canvas.undo();
81
- // After 3 undos: canvas should be empty
82
- expect(canvas.getObjects().length).toBe(0);
83
- await canvas.redo();
84
- await canvas.redo();
85
- await canvas.redo();
86
- // After 3 redos: canvas should contain all 3 objects
87
- expect(canvas.getObjects().length).toBe(3);
88
- });
89
- test("redo does nothing when there is nothing to redo", async () => {
90
- canvas.add(rect);
91
- canvas.add(circle);
92
- await canvas.redo();
93
- // Redo with no prior undo: canvas should still contain 2 objects
94
- expect(canvas.getObjects().length).toBe(2);
95
- });
96
- test("redo stack is cleared after new action", async () => {
97
- canvas.add(rect);
98
- canvas.add(circle);
99
- canvas.add(path);
100
- await canvas.undo();
101
- // Add a new object after undo (this should clear redo stack)
102
- const newRect = new Rect({
103
- left: 200,
104
- top: 200,
105
- fill: "blue",
106
- width: 30,
107
- height: 30,
108
- });
109
- canvas.add(newRect);
110
- // Try to redo - should do nothing since redo stack was cleared
111
- await canvas.redo();
112
- // Canvas should still have 3 objects (rect, circle, newRect) - path should not come back
113
- expect(canvas.getObjects().length).toBe(3);
114
- expect(canvas.canRedo()).toBe(false);
115
- });
116
- test("canUndo returns correct value", async () => {
117
- expect(canvas.canUndo()).toBe(false);
118
- canvas.add(rect);
119
- expect(canvas.canUndo()).toBe(true);
120
- await canvas.undo();
121
- expect(canvas.canUndo()).toBe(false);
122
- });
123
- test("canRedo returns correct value", async () => {
124
- canvas.add(rect);
125
- expect(canvas.canRedo()).toBe(false);
126
- await canvas.undo();
127
- expect(canvas.canRedo()).toBe(true);
128
- await canvas.redo();
129
- expect(canvas.canRedo()).toBe(false);
130
- });
131
- test("undo does nothing when there is nothing to undo", async () => {
132
- await canvas.undo();
133
- // Undo on empty canvas: should still be empty with no errors
134
- expect(canvas.getObjects().length).toBe(0);
135
- });
136
- });
137
- //# sourceMappingURL=canvas.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"canvas.test.js","sourceRoot":"","sources":["../src/canvas.test.ts"],"names":[],"mappings":"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"}
@@ -1,178 +0,0 @@
1
- // @vitest-environment jsdom
2
-
3
- import { Circle, Path, Rect } from "fabric";
4
- import { beforeEach, describe, expect, test } from "vitest";
5
- import { CanvasWithHistory } from "./canvas";
6
-
7
- describe("canvas operations with history management", () => {
8
- let canvas: CanvasWithHistory;
9
- let circle: Circle;
10
- let path: Path;
11
- let rect: Rect;
12
-
13
- beforeEach(() => {
14
- const canvasEl = document.createElement("canvas");
15
- canvas = new CanvasWithHistory(canvasEl);
16
-
17
- circle = new Circle({
18
- radius: 20,
19
- fill: "green",
20
- left: 100,
21
- top: 100,
22
- });
23
-
24
- path = new Path("M 0 0 L 100 100 L 0 100 z", {
25
- fill: "",
26
- stroke: "red",
27
- });
28
-
29
- rect = new Rect({
30
- left: 100,
31
- top: 100,
32
- fill: "red",
33
- width: 50,
34
- height: 50,
35
- });
36
- });
37
-
38
- test("canvas only contains rect and circle after undo", async () => {
39
- canvas.add(rect);
40
- canvas.add(circle);
41
- canvas.add(path);
42
-
43
- await canvas.undo();
44
-
45
- // After undo: canvas should only contain rect and circle object
46
- expect(canvas.contains(path)).toBe(false);
47
- });
48
-
49
- test("canvas only contains circle after undo", async () => {
50
- canvas.add(circle);
51
- canvas.add(rect);
52
- canvas.add(path);
53
-
54
- await canvas.undo();
55
- await canvas.undo();
56
-
57
- // After undo: canvas should only contain circle object
58
- expect(canvas.contains(rect) && canvas.contains(path)).toBe(false);
59
- });
60
-
61
- test("canvas contains nothing after undo", async () => {
62
- canvas.add(path);
63
-
64
- await canvas.undo();
65
-
66
- // After undo: canvas should have 0 objects
67
- expect(canvas.contains(path)).toBe(false);
68
- });
69
-
70
- // Redo tests
71
- test("canvas contains path again after undo then redo", async () => {
72
- canvas.add(rect);
73
- canvas.add(circle);
74
- canvas.add(path);
75
-
76
- await canvas.undo();
77
- await canvas.redo();
78
-
79
- // After redo: canvas should contain all 3 objects again
80
- expect(canvas.getObjects().length).toBe(3);
81
- });
82
-
83
- test("canvas contains rect and path after two undos then one redo", async () => {
84
- canvas.add(rect);
85
- canvas.add(circle);
86
- canvas.add(path);
87
-
88
- await canvas.undo();
89
- await canvas.undo();
90
- await canvas.redo();
91
-
92
- // After 2 undos and 1 redo: canvas should contain rect and circle
93
- expect(canvas.getObjects().length).toBe(2);
94
- });
95
-
96
- test("canvas contains all objects after multiple undos then multiple redos", async () => {
97
- canvas.add(rect);
98
- canvas.add(circle);
99
- canvas.add(path);
100
-
101
- await canvas.undo();
102
- await canvas.undo();
103
- await canvas.undo();
104
-
105
- // After 3 undos: canvas should be empty
106
- expect(canvas.getObjects().length).toBe(0);
107
-
108
- await canvas.redo();
109
- await canvas.redo();
110
- await canvas.redo();
111
-
112
- // After 3 redos: canvas should contain all 3 objects
113
- expect(canvas.getObjects().length).toBe(3);
114
- });
115
-
116
- test("redo does nothing when there is nothing to redo", async () => {
117
- canvas.add(rect);
118
- canvas.add(circle);
119
-
120
- await canvas.redo();
121
-
122
- // Redo with no prior undo: canvas should still contain 2 objects
123
- expect(canvas.getObjects().length).toBe(2);
124
- });
125
-
126
- test("redo stack is cleared after new action", async () => {
127
- canvas.add(rect);
128
- canvas.add(circle);
129
- canvas.add(path);
130
-
131
- await canvas.undo();
132
-
133
- // Add a new object after undo (this should clear redo stack)
134
- const newRect = new Rect({
135
- left: 200,
136
- top: 200,
137
- fill: "blue",
138
- width: 30,
139
- height: 30,
140
- });
141
- canvas.add(newRect);
142
-
143
- // Try to redo - should do nothing since redo stack was cleared
144
- await canvas.redo();
145
-
146
- // Canvas should still have 3 objects (rect, circle, newRect) - path should not come back
147
- expect(canvas.getObjects().length).toBe(3);
148
- expect(canvas.canRedo()).toBe(false);
149
- });
150
-
151
- test("canUndo returns correct value", async () => {
152
- expect(canvas.canUndo()).toBe(false);
153
-
154
- canvas.add(rect);
155
- expect(canvas.canUndo()).toBe(true);
156
-
157
- await canvas.undo();
158
- expect(canvas.canUndo()).toBe(false);
159
- });
160
-
161
- test("canRedo returns correct value", async () => {
162
- canvas.add(rect);
163
- expect(canvas.canRedo()).toBe(false);
164
-
165
- await canvas.undo();
166
- expect(canvas.canRedo()).toBe(true);
167
-
168
- await canvas.redo();
169
- expect(canvas.canRedo()).toBe(false);
170
- });
171
-
172
- test("undo does nothing when there is nothing to undo", async () => {
173
- await canvas.undo();
174
-
175
- // Undo on empty canvas: should still be empty with no errors
176
- expect(canvas.getObjects().length).toBe(0);
177
- });
178
- });