@anth0nycodes/fabric-history 0.1.3 → 0.2.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
 
@@ -55,9 +57,6 @@ await canvas.undo();
55
57
 
56
58
  // Redo the undone action
57
59
  await canvas.redo();
58
-
59
- // Clear history if needed
60
- canvas.clearHistory();
61
60
  ```
62
61
 
63
62
  ## API
@@ -68,14 +67,15 @@ Extends fabric.js `Canvas` class with history management capabilities.
68
67
 
69
68
  #### Methods
70
69
 
71
- | Method | Returns | Description |
72
- | ---------------- | --------------- | ----------------------------------------------- |
73
- | `undo()` | `Promise<void>` | Undo the most recent action |
74
- | `redo()` | `Promise<void>` | Redo the most recently undone action |
75
- | `canUndo()` | `boolean` | Check if an undo action is available |
76
- | `canRedo()` | `boolean` | Check if a redo action is available |
77
- | `clearHistory()` | `void` | Clear all undo and redo history |
78
- | `dispose()` | `void` | Clean up event listeners and dispose the canvas |
70
+ | Method | Returns | Description |
71
+ | ---------------- | --------------- | ----------------------------------------------------------------------------------------------------- |
72
+ | `undo()` | `Promise<void>` | Undo the most recent action |
73
+ | `redo()` | `Promise<void>` | Redo the most recently undone action |
74
+ | `canUndo()` | `boolean` | Check if an undo action is available |
75
+ | `canRedo()` | `boolean` | Check if a redo action is available |
76
+ | `clearHistory()` | `void` | Clear the undo and redo history stacks |
77
+ | `clearCanvas()` | `void` | Clear the canvas and save the cleared state to history (use this instead of the inherited `clear()`) |
78
+ | `dispose()` | `void` | Clean up event listeners and dispose the canvas |
79
79
 
80
80
  #### Tracked Events
81
81
 
@@ -88,6 +88,29 @@ History is automatically saved when these fabric.js events occur:
88
88
  - `erasing:end` - When an erasing operation completes
89
89
  - `canvas:cleared` - When the canvas is cleared
90
90
 
91
+ #### Custom Events
92
+
93
+ `CanvasWithHistory` fires custom events that you can listen to for history state changes:
94
+
95
+ | Event | Payload | Description |
96
+ | ----------------- | ------------------------------------------------ | ---------------------------------- |
97
+ | `history:append` | `{ json: string, initial: boolean }` | Fired when a state is saved |
98
+ | `history:undo` | `{ lastUndoAction: string }` | Fired when an undo is performed |
99
+ | `history:redo` | `{ lastRedoAction: string }` | Fired when a redo is performed |
100
+ | `history:cleared` | `{}` | Fired when history stacks cleared |
101
+
102
+ **Example:**
103
+
104
+ ```typescript
105
+ canvas.on("history:append", ({ json, initial }) => {
106
+ console.log("State saved:", initial ? "initial" : "action");
107
+ });
108
+
109
+ canvas.on("history:undo", ({ lastUndoAction }) => {
110
+ console.log("Undo performed");
111
+ });
112
+ ```
113
+
91
114
  ## Requirements
92
115
 
93
116
  - fabric.js 6.x or 7.x
@@ -107,9 +130,15 @@ pnpm build
107
130
  # Type check
108
131
  pnpm check
109
132
 
110
- # Run tests
133
+ # Run all tests
111
134
  pnpm test
112
135
 
136
+ # Run integration tests only
137
+ pnpm test:it
138
+
139
+ # Run E2E tests only (uses Playwright)
140
+ pnpm test:e2e
141
+
113
142
  # Run tests with coverage
114
143
  pnpm coverage
115
144
  ```
package/dist/canvas.d.ts CHANGED
@@ -1,8 +1,37 @@
1
- import { Canvas } from "fabric";
1
+ import { Canvas, type TEvent } from "fabric";
2
+ declare module "fabric" {
3
+ interface CanvasEvents {
4
+ "history:append": Partial<TEvent> & {
5
+ /**
6
+ * Serialized canvas state that was saved to history.
7
+ */
8
+ json: string;
9
+ /**
10
+ * Boolean flag indicating whether or not the appended history action is the initial state of the canvas.
11
+ */
12
+ initial: boolean;
13
+ };
14
+ "history:undo": Partial<TEvent> & {
15
+ /**
16
+ * Serialized canvas state that was most recently undone.
17
+ */
18
+ lastUndoAction: string;
19
+ };
20
+ "history:redo": Partial<TEvent> & {
21
+ /**
22
+ * Serialized canvas state that was most recently redone.
23
+ */
24
+ lastRedoAction: string;
25
+ };
26
+ "history:cleared": Partial<TEvent>;
27
+ }
28
+ }
2
29
  export declare class CanvasWithHistory extends Canvas {
3
30
  private _historyUndo;
4
31
  private _historyRedo;
5
- private _isMoving;
32
+ private _selectedObjects;
33
+ private _isMultiSelection;
34
+ private _historyIsMoving;
6
35
  private _historyProcessing;
7
36
  private _historyCurrentState;
8
37
  constructor(...args: ConstructorParameters<typeof Canvas>);
@@ -10,14 +39,36 @@ export declare class CanvasWithHistory extends Canvas {
10
39
  * Binds all relevant fabric event listeners.
11
40
  */
12
41
  private _bindEventListeners;
42
+ /**
43
+ * Stores the multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true.
44
+ *
45
+ * @param options - The options object containing the selected objects.
46
+ */
47
+ private _handleSelectionCreated;
48
+ /**
49
+ * Stores the updated multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true if there are more than 1 objects selected.
50
+ *
51
+ * @param options - The options object containing the updated selected objects.
52
+ */
53
+ private _handleSelectionUpdated;
54
+ /**
55
+ * Clears the multi-selection state and sets the `_isMultiSelection` flag to false.
56
+ */
57
+ private _handleSelectionCleared;
13
58
  /**
14
59
  * Saves the initial state of the canvas.
15
60
  */
16
- private _saveInitialState;
61
+ private _historySaveInitialState;
62
+ /**
63
+ * Handles object removal events.
64
+ *
65
+ * @param options - The options object containing details about the removed object.
66
+ */
67
+ private _handleObjectRemoved;
17
68
  /**
18
69
  * Starts the movement event listener for objects.
19
70
  */
20
- private _objectMoving;
71
+ private _handleObjectMoving;
21
72
  /**
22
73
  * Handles object modification events, including moving, resizing, rotating,
23
74
  * scaling, and skewing.
@@ -32,7 +83,7 @@ export declare class CanvasWithHistory extends Canvas {
32
83
  */
33
84
  private _historyCurrent;
34
85
  /**
35
- * Records the current canvas, object, or path state into the history stack for undo/redo.
86
+ * 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
87
  */
37
88
  private _historySaveAction;
38
89
  /**
@@ -60,11 +111,26 @@ export declare class CanvasWithHistory extends Canvas {
60
111
  /**
61
112
  * Clears the history stacks for undo and redo.
62
113
  */
63
- private _clearHistory;
114
+ clearHistory(): void;
115
+ /**
116
+ * Debug method to log relevant events to the console. Always remember to remove before pushing once you're done debugging locally!
117
+ */
118
+ private _historyDebug;
64
119
  /**
65
120
  * Unsubscribes all relevant fabric event listeners.
66
121
  */
67
122
  private _disposeEventListeners;
123
+ /**
124
+ * Cleans up event listeners and history stacks before disposing of the canvas instance.
125
+ */
68
126
  dispose(): Promise<boolean>;
127
+ /**
128
+ * Clears the canvas and saves the cleared state to history.
129
+ *
130
+ * @remarks
131
+ * When using `CanvasWithHistory`, use this method instead of `clear()`.
132
+ * The inherited `clear()` method does not record to history.
133
+ */
134
+ clearCanvas(): void;
69
135
  }
70
136
  //# 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,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;;;;OAIG;IACH,OAAO,CAAC,uBAAuB;IAM/B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAK/B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAMhC;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAsB5B;;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,YAAY;IAMZ;;OAEG;IACH,OAAO,CAAC,aAAa;IAwBrB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAc9B;;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,82 @@ 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
+ * @param options - The options object containing the updated selected objects.
57
+ */
58
+ _handleSelectionUpdated(options) {
59
+ const allSelectedObjects = this.getActiveObjects();
60
+ this._selectedObjects = allSelectedObjects;
61
+ this._isMultiSelection = allSelectedObjects.length > 1;
62
+ }
63
+ /**
64
+ * Clears the multi-selection state and sets the `_isMultiSelection` flag to false.
65
+ */
66
+ _handleSelectionCleared() {
67
+ this._selectedObjects = [];
68
+ this._isMultiSelection = false;
69
+ }
34
70
  /**
35
71
  * Saves the initial state of the canvas.
36
72
  */
37
- _saveInitialState() {
73
+ _historySaveInitialState() {
38
74
  const initialState = this._historyCurrent();
39
75
  this._historyUndo = [initialState];
40
76
  this._historyCurrentState = initialState;
41
77
  }
78
+ /**
79
+ * Handles object removal events.
80
+ *
81
+ * @param options - The options object containing details about the removed object.
82
+ */
83
+ _handleObjectRemoved(options) {
84
+ /*
85
+ Check !_historyProcessing to prevent recursion: this.remove() fires
86
+ object:removed events, which would re-enter this handler while we're
87
+ still processing the first removal.
88
+ */
89
+ if (!this._historyProcessing &&
90
+ this._isMultiSelection &&
91
+ this._selectedObjects.some((obj) => obj === options.target)) {
92
+ this._historyProcessing = true;
93
+ const objectsToRemove = [...this._selectedObjects];
94
+ this._selectedObjects = [];
95
+ this.remove(...objectsToRemove);
96
+ this.discardActiveObject();
97
+ this._historyProcessing = false;
98
+ this._historySaveAction();
99
+ }
100
+ else {
101
+ this._historySaveAction();
102
+ }
103
+ }
42
104
  /**
43
105
  * Starts the movement event listener for objects.
44
106
  */
45
- _objectMoving() {
46
- this._isMoving = true;
107
+ _handleObjectMoving() {
108
+ this._historyIsMoving = true;
47
109
  }
48
110
  /**
49
111
  * Handles object modification events, including moving, resizing, rotating,
@@ -53,7 +115,7 @@ export class CanvasWithHistory extends Canvas {
53
115
  */
54
116
  _handleObjectModified() {
55
117
  // object:moving -> object:modified - modification is triggered as soon as the movement of an object halts
56
- this._isMoving = false;
118
+ this._historyIsMoving = false;
57
119
  this._historySaveAction();
58
120
  }
59
121
  /**
@@ -65,17 +127,18 @@ export class CanvasWithHistory extends Canvas {
65
127
  return JSON.stringify(this.toDatalessJSON());
66
128
  }
67
129
  /**
68
- * Records the current canvas, object, or path state into the history stack for undo/redo.
130
+ * 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
131
  */
70
132
  _historySaveAction() {
71
- if (this._historyProcessing || this._isMoving)
133
+ if (this._historyProcessing || this._historyIsMoving)
72
134
  return;
73
135
  const latestJSON = this._historyCurrent();
74
136
  if (this._historyCurrentState === latestJSON)
75
- return;
137
+ return; // skips duplicates
76
138
  this._historyUndo.push(latestJSON);
77
139
  this._historyCurrentState = latestJSON;
78
140
  this._historyRedo = [];
141
+ this.fire("history:append", { json: latestJSON, initial: false });
79
142
  }
80
143
  /**
81
144
  * Undo the most recent action.
@@ -94,6 +157,7 @@ export class CanvasWithHistory extends Canvas {
94
157
  return;
95
158
  this._historyCurrentState = previousState;
96
159
  await this._loadFromHistory(previousState);
160
+ this.fire("history:undo", { lastUndoAction: poppedState });
97
161
  }
98
162
  /**
99
163
  * Checks for whether or not an action can be undone.
@@ -115,6 +179,7 @@ export class CanvasWithHistory extends Canvas {
115
179
  // refresh canvas to load the popped state
116
180
  this._historyCurrentState = poppedState;
117
181
  await this._loadFromHistory(poppedState);
182
+ this.fire("history:redo", { lastRedoAction: poppedState });
118
183
  }
119
184
  /**
120
185
  * Checks for whether or not an action can be redone.
@@ -128,12 +193,11 @@ export class CanvasWithHistory extends Canvas {
128
193
  * @param historyState - The JSON string representing the canvas history state to load.
129
194
  */
130
195
  async _loadFromHistory(historyState) {
131
- this.clear();
132
- this.discardActiveObject();
133
196
  try {
134
- const parsed = JSON.parse(historyState);
135
- await this.loadFromJSON(parsed);
136
- this.renderAll();
197
+ this.clear();
198
+ this.discardActiveObject();
199
+ await this.loadFromJSON(JSON.parse(historyState));
200
+ this.requestRenderAll();
137
201
  }
138
202
  catch (error) {
139
203
  console.error("Error loading from history:", error);
@@ -145,9 +209,33 @@ export class CanvasWithHistory extends Canvas {
145
209
  /**
146
210
  * Clears the history stacks for undo and redo.
147
211
  */
148
- _clearHistory() {
149
- this._historyUndo = [];
212
+ clearHistory() {
213
+ this._historySaveInitialState();
150
214
  this._historyRedo = [];
215
+ this.fire("history:cleared");
216
+ }
217
+ /**
218
+ * Debug method to log relevant events to the console. Always remember to remove before pushing once you're done debugging locally!
219
+ */
220
+ _historyDebug() {
221
+ const EVENTS = [
222
+ "path:created",
223
+ "erasing:end",
224
+ "object:added",
225
+ "object:removed",
226
+ "object:moving",
227
+ "object:modified",
228
+ "selection:created",
229
+ "selection:cleared",
230
+ "canvas:cleared",
231
+ "history:append",
232
+ "history:undo",
233
+ "history:redo",
234
+ "history:cleared",
235
+ ];
236
+ EVENTS.forEach((e) => {
237
+ this.on(e, () => console.log(`📝 Event triggered: ${e}`));
238
+ });
151
239
  }
152
240
  /**
153
241
  * Unsubscribes all relevant fabric event listeners.
@@ -157,16 +245,34 @@ export class CanvasWithHistory extends Canvas {
157
245
  "path:created": this._historySaveAction.bind(this),
158
246
  "erasing:end": this._historySaveAction.bind(this),
159
247
  "object:added": this._historySaveAction.bind(this),
160
- "object:removed": this._historySaveAction.bind(this),
161
- "object:moving": this._objectMoving.bind(this),
248
+ "object:removed": this._handleObjectRemoved.bind(this),
249
+ "object:moving": this._handleObjectMoving.bind(this),
162
250
  "object:modified": this._handleObjectModified.bind(this),
163
- "canvas:cleared": this._historySaveAction.bind(this),
251
+ "selection:created": this._handleSelectionCreated.bind(this),
252
+ "selection:updated": this._handleSelectionUpdated.bind(this),
253
+ "selection:cleared": this._handleSelectionCleared.bind(this),
164
254
  });
165
255
  }
256
+ /**
257
+ * Cleans up event listeners and history stacks before disposing of the canvas instance.
258
+ */
166
259
  dispose() {
167
260
  this._disposeEventListeners();
168
- this._clearHistory();
261
+ this.clearHistory();
169
262
  return super.dispose();
170
263
  }
264
+ /**
265
+ * Clears the canvas and saves the cleared state to history.
266
+ *
267
+ * @remarks
268
+ * When using `CanvasWithHistory`, use this method instead of `clear()`.
269
+ * The inherited `clear()` method does not record to history.
270
+ */
271
+ clearCanvas() {
272
+ this._historyProcessing = true;
273
+ this.clear();
274
+ this._historyProcessing = false;
275
+ this._historySaveAction();
276
+ }
171
277
  }
172
278
  //# 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.3",
3
+ "version": "0.2.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",
@@ -32,7 +32,9 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
35
+ "@vitest/browser-playwright": "^4.0.18",
35
36
  "@vitest/coverage-v8": "^4.0.18",
37
+ "@vitest/ui": "^4.0.18",
36
38
  "canvas": "^3.2.1",
37
39
  "jsdom": "^28.0.0",
38
40
  "prettier-plugin-tailwindcss": "^0.7.2",
@@ -44,6 +46,8 @@
44
46
  "dev": "tsx src/index.ts",
45
47
  "build": "tsc",
46
48
  "test": "vitest",
49
+ "test:it": "vitest --project integration",
50
+ "test:e2e": "vitest --project e2e",
47
51
  "coverage": "vitest run --coverage",
48
52
  "start": "node dist/index.js",
49
53
  "check": "tsc --noEmit",
package/src/canvas.ts CHANGED
@@ -1,12 +1,50 @@
1
- import { Canvas } from "fabric";
1
+ import {
2
+ Canvas,
3
+ type CanvasEvents,
4
+ type FabricObject,
5
+ type TEvent,
6
+ } from "fabric";
7
+
8
+ // Defines custom event listeners for history events
9
+ declare module "fabric" {
10
+ interface CanvasEvents {
11
+ "history:append": Partial<TEvent> & {
12
+ /**
13
+ * Serialized canvas state that was saved to history.
14
+ */
15
+ json: string;
16
+ /**
17
+ * Boolean flag indicating whether or not the appended history action is the initial state of the canvas.
18
+ */
19
+ initial: boolean;
20
+ };
21
+ "history:undo": Partial<TEvent> & {
22
+ /**
23
+ * Serialized canvas state that was most recently undone.
24
+ */
25
+ lastUndoAction: string;
26
+ };
27
+ "history:redo": Partial<TEvent> & {
28
+ /**
29
+ * Serialized canvas state that was most recently redone.
30
+ */
31
+ lastRedoAction: string;
32
+ };
33
+ "history:cleared": Partial<TEvent>;
34
+ }
35
+ }
2
36
 
3
37
  export class CanvasWithHistory extends Canvas {
4
- // History stacks
38
+ // History stack properties
5
39
  private _historyUndo: string[];
6
40
  private _historyRedo: string[];
7
41
 
8
- // Boolean values to determine whether or not we should save to history
9
- private _isMoving: boolean;
42
+ // Multi-selection properties
43
+ private _selectedObjects: FabricObject[];
44
+ private _isMultiSelection: boolean;
45
+
46
+ // Boolean properties to determine whether or not we should save to history
47
+ private _historyIsMoving: boolean;
10
48
  private _historyProcessing: boolean;
11
49
 
12
50
  private _historyCurrentState: string;
@@ -16,12 +54,14 @@ export class CanvasWithHistory extends Canvas {
16
54
 
17
55
  this._historyUndo = [];
18
56
  this._historyRedo = [];
19
- this._isMoving = false;
57
+ this._selectedObjects = [];
58
+ this._isMultiSelection = false;
59
+ this._historyIsMoving = false;
20
60
  this._historyProcessing = false;
21
61
  this._historyCurrentState = this._historyCurrent();
22
62
 
23
63
  this._bindEventListeners();
24
- this._saveInitialState();
64
+ this._historySaveInitialState();
25
65
  }
26
66
 
27
67
  /**
@@ -32,27 +72,89 @@ export class CanvasWithHistory extends Canvas {
32
72
  "path:created": this._historySaveAction.bind(this),
33
73
  "erasing:end": this._historySaveAction.bind(this),
34
74
  "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),
75
+ "object:removed": this._handleObjectRemoved.bind(this),
76
+ "object:moving": this._handleObjectMoving.bind(this),
37
77
  "object:modified": this._handleObjectModified.bind(this),
38
- "canvas:cleared": this._historySaveAction.bind(this),
78
+ "selection:created": this._handleSelectionCreated.bind(this),
79
+ "selection:updated": this._handleSelectionUpdated.bind(this),
80
+ "selection:cleared": this._handleSelectionCleared.bind(this),
39
81
  });
40
82
  }
41
83
 
84
+ /**
85
+ * Stores the multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true.
86
+ *
87
+ * @param options - The options object containing the selected objects.
88
+ */
89
+ private _handleSelectionCreated(options: { selected: FabricObject[] }) {
90
+ const currentSelectedObjects = options.selected;
91
+
92
+ if (currentSelectedObjects.length > 1) {
93
+ this._selectedObjects = currentSelectedObjects;
94
+ this._isMultiSelection = true;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Stores the updated multi-selection state inside `_selectedObjects` and sets the `_isMultiSelection` flag to true if there are more than 1 objects selected.
100
+ *
101
+ * @param options - The options object containing the updated selected objects.
102
+ */
103
+ private _handleSelectionUpdated(options: { selected: FabricObject[] }) {
104
+ const allSelectedObjects = this.getActiveObjects();
105
+ this._selectedObjects = allSelectedObjects;
106
+ this._isMultiSelection = allSelectedObjects.length > 1;
107
+ }
108
+
109
+ /**
110
+ * Clears the multi-selection state and sets the `_isMultiSelection` flag to false.
111
+ */
112
+ private _handleSelectionCleared() {
113
+ this._selectedObjects = [];
114
+ this._isMultiSelection = false;
115
+ }
116
+
42
117
  /**
43
118
  * Saves the initial state of the canvas.
44
119
  */
45
- private _saveInitialState() {
120
+ private _historySaveInitialState() {
46
121
  const initialState = this._historyCurrent();
47
122
  this._historyUndo = [initialState];
48
123
  this._historyCurrentState = initialState;
49
124
  }
50
125
 
126
+ /**
127
+ * Handles object removal events.
128
+ *
129
+ * @param options - The options object containing details about the removed object.
130
+ */
131
+ private _handleObjectRemoved(options: { target: FabricObject }) {
132
+ /*
133
+ Check !_historyProcessing to prevent recursion: this.remove() fires
134
+ object:removed events, which would re-enter this handler while we're
135
+ still processing the first removal.
136
+ */
137
+ if (
138
+ !this._historyProcessing &&
139
+ this._isMultiSelection &&
140
+ this._selectedObjects.some((obj) => obj === options.target)
141
+ ) {
142
+ this._historyProcessing = true;
143
+ const objectsToRemove = [...this._selectedObjects];
144
+ this._selectedObjects = [];
145
+ this.remove(...objectsToRemove);
146
+ this.discardActiveObject();
147
+ this._historyProcessing = false;
148
+ this._historySaveAction();
149
+ } else {
150
+ this._historySaveAction();
151
+ }
152
+ }
51
153
  /**
52
154
  * Starts the movement event listener for objects.
53
155
  */
54
- private _objectMoving() {
55
- this._isMoving = true;
156
+ private _handleObjectMoving() {
157
+ this._historyIsMoving = true;
56
158
  }
57
159
 
58
160
  /**
@@ -63,7 +165,7 @@ export class CanvasWithHistory extends Canvas {
63
165
  */
64
166
  private _handleObjectModified() {
65
167
  // object:moving -> object:modified - modification is triggered as soon as the movement of an object halts
66
- this._isMoving = false;
168
+ this._historyIsMoving = false;
67
169
  this._historySaveAction();
68
170
  }
69
171
 
@@ -77,16 +179,17 @@ export class CanvasWithHistory extends Canvas {
77
179
  }
78
180
 
79
181
  /**
80
- * Records the current canvas, object, or path state into the history stack for undo/redo.
182
+ * 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
183
  */
82
184
  private _historySaveAction() {
83
- if (this._historyProcessing || this._isMoving) return;
185
+ if (this._historyProcessing || this._historyIsMoving) return;
84
186
  const latestJSON = this._historyCurrent();
85
187
 
86
- if (this._historyCurrentState === latestJSON) return;
188
+ if (this._historyCurrentState === latestJSON) return; // skips duplicates
87
189
  this._historyUndo.push(latestJSON);
88
190
  this._historyCurrentState = latestJSON;
89
191
  this._historyRedo = [];
192
+ this.fire("history:append", { json: latestJSON, initial: false });
90
193
  }
91
194
 
92
195
  /**
@@ -106,6 +209,7 @@ export class CanvasWithHistory extends Canvas {
106
209
  if (!previousState) return;
107
210
  this._historyCurrentState = previousState;
108
211
  await this._loadFromHistory(previousState);
212
+ this.fire("history:undo", { lastUndoAction: poppedState });
109
213
  }
110
214
 
111
215
  /**
@@ -130,6 +234,7 @@ export class CanvasWithHistory extends Canvas {
130
234
  // refresh canvas to load the popped state
131
235
  this._historyCurrentState = poppedState;
132
236
  await this._loadFromHistory(poppedState);
237
+ this.fire("history:redo", { lastRedoAction: poppedState });
133
238
  }
134
239
 
135
240
  /**
@@ -145,13 +250,11 @@ export class CanvasWithHistory extends Canvas {
145
250
  * @param historyState - The JSON string representing the canvas history state to load.
146
251
  */
147
252
  private async _loadFromHistory(historyState: string) {
148
- this.clear();
149
- this.discardActiveObject();
150
-
151
253
  try {
152
- const parsed = JSON.parse(historyState);
153
- await this.loadFromJSON(parsed);
154
- this.renderAll();
254
+ this.clear();
255
+ this.discardActiveObject();
256
+ await this.loadFromJSON(JSON.parse(historyState));
257
+ this.requestRenderAll();
155
258
  } catch (error) {
156
259
  console.error("Error loading from history:", error);
157
260
  } finally {
@@ -162,9 +265,37 @@ export class CanvasWithHistory extends Canvas {
162
265
  /**
163
266
  * Clears the history stacks for undo and redo.
164
267
  */
165
- private _clearHistory() {
166
- this._historyUndo = [];
268
+ clearHistory() {
269
+ this._historySaveInitialState();
167
270
  this._historyRedo = [];
271
+ this.fire("history:cleared");
272
+ }
273
+
274
+ /**
275
+ * Debug method to log relevant events to the console. Always remember to remove before pushing once you're done debugging locally!
276
+ */
277
+ private _historyDebug() {
278
+ const EVENTS = [
279
+ "path:created",
280
+ "erasing:end",
281
+ "object:added",
282
+ "object:removed",
283
+ "object:moving",
284
+ "object:modified",
285
+ "selection:created",
286
+ "selection:cleared",
287
+ "canvas:cleared",
288
+ "history:append",
289
+ "history:undo",
290
+ "history:redo",
291
+ "history:cleared",
292
+ ];
293
+
294
+ EVENTS.forEach((e) => {
295
+ this.on(e as keyof CanvasEvents, () =>
296
+ console.log(`📝 Event triggered: ${e}`)
297
+ );
298
+ });
168
299
  }
169
300
 
170
301
  /**
@@ -175,16 +306,35 @@ export class CanvasWithHistory extends Canvas {
175
306
  "path:created": this._historySaveAction.bind(this),
176
307
  "erasing:end": this._historySaveAction.bind(this),
177
308
  "object:added": this._historySaveAction.bind(this),
178
- "object:removed": this._historySaveAction.bind(this),
179
- "object:moving": this._objectMoving.bind(this),
309
+ "object:removed": this._handleObjectRemoved.bind(this),
310
+ "object:moving": this._handleObjectMoving.bind(this),
180
311
  "object:modified": this._handleObjectModified.bind(this),
181
- "canvas:cleared": this._historySaveAction.bind(this),
312
+ "selection:created": this._handleSelectionCreated.bind(this),
313
+ "selection:updated": this._handleSelectionUpdated.bind(this),
314
+ "selection:cleared": this._handleSelectionCleared.bind(this),
182
315
  });
183
316
  }
184
317
 
318
+ /**
319
+ * Cleans up event listeners and history stacks before disposing of the canvas instance.
320
+ */
185
321
  dispose() {
186
322
  this._disposeEventListeners();
187
- this._clearHistory();
323
+ this.clearHistory();
188
324
  return super.dispose();
189
325
  }
326
+
327
+ /**
328
+ * Clears the canvas and saves the cleared state to history.
329
+ *
330
+ * @remarks
331
+ * When using `CanvasWithHistory`, use this method instead of `clear()`.
332
+ * The inherited `clear()` method does not record to history.
333
+ */
334
+ clearCanvas() {
335
+ this._historyProcessing = true;
336
+ this.clear();
337
+ this._historyProcessing = false;
338
+ this._historySaveAction();
339
+ }
190
340
  }
@@ -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
- });