@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 +64 -8
- package/dist/canvas.d.ts +78 -7
- package/dist/canvas.d.ts.map +1 -1
- package/dist/canvas.js +156 -29
- package/dist/canvas.js.map +1 -1
- package/package.json +6 -1
- package/src/canvas.ts +204 -30
- package/dist/canvas.test.d.ts +0 -2
- package/dist/canvas.test.d.ts.map +0 -1
- package/dist/canvas.test.js +0 -137
- package/dist/canvas.test.js.map +0 -1
- package/src/canvas.test.ts +0 -178
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
|
|
69
|
-
|
|
|
70
|
-
| `undo()`
|
|
71
|
-
| `redo()`
|
|
72
|
-
| `canUndo()`
|
|
73
|
-
| `canRedo()`
|
|
74
|
-
| `
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
package/dist/canvas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.d.ts","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,
|
|
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
|
|
3
|
+
// History stack properties
|
|
4
4
|
_historyUndo;
|
|
5
5
|
_historyRedo;
|
|
6
|
-
//
|
|
7
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
29
|
-
"object:moving": this.
|
|
33
|
+
"object:removed": this._handleObjectRemoved.bind(this),
|
|
34
|
+
"object:moving": this._handleObjectMoving.bind(this),
|
|
30
35
|
"object:modified": this._handleObjectModified.bind(this),
|
|
31
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
this.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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.
|
|
161
|
-
"object:moving": this.
|
|
244
|
+
"object:removed": this._handleObjectRemoved.bind(this),
|
|
245
|
+
"object:moving": this._handleObjectMoving.bind(this),
|
|
162
246
|
"object:modified": this._handleObjectModified.bind(this),
|
|
163
|
-
"
|
|
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.
|
|
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
|
package/dist/canvas.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"
|
|
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.
|
|
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 {
|
|
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
|
|
39
|
+
// History stack properties
|
|
5
40
|
private _historyUndo: string[];
|
|
6
41
|
private _historyRedo: string[];
|
|
7
42
|
|
|
8
|
-
//
|
|
9
|
-
private
|
|
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.
|
|
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.
|
|
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.
|
|
36
|
-
"object:moving": this.
|
|
76
|
+
"object:removed": this._handleObjectRemoved.bind(this),
|
|
77
|
+
"object:moving": this._handleObjectMoving.bind(this),
|
|
37
78
|
"object:modified": this._handleObjectModified.bind(this),
|
|
38
|
-
"
|
|
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
|
|
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
|
|
55
|
-
this.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
this.
|
|
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
|
-
*
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
179
|
-
"object:moving": this.
|
|
305
|
+
"object:removed": this._handleObjectRemoved.bind(this),
|
|
306
|
+
"object:moving": this._handleObjectMoving.bind(this),
|
|
180
307
|
"object:modified": this._handleObjectModified.bind(this),
|
|
181
|
-
"
|
|
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.
|
|
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
|
}
|
package/dist/canvas.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.test.d.ts","sourceRoot":"","sources":["../src/canvas.test.ts"],"names":[],"mappings":""}
|
package/dist/canvas.test.js
DELETED
|
@@ -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
|
package/dist/canvas.test.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"canvas.test.js","sourceRoot":"","sources":["../src/canvas.test.ts"],"names":[],"mappings":"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"}
|
package/src/canvas.test.ts
DELETED
|
@@ -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
|
-
});
|