@anth0nycodes/fabric-history 0.1.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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(ls:*)",
5
+ "Bash(npm run build:*)",
6
+ "Bash(npm create:*)",
7
+ "Bash(npm install)",
8
+ "Bash(npm install:*)",
9
+ "mcp__context7__query-docs",
10
+ "Bash(pnpm remove:*)",
11
+ "Bash(pnpm add:*)"
12
+ ]
13
+ }
14
+ }
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: Bug Report
3
+ about: Report a bug or unexpected behavior
4
+ title: "[BUG] "
5
+ labels: bug
6
+ assignees: ""
7
+ ---
8
+
9
+ ## Description
10
+
11
+ <!-- A clear and concise description of the bug -->
12
+
13
+ ## Steps to Reproduce
14
+
15
+ 1.
16
+ 2.
17
+ 3.
18
+ 4.
19
+
20
+ ## Expected Behavior
21
+
22
+ <!-- What you expected to happen -->
23
+
24
+ ## Actual Behavior
25
+
26
+ <!-- What actually happened -->
27
+
28
+ ## Environment
29
+
30
+ - **OS:** <!-- e.g., macOS 14.0, Windows 11, Ubuntu 22.04 -->
31
+ - **Node.js version:** <!-- run `node --version` -->
32
+ - **Package version:** <!-- run `npm list -g @anth0nycodes/fabric-history` -->
33
+ - **Installation method:** <!-- npm or pnpm -->
34
+
35
+ ## Error Output
36
+
37
+ <!-- If applicable, paste any error messages or screenshots -->
38
+
39
+ ```
40
+ Paste error output here
41
+ ```
42
+
43
+ ## Additional Context
44
+
45
+ <!-- Add any other context about the problem here -->
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: Feature Request
3
+ about: Suggest a new feature or enhancement
4
+ title: "[FEATURE] "
5
+ labels: enhancement
6
+ assignees: ""
7
+ ---
8
+
9
+ ## Feature Description
10
+
11
+ <!-- A clear and concise description of the feature you'd like to see -->
12
+
13
+ ## Problem/Use Case
14
+
15
+ <!-- What problem does this solve? Why would this feature be useful? -->
16
+
17
+ ## Proposed Solution
18
+
19
+ <!-- How do you envision this feature working? -->
20
+
21
+ ## Alternative Solutions
22
+
23
+ <!-- Have you considered any alternative solutions or features? -->
24
+
25
+ ## Examples
26
+
27
+ <!-- If applicable, provide examples of how this feature would be used -->
28
+
29
+ ## Additional Context
30
+
31
+ <!-- Add any other context, screenshots, or references about the feature request -->
32
+
33
+ ## Would you like to contribute this feature?
34
+
35
+ - [ ] Yes, I'd like to work on this
36
+ - [ ] No, just suggesting
@@ -0,0 +1,44 @@
1
+ ## Description
2
+
3
+ <!-- Provide a brief description of the changes in this PR -->
4
+
5
+ ## Type of Change
6
+
7
+ <!-- Mark the relevant option with an "x" -->
8
+
9
+ - [ ] Bug fix (non-breaking change which fixes an issue)
10
+ - [ ] New feature (non-breaking change which adds functionality)
11
+ - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
12
+ - [ ] Documentation update
13
+ - [ ] Refactoring (no functional changes)
14
+ - [ ] Other (please describe):
15
+
16
+ ## Related Issues
17
+
18
+ <!-- Link to related issues using #issue_number -->
19
+
20
+ Fixes #
21
+
22
+ ## Changes Made
23
+
24
+ <!-- List the specific changes made in this PR -->
25
+
26
+ -
27
+ -
28
+ -
29
+
30
+ ## Testing
31
+
32
+ <!-- Describe the testing you've done -->
33
+
34
+ - [ ] Tests pass locally with `pnpm test`
35
+ - [ ] Type checking passes with `pnpm check`
36
+ - [ ] Build succeeds with `pnpm build`
37
+
38
+ ## Checklist
39
+
40
+ - [ ] My code follows the project's coding style
41
+ - [ ] I have performed a self-review of my own code
42
+ - [ ] I have commented my code where necessary
43
+ - [ ] My changes generate no new warnings or errors
44
+ - [ ] I have updated the documentation (if applicable)
@@ -0,0 +1,23 @@
1
+ # Dependencies
2
+ node_modules
3
+
4
+ # Build outputs
5
+ dist
6
+ build
7
+ .next
8
+ out
9
+
10
+ # Coverage
11
+ coverage
12
+
13
+ # Lock files
14
+ pnpm-lock.yaml
15
+ package-lock.json
16
+ yarn.lock
17
+
18
+ # Environment files
19
+ .env
20
+ .env.*
21
+
22
+ # Generated files
23
+ *.log
package/.prettierrc ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": false,
4
+ "tabWidth": 2,
5
+ "trailingComma": "es5",
6
+ "printWidth": 80,
7
+ "plugins": [
8
+ "@ianvs/prettier-plugin-sort-imports",
9
+ "prettier-plugin-tailwindcss"
10
+ ],
11
+ "importOrder": [
12
+ "^react",
13
+ "^next",
14
+ "<THIRD_PARTY_MODULES>",
15
+ "^@/(.*)$",
16
+ "^[./]"
17
+ ]
18
+ }
@@ -0,0 +1,79 @@
1
+ # Contributing to fabric-history
2
+
3
+ Thank you for your interest in contributing to fabric-history! We welcome contributions from the community.
4
+
5
+ ## Getting Started
6
+
7
+ 1. Fork the repository
8
+ 2. Clone your fork:
9
+ ```bash
10
+ git clone https://github.com/YOUR_USERNAME/fabric-history.git
11
+ cd fabric-history
12
+ ```
13
+ 3. Install dependencies:
14
+ ```bash
15
+ pnpm install
16
+ ```
17
+
18
+ ## Development Workflow
19
+
20
+ 1. Create a new branch for your feature or bug fix:
21
+
22
+ ```bash
23
+ git checkout -b feature/your-feature-name
24
+ ```
25
+
26
+ 2. Make your changes and ensure they follow the project's coding standards
27
+
28
+ 3. Run the test suite to ensure all tests pass:
29
+
30
+ ```bash
31
+ pnpm test
32
+ ```
33
+
34
+ For test coverage:
35
+
36
+ ```bash
37
+ pnpm coverage
38
+ ```
39
+
40
+ 4. Run type checking to ensure there are no TypeScript errors:
41
+
42
+ ```bash
43
+ pnpm check
44
+ ```
45
+
46
+ 5. Build the project to verify everything compiles:
47
+
48
+ ```bash
49
+ pnpm build
50
+ ```
51
+
52
+ 7. Commit your changes with a clear and descriptive commit message:
53
+
54
+ ```bash
55
+ git commit -m "Add: description of your changes"
56
+ ```
57
+
58
+ 8. Push to your fork:
59
+
60
+ ```bash
61
+ git push origin feature/your-feature-name
62
+ ```
63
+
64
+ 9. Open a pull request against the main repository
65
+
66
+ ## Reporting Issues
67
+
68
+ If you find a bug or have a feature request, please create an issue on GitHub with:
69
+
70
+ - A clear title and description
71
+ - Steps to reproduce (for bugs)
72
+ - Expected vs actual behavior
73
+ - Your environment details (fabric.js version, browser, etc.)
74
+
75
+ ## Questions?
76
+
77
+ Feel free to open an issue for any questions or concerns.
78
+
79
+ Thank you for contributing!
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 anth0nycodes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # fabric-history
2
+
3
+ A library built on top of fabric.js that provides undo/redo history management for canvas operations.
4
+
5
+ ## Features
6
+
7
+ - Undo/Redo functionality for fabric.js canvases
8
+ - Automatic history tracking for object additions, removals, and modifications
9
+ - Support for path creation and erasing events
10
+ - Easy integration with existing fabric.js projects
11
+ - Support for fabric.js v6 and v7
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @anth0nycodes/fabric-history
17
+ ```
18
+
19
+ or with pnpm:
20
+
21
+ ```bash
22
+ pnpm add @anth0nycodes/fabric-history
23
+ ```
24
+
25
+ or with yarn:
26
+
27
+ ```bash
28
+ yarn add @anth0nycodes/fabric-history
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```typescript
34
+ import { CanvasWithHistory } from "@anth0nycodes/fabric-history";
35
+ import { Rect } from "fabric";
36
+
37
+ // Create a canvas with history support (same constructor as fabric.Canvas)
38
+ const canvas = new CanvasWithHistory("my-canvas", {
39
+ width: 800,
40
+ height: 600,
41
+ });
42
+
43
+ // Add objects - history is tracked automatically
44
+ const rect = new Rect({
45
+ left: 100,
46
+ top: 100,
47
+ width: 50,
48
+ height: 50,
49
+ fill: "red",
50
+ });
51
+ canvas.add(rect);
52
+
53
+ // Undo the last action
54
+ await canvas.undo();
55
+
56
+ // Redo the undone action
57
+ await canvas.redo();
58
+
59
+ // Clear history if needed
60
+ canvas.clearHistory();
61
+ ```
62
+
63
+ ## API
64
+
65
+ ### `CanvasWithHistory`
66
+
67
+ Extends fabric.js `Canvas` class with history management capabilities.
68
+
69
+ #### Methods
70
+
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 |
79
+
80
+ #### Tracked Events
81
+
82
+ History is automatically saved when these fabric.js events occur:
83
+
84
+ - `object:added` - When an object is added to the canvas
85
+ - `object:removed` - When an object is removed from the canvas
86
+ - `object:modified` - When an object is modified (moved, scaled, rotated, etc.)
87
+ - `path:created` - When a path is created (e.g., freehand drawing)
88
+ - `erasing:end` - When an erasing operation completes
89
+ - `canvas:cleared` - When the canvas is cleared
90
+
91
+ ## Requirements
92
+
93
+ - fabric.js 6.x or 7.x
94
+
95
+ ## Development
96
+
97
+ ```bash
98
+ # Install dependencies
99
+ pnpm install
100
+
101
+ # Run development server
102
+ pnpm dev
103
+
104
+ # Build the project
105
+ pnpm build
106
+
107
+ # Type check
108
+ pnpm check
109
+
110
+ # Run tests
111
+ pnpm test
112
+
113
+ # Run tests with coverage
114
+ pnpm coverage
115
+ ```
116
+
117
+ ## Contributing
118
+
119
+ Contributions are welcome! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to submit pull requests.
120
+
121
+ ## License
122
+
123
+ MIT
124
+
125
+ ## Author
126
+
127
+ Anthony Hoang
@@ -0,0 +1,70 @@
1
+ import { Canvas } from "fabric";
2
+ export declare class CanvasWithHistory extends Canvas {
3
+ private _historyUndo;
4
+ private _historyRedo;
5
+ private _isMoving;
6
+ private _historyProcessing;
7
+ private _historyCurrentState;
8
+ constructor(...args: ConstructorParameters<typeof Canvas>);
9
+ /**
10
+ * Binds all relevant fabric event listeners.
11
+ */
12
+ private _bindEventListeners;
13
+ /**
14
+ * Saves the initial state of the canvas.
15
+ */
16
+ private _saveInitialState;
17
+ /**
18
+ * Starts the movement event listener for objects.
19
+ */
20
+ private _objectMoving;
21
+ /**
22
+ * Handles object modification events, including moving, resizing, rotating,
23
+ * scaling, and skewing.
24
+ *
25
+ * @see {@link https://fabricjs.com/api/type-aliases/ObjectModificationEvents/ | Fabric.js ObjectModificationEvents}
26
+ */
27
+ private _handleObjectModified;
28
+ /**
29
+ * Returns the current state of the canvas as a string.
30
+ *
31
+ * @see {@link https://fabricjs.com/docs/old-docs/fabric-intro-part-3/#serialization | Fabric.js Serialization} for why we use toDatalessJSON() instead of toJSON().
32
+ */
33
+ private _historyCurrent;
34
+ /**
35
+ * Records the current canvas, object, or path state into the history stack for undo/redo.
36
+ */
37
+ private _historySaveAction;
38
+ /**
39
+ * Undo the most recent action.
40
+ */
41
+ undo(): Promise<void>;
42
+ /**
43
+ * Checks for whether or not an action can be undone.
44
+ */
45
+ canUndo(): boolean;
46
+ /**
47
+ * Redo the most recently undone action.
48
+ */
49
+ redo(): Promise<void>;
50
+ /**
51
+ * Checks for whether or not an action can be redone.
52
+ */
53
+ canRedo(): boolean;
54
+ /**
55
+ * Loads a canvas history state from the history stack and renders it on the canvas.
56
+ *
57
+ * @param historyState - The JSON string representing the canvas history state to load.
58
+ */
59
+ private _loadFromHistory;
60
+ /**
61
+ * Clears the history stacks for undo and redo.
62
+ */
63
+ clearHistory(): void;
64
+ /**
65
+ * Unsubscribes all relevant fabric event listeners.
66
+ */
67
+ private _disposeEventListeners;
68
+ dispose(): Promise<boolean>;
69
+ }
70
+ //# sourceMappingURL=canvas.d.ts.map
@@ -0,0 +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,YAAY;IAKZ;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAY9B,OAAO;CAIR"}
package/dist/canvas.js ADDED
@@ -0,0 +1,171 @@
1
+ import { Canvas } from "fabric";
2
+ export class CanvasWithHistory extends Canvas {
3
+ // History stacks
4
+ _historyUndo;
5
+ _historyRedo;
6
+ // Boolean values to determine whether or not we should save to history
7
+ _isMoving;
8
+ _historyProcessing;
9
+ _historyCurrentState;
10
+ constructor(...args) {
11
+ super(...args);
12
+ this._historyUndo = [];
13
+ this._historyRedo = [];
14
+ this._isMoving = false;
15
+ this._historyProcessing = false;
16
+ this._historyCurrentState = this._historyCurrent();
17
+ this._bindEventListeners();
18
+ this._saveInitialState();
19
+ }
20
+ /**
21
+ * Binds all relevant fabric event listeners.
22
+ */
23
+ _bindEventListeners() {
24
+ this.on({
25
+ "path:created": this._historySaveAction.bind(this),
26
+ "erasing:end": this._historySaveAction.bind(this),
27
+ "object:added": this._historySaveAction.bind(this),
28
+ "object:removed": this._historySaveAction.bind(this),
29
+ "object:moving": this._objectMoving.bind(this),
30
+ "object:modified": this._handleObjectModified.bind(this),
31
+ "canvas:cleared": this._historySaveAction.bind(this),
32
+ });
33
+ }
34
+ /**
35
+ * Saves the initial state of the canvas.
36
+ */
37
+ _saveInitialState() {
38
+ const initialState = this._historyCurrent();
39
+ this._historyUndo = [initialState];
40
+ this._historyCurrentState = initialState;
41
+ }
42
+ /**
43
+ * Starts the movement event listener for objects.
44
+ */
45
+ _objectMoving() {
46
+ this._isMoving = true;
47
+ }
48
+ /**
49
+ * Handles object modification events, including moving, resizing, rotating,
50
+ * scaling, and skewing.
51
+ *
52
+ * @see {@link https://fabricjs.com/api/type-aliases/ObjectModificationEvents/ | Fabric.js ObjectModificationEvents}
53
+ */
54
+ _handleObjectModified() {
55
+ // object:moving -> object:modified - modification is triggered as soon as the movement of an object halts
56
+ this._isMoving = false;
57
+ this._historySaveAction();
58
+ }
59
+ /**
60
+ * Returns the current state of the canvas as a string.
61
+ *
62
+ * @see {@link https://fabricjs.com/docs/old-docs/fabric-intro-part-3/#serialization | Fabric.js Serialization} for why we use toDatalessJSON() instead of toJSON().
63
+ */
64
+ _historyCurrent() {
65
+ return JSON.stringify(this.toDatalessJSON());
66
+ }
67
+ /**
68
+ * Records the current canvas, object, or path state into the history stack for undo/redo.
69
+ */
70
+ _historySaveAction() {
71
+ if (this._historyProcessing || this._isMoving)
72
+ return;
73
+ const latestJSON = this._historyCurrent();
74
+ if (this._historyCurrentState === latestJSON)
75
+ return;
76
+ this._historyUndo.push(latestJSON);
77
+ this._historyCurrentState = latestJSON;
78
+ this._historyRedo = [];
79
+ }
80
+ /**
81
+ * Undo the most recent action.
82
+ */
83
+ async undo() {
84
+ if (this._historyUndo.length <= 1)
85
+ return;
86
+ this._historyProcessing = true;
87
+ const poppedState = this._historyUndo.pop();
88
+ if (!poppedState)
89
+ return;
90
+ this._historyRedo.push(poppedState);
91
+ // refresh canvas to load what remains on the undo stack after popping
92
+ const previousState = this._historyUndo[this._historyUndo.length - 1];
93
+ if (!previousState)
94
+ return;
95
+ this._historyCurrentState = previousState;
96
+ await this._loadFromHistory(previousState);
97
+ }
98
+ /**
99
+ * Checks for whether or not an action can be undone.
100
+ */
101
+ canUndo() {
102
+ return this._historyUndo.length > 1;
103
+ }
104
+ /**
105
+ * Redo the most recently undone action.
106
+ */
107
+ async redo() {
108
+ if (this._historyRedo.length === 0)
109
+ return;
110
+ this._historyProcessing = true;
111
+ const poppedState = this._historyRedo.pop();
112
+ if (!poppedState)
113
+ return;
114
+ this._historyUndo.push(poppedState);
115
+ // refresh canvas to load the popped state
116
+ this._historyCurrentState = poppedState;
117
+ await this._loadFromHistory(poppedState);
118
+ }
119
+ /**
120
+ * Checks for whether or not an action can be redone.
121
+ */
122
+ canRedo() {
123
+ return this._historyRedo.length > 0;
124
+ }
125
+ /**
126
+ * Loads a canvas history state from the history stack and renders it on the canvas.
127
+ *
128
+ * @param historyState - The JSON string representing the canvas history state to load.
129
+ */
130
+ async _loadFromHistory(historyState) {
131
+ this.clear();
132
+ this.discardActiveObject();
133
+ try {
134
+ const parsed = JSON.parse(historyState);
135
+ await this.loadFromJSON(parsed);
136
+ this.renderAll();
137
+ }
138
+ catch (error) {
139
+ console.error("Error loading from history:", error);
140
+ }
141
+ finally {
142
+ this._historyProcessing = false;
143
+ }
144
+ }
145
+ /**
146
+ * Clears the history stacks for undo and redo.
147
+ */
148
+ clearHistory() {
149
+ this._historyUndo = [];
150
+ this._historyRedo = [];
151
+ }
152
+ /**
153
+ * Unsubscribes all relevant fabric event listeners.
154
+ */
155
+ _disposeEventListeners() {
156
+ this.off({
157
+ "path:created": this._historySaveAction.bind(this),
158
+ "erasing:end": this._historySaveAction.bind(this),
159
+ "object:added": this._historySaveAction.bind(this),
160
+ "object:removed": this._historySaveAction.bind(this),
161
+ "object:moving": this._objectMoving.bind(this),
162
+ "object:modified": this._handleObjectModified.bind(this),
163
+ "canvas:cleared": this._historySaveAction.bind(this),
164
+ });
165
+ }
166
+ dispose() {
167
+ this._disposeEventListeners();
168
+ return super.dispose();
169
+ }
170
+ }
171
+ //# sourceMappingURL=canvas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas.js","sourceRoot":"","sources":["../src/canvas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=canvas.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas.test.d.ts","sourceRoot":"","sources":["../src/canvas.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,137 @@
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas.test.js","sourceRoot":"","sources":["../src/canvas.test.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAE5B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,QAAQ,CAAC,2CAA2C,EAAE,GAAG,EAAE;IACzD,IAAI,MAAyB,CAAC;IAC9B,IAAI,MAAc,CAAC;IACnB,IAAI,IAAU,CAAC;IACf,IAAI,IAAU,CAAC;IAEf,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,GAAG,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAEzC,MAAM,GAAG,IAAI,MAAM,CAAC;YAClB,MAAM,EAAE,EAAE;YACV,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,GAAG;YACT,GAAG,EAAE,GAAG;SACT,CAAC,CAAC;QAEH,IAAI,GAAG,IAAI,IAAI,CAAC,2BAA2B,EAAE;YAC3C,IAAI,EAAE,EAAE;YACR,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;QAEH,IAAI,GAAG,IAAI,IAAI,CAAC;YACd,IAAI,EAAE,GAAG;YACT,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,KAAK;YACX,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,EAAE;SACX,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEjB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,gEAAgE;QAChE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEjB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,uDAAuD;QACvD,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QACpD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEjB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,2CAA2C;QAC3C,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,aAAa;IACb,IAAI,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEjB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,wDAAwD;QACxD,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC7E,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEjB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,kEAAkE;QAClE,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACtF,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEjB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,wCAAwC;QACxC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAE3C,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,qDAAqD;QACrD,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAEnB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,iEAAiE;QACjE,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEjB,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,6DAA6D;QAC7D,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC;YACvB,IAAI,EAAE,GAAG;YACT,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,MAAM;YACZ,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,EAAE;SACX,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAEpB,+DAA+D;QAC/D,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,yFAAyF;QACzF,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAErC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEpC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAErC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEpC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAEpB,6DAA6D;QAC7D,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { CanvasWithHistory } from "./canvas.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { CanvasWithHistory } from "./canvas.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@anth0nycodes/fabric-history",
3
+ "version": "0.1.0",
4
+ "description": "A library built on top of fabric.js that allows for easy access to canvas history.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "keywords": [
8
+ "fabric",
9
+ "fabric.js",
10
+ "canvas history",
11
+ "canvas undo",
12
+ "canvas redo",
13
+ "canvas"
14
+ ],
15
+ "author": "Anthony Hoang",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/anth0nycodes/fabric-history.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/anth0nycodes/fabric-history/issues"
23
+ },
24
+ "homepage": "https://github.com/anth0nycodes/fabric-history#readme",
25
+ "peerDependencies": {
26
+ "fabric": ">=6.0.0 <8.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@ianvs/prettier-plugin-sort-imports": "^4.7.0",
30
+ "@vitest/coverage-v8": "^4.0.18",
31
+ "canvas": "^3.2.1",
32
+ "jsdom": "^28.0.0",
33
+ "prettier-plugin-tailwindcss": "^0.7.2",
34
+ "tsx": "^4.21.0",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.0.18"
37
+ },
38
+ "scripts": {
39
+ "dev": "tsx src/index.ts",
40
+ "build": "tsc",
41
+ "test": "vitest",
42
+ "coverage": "vitest run --coverage",
43
+ "start": "node dist/index.js",
44
+ "check": "tsc --noEmit",
45
+ "publish:npm": "pnpm publish --access public"
46
+ }
47
+ }
@@ -0,0 +1,178 @@
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
+ });
package/src/canvas.ts ADDED
@@ -0,0 +1,189 @@
1
+ import { Canvas } from "fabric";
2
+
3
+ export class CanvasWithHistory extends Canvas {
4
+ // History stacks
5
+ private _historyUndo: string[];
6
+ private _historyRedo: string[];
7
+
8
+ // Boolean values to determine whether or not we should save to history
9
+ private _isMoving: boolean;
10
+ private _historyProcessing: boolean;
11
+
12
+ private _historyCurrentState: string;
13
+
14
+ constructor(...args: ConstructorParameters<typeof Canvas>) {
15
+ super(...args);
16
+
17
+ this._historyUndo = [];
18
+ this._historyRedo = [];
19
+ this._isMoving = false;
20
+ this._historyProcessing = false;
21
+ this._historyCurrentState = this._historyCurrent();
22
+
23
+ this._bindEventListeners();
24
+ this._saveInitialState();
25
+ }
26
+
27
+ /**
28
+ * Binds all relevant fabric event listeners.
29
+ */
30
+ private _bindEventListeners() {
31
+ this.on({
32
+ "path:created": this._historySaveAction.bind(this),
33
+ "erasing:end": this._historySaveAction.bind(this),
34
+ "object:added": this._historySaveAction.bind(this),
35
+ "object:removed": this._historySaveAction.bind(this),
36
+ "object:moving": this._objectMoving.bind(this),
37
+ "object:modified": this._handleObjectModified.bind(this),
38
+ "canvas:cleared": this._historySaveAction.bind(this),
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Saves the initial state of the canvas.
44
+ */
45
+ private _saveInitialState() {
46
+ const initialState = this._historyCurrent();
47
+ this._historyUndo = [initialState];
48
+ this._historyCurrentState = initialState;
49
+ }
50
+
51
+ /**
52
+ * Starts the movement event listener for objects.
53
+ */
54
+ private _objectMoving() {
55
+ this._isMoving = true;
56
+ }
57
+
58
+ /**
59
+ * Handles object modification events, including moving, resizing, rotating,
60
+ * scaling, and skewing.
61
+ *
62
+ * @see {@link https://fabricjs.com/api/type-aliases/ObjectModificationEvents/ | Fabric.js ObjectModificationEvents}
63
+ */
64
+ private _handleObjectModified() {
65
+ // object:moving -> object:modified - modification is triggered as soon as the movement of an object halts
66
+ this._isMoving = false;
67
+ this._historySaveAction();
68
+ }
69
+
70
+ /**
71
+ * Returns the current state of the canvas as a string.
72
+ *
73
+ * @see {@link https://fabricjs.com/docs/old-docs/fabric-intro-part-3/#serialization | Fabric.js Serialization} for why we use toDatalessJSON() instead of toJSON().
74
+ */
75
+ private _historyCurrent() {
76
+ return JSON.stringify(this.toDatalessJSON());
77
+ }
78
+
79
+ /**
80
+ * Records the current canvas, object, or path state into the history stack for undo/redo.
81
+ */
82
+ private _historySaveAction() {
83
+ if (this._historyProcessing || this._isMoving) return;
84
+ const latestJSON = this._historyCurrent();
85
+
86
+ if (this._historyCurrentState === latestJSON) return;
87
+ this._historyUndo.push(latestJSON);
88
+ this._historyCurrentState = latestJSON;
89
+ this._historyRedo = [];
90
+ }
91
+
92
+ /**
93
+ * Undo the most recent action.
94
+ */
95
+ async undo() {
96
+ if (this._historyUndo.length <= 1) return;
97
+ this._historyProcessing = true;
98
+
99
+ const poppedState = this._historyUndo.pop();
100
+ if (!poppedState) return;
101
+
102
+ this._historyRedo.push(poppedState);
103
+
104
+ // refresh canvas to load what remains on the undo stack after popping
105
+ const previousState = this._historyUndo[this._historyUndo.length - 1];
106
+ if (!previousState) return;
107
+ this._historyCurrentState = previousState;
108
+ await this._loadFromHistory(previousState);
109
+ }
110
+
111
+ /**
112
+ * Checks for whether or not an action can be undone.
113
+ */
114
+ canUndo() {
115
+ return this._historyUndo.length > 1;
116
+ }
117
+
118
+ /**
119
+ * Redo the most recently undone action.
120
+ */
121
+ async redo() {
122
+ if (this._historyRedo.length === 0) return;
123
+ this._historyProcessing = true;
124
+
125
+ const poppedState = this._historyRedo.pop();
126
+ if (!poppedState) return;
127
+
128
+ this._historyUndo.push(poppedState);
129
+
130
+ // refresh canvas to load the popped state
131
+ this._historyCurrentState = poppedState;
132
+ await this._loadFromHistory(poppedState);
133
+ }
134
+
135
+ /**
136
+ * Checks for whether or not an action can be redone.
137
+ */
138
+ canRedo() {
139
+ return this._historyRedo.length > 0;
140
+ }
141
+
142
+ /**
143
+ * Loads a canvas history state from the history stack and renders it on the canvas.
144
+ *
145
+ * @param historyState - The JSON string representing the canvas history state to load.
146
+ */
147
+ private async _loadFromHistory(historyState: string) {
148
+ this.clear();
149
+ this.discardActiveObject();
150
+
151
+ try {
152
+ const parsed = JSON.parse(historyState);
153
+ await this.loadFromJSON(parsed);
154
+ this.renderAll();
155
+ } catch (error) {
156
+ console.error("Error loading from history:", error);
157
+ } finally {
158
+ this._historyProcessing = false;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Clears the history stacks for undo and redo.
164
+ */
165
+ clearHistory() {
166
+ this._historyUndo = [];
167
+ this._historyRedo = [];
168
+ }
169
+
170
+ /**
171
+ * Unsubscribes all relevant fabric event listeners.
172
+ */
173
+ private _disposeEventListeners() {
174
+ this.off({
175
+ "path:created": this._historySaveAction.bind(this),
176
+ "erasing:end": this._historySaveAction.bind(this),
177
+ "object:added": this._historySaveAction.bind(this),
178
+ "object:removed": this._historySaveAction.bind(this),
179
+ "object:moving": this._objectMoving.bind(this),
180
+ "object:modified": this._handleObjectModified.bind(this),
181
+ "canvas:cleared": this._historySaveAction.bind(this),
182
+ });
183
+ }
184
+
185
+ dispose() {
186
+ this._disposeEventListeners();
187
+ return super.dispose();
188
+ }
189
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { CanvasWithHistory } from "./canvas.js";
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "lib": ["ES2022", "dom"],
6
+ "moduleResolution": "bundler",
7
+ "resolveJsonModule": true,
8
+ "skipDefaultLibCheck": true,
9
+ "allowJs": true,
10
+ "checkJs": false,
11
+ "outDir": "./dist",
12
+ "rootDir": "./src",
13
+ "strict": true,
14
+ "esModuleInterop": true,
15
+ "skipLibCheck": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "declaration": true,
18
+ "declarationMap": true,
19
+ "sourceMap": true,
20
+ "types": ["node"]
21
+ },
22
+ "include": ["src/**/*"],
23
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts"]
24
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "jsdom",
6
+ setupFiles: ["./vitest.setup.ts"],
7
+ exclude: ["node_modules", "dist"],
8
+ },
9
+ });
@@ -0,0 +1,34 @@
1
+ import { vi } from "vitest";
2
+
3
+ // Mock canvas getContext for jsdom
4
+ const getContext = vi.fn(() => ({
5
+ scale: vi.fn(),
6
+ clearRect: vi.fn(),
7
+ fillRect: vi.fn(),
8
+ getImageData: vi.fn(() => ({ data: [] })),
9
+ putImageData: vi.fn(),
10
+ createImageData: vi.fn(() => []),
11
+ setTransform: vi.fn(),
12
+ drawImage: vi.fn(),
13
+ save: vi.fn(),
14
+ restore: vi.fn(),
15
+ beginPath: vi.fn(),
16
+ moveTo: vi.fn(),
17
+ lineTo: vi.fn(),
18
+ closePath: vi.fn(),
19
+ stroke: vi.fn(),
20
+ fill: vi.fn(),
21
+ translate: vi.fn(),
22
+ rotate: vi.fn(),
23
+ arc: vi.fn(),
24
+ rect: vi.fn(),
25
+ clip: vi.fn(),
26
+ measureText: vi.fn(() => ({ width: 0 })),
27
+ transform: vi.fn(),
28
+ canvas: {
29
+ width: 300,
30
+ height: 150,
31
+ },
32
+ }));
33
+
34
+ HTMLCanvasElement.prototype.getContext = getContext as any;