fabricjs-document-engine 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +74 -3
  2. package/dist/assets/asset-pipeline.cjs +3 -1
  3. package/dist/assets/asset-pipeline.js +3 -1
  4. package/dist/engine/create-document-engine.cjs +8 -4
  5. package/dist/engine/create-document-engine.d.cts +5 -0
  6. package/dist/engine/create-document-engine.d.ts +5 -0
  7. package/dist/engine/create-document-engine.js +9 -5
  8. package/dist/engine/errors.d.cts +1 -1
  9. package/dist/engine/errors.d.ts +1 -1
  10. package/dist/export/render-export.cjs +3 -15
  11. package/dist/export/render-export.js +4 -15
  12. package/dist/history/apply-state.cjs +3 -1
  13. package/dist/history/apply-state.js +3 -1
  14. package/dist/history/create-history.cjs +4 -1
  15. package/dist/history/create-history.d.cts +1 -0
  16. package/dist/history/create-history.d.ts +1 -0
  17. package/dist/history/create-history.js +4 -1
  18. package/dist/history/history-stack.cjs +44 -8
  19. package/dist/history/history-stack.js +44 -9
  20. package/dist/index.cjs +6 -0
  21. package/dist/index.d.cts +3 -1
  22. package/dist/index.d.ts +3 -1
  23. package/dist/index.js +3 -1
  24. package/dist/react/engine-context.cjs +12 -0
  25. package/dist/react/engine-context.d.cts +10 -0
  26. package/dist/react/engine-context.d.ts +10 -0
  27. package/dist/react/engine-context.js +11 -0
  28. package/dist/react/use-document-engine.cjs +23 -0
  29. package/dist/react/use-document-engine.d.cts +6 -0
  30. package/dist/react/use-document-engine.d.ts +6 -0
  31. package/dist/react/use-document-engine.js +23 -0
  32. package/dist/react/use-document-event.cjs +12 -0
  33. package/dist/react/use-document-event.d.cts +4 -0
  34. package/dist/react/use-document-event.d.ts +4 -0
  35. package/dist/react/use-document-event.js +12 -0
  36. package/dist/react/use-document-state.cjs +11 -0
  37. package/dist/react/use-document-state.d.cts +5 -0
  38. package/dist/react/use-document-state.d.ts +5 -0
  39. package/dist/react/use-document-state.js +11 -0
  40. package/dist/react.cjs +11 -0
  41. package/dist/react.d.cts +6 -0
  42. package/dist/react.d.ts +6 -0
  43. package/dist/react.js +6 -0
  44. package/dist/recovery/recovery-controller.cjs +29 -3
  45. package/dist/recovery/recovery-controller.js +29 -3
  46. package/dist/security/content-limits.cjs +86 -0
  47. package/dist/security/content-limits.d.cts +11 -0
  48. package/dist/security/content-limits.d.ts +11 -0
  49. package/dist/security/content-limits.js +84 -0
  50. package/dist/state/document-state-store.cjs +114 -0
  51. package/dist/state/document-state-store.d.cts +28 -0
  52. package/dist/state/document-state-store.d.ts +28 -0
  53. package/dist/state/document-state-store.js +114 -0
  54. package/package.json +27 -5
@@ -0,0 +1,114 @@
1
+ //#region src/state/document-state-store.ts
2
+ function initialState(engine) {
3
+ const saveState = engine.getSaveState();
4
+ const history = engine.getHistory();
5
+ return {
6
+ documentId: engine.getDocumentInfo().id,
7
+ isLoading: false,
8
+ loadError: void 0,
9
+ saveStatus: saveState.status,
10
+ isDirty: saveState.isDirty,
11
+ isSaving: saveState.isSaving,
12
+ revision: saveState.revision,
13
+ lastSavedAt: saveState.lastSavedAt,
14
+ saveError: saveState.error,
15
+ canUndo: history.undo.length > 0,
16
+ canRedo: history.redo.length > 0,
17
+ undoLabel: history.undo[0],
18
+ redoLabel: history.redo[0],
19
+ assetWarnings: []
20
+ };
21
+ }
22
+ function withLiveEngineValues(engine, previous) {
23
+ return {
24
+ ...previous,
25
+ ...initialState(engine),
26
+ isLoading: previous.isLoading,
27
+ loadError: previous.loadError,
28
+ assetWarnings: previous.assetWarnings
29
+ };
30
+ }
31
+ function isSameState(first, second) {
32
+ return Object.keys(first).every((key) => first[key] === second[key]);
33
+ }
34
+ function createDocumentStateStore(engine) {
35
+ let state = initialState(engine);
36
+ const listeners = /* @__PURE__ */ new Set();
37
+ let detachFromEngine;
38
+ function update(changes) {
39
+ const next = {
40
+ ...state,
41
+ ...changes
42
+ };
43
+ if (isSameState(state, next)) return;
44
+ state = next;
45
+ for (const listener of [...listeners]) listener();
46
+ }
47
+ function attachToEngine() {
48
+ const unsubscribers = [
49
+ engine.on("load:start", () => update({
50
+ isLoading: true,
51
+ loadError: void 0
52
+ })),
53
+ engine.on("load:success", ({ warnings }) => update({
54
+ isLoading: false,
55
+ assetWarnings: warnings
56
+ })),
57
+ engine.on("load:error", ({ error }) => {
58
+ if (error.code !== "LOAD_ABORTED") update({
59
+ isLoading: false,
60
+ loadError: error
61
+ });
62
+ }),
63
+ engine.on("document:change", ({ documentId }) => update({
64
+ documentId,
65
+ loadError: void 0
66
+ })),
67
+ engine.on("assets:warning", ({ warnings }) => update({ assetWarnings: warnings })),
68
+ engine.on("save:status", (saveState) => update({
69
+ saveStatus: saveState.status,
70
+ isDirty: saveState.isDirty,
71
+ isSaving: saveState.isSaving,
72
+ revision: saveState.revision,
73
+ lastSavedAt: saveState.lastSavedAt,
74
+ saveError: saveState.error
75
+ })),
76
+ engine.on("history:change", (history) => update({
77
+ canUndo: history.canUndo,
78
+ canRedo: history.canRedo,
79
+ undoLabel: history.undoLabel,
80
+ redoLabel: history.redoLabel
81
+ }))
82
+ ];
83
+ return () => unsubscribers.forEach((unsubscribe) => unsubscribe());
84
+ }
85
+ function refreshWhileDetached() {
86
+ if (detachFromEngine) return;
87
+ const fresh = withLiveEngineValues(engine, state);
88
+ if (!isSameState(state, fresh)) state = fresh;
89
+ }
90
+ return {
91
+ getSnapshot() {
92
+ refreshWhileDetached();
93
+ return state;
94
+ },
95
+ subscribe(listener) {
96
+ refreshWhileDetached();
97
+ listeners.add(listener);
98
+ detachFromEngine ?? (detachFromEngine = attachToEngine());
99
+ return () => {
100
+ listeners.delete(listener);
101
+ if (listeners.size > 0) return;
102
+ detachFromEngine?.();
103
+ detachFromEngine = void 0;
104
+ };
105
+ },
106
+ destroy() {
107
+ listeners.clear();
108
+ detachFromEngine?.();
109
+ detachFromEngine = void 0;
110
+ }
111
+ };
112
+ }
113
+ //#endregion
114
+ exports.createDocumentStateStore = createDocumentStateStore;
@@ -0,0 +1,28 @@
1
+ import { AssetWarning } from "../assets/asset-pipeline.cjs";
2
+ import { DocumentEngineError } from "../engine/errors.cjs";
3
+ import { SaveStatus } from "../save/save-controller.cjs";
4
+ import { DocumentEngine } from "../engine/create-document-engine.cjs";
5
+ //#region src/state/document-state-store.d.ts
6
+ export interface DocumentState {
7
+ documentId: string;
8
+ isLoading: boolean;
9
+ loadError: DocumentEngineError | undefined;
10
+ saveStatus: SaveStatus;
11
+ isDirty: boolean;
12
+ isSaving: boolean;
13
+ revision: number;
14
+ lastSavedAt: string | undefined;
15
+ saveError: DocumentEngineError | undefined;
16
+ canUndo: boolean;
17
+ canRedo: boolean;
18
+ undoLabel: string | undefined;
19
+ redoLabel: string | undefined;
20
+ assetWarnings: AssetWarning[];
21
+ }
22
+ export interface DocumentStateStore {
23
+ getSnapshot(): DocumentState;
24
+ subscribe(listener: () => void): () => void;
25
+ destroy(): void;
26
+ }
27
+ export declare function createDocumentStateStore(engine: DocumentEngine): DocumentStateStore;
28
+ //#endregion
@@ -0,0 +1,28 @@
1
+ import { AssetWarning } from "../assets/asset-pipeline.js";
2
+ import { DocumentEngineError } from "../engine/errors.js";
3
+ import { SaveStatus } from "../save/save-controller.js";
4
+ import { DocumentEngine } from "../engine/create-document-engine.js";
5
+ //#region src/state/document-state-store.d.ts
6
+ export interface DocumentState {
7
+ documentId: string;
8
+ isLoading: boolean;
9
+ loadError: DocumentEngineError | undefined;
10
+ saveStatus: SaveStatus;
11
+ isDirty: boolean;
12
+ isSaving: boolean;
13
+ revision: number;
14
+ lastSavedAt: string | undefined;
15
+ saveError: DocumentEngineError | undefined;
16
+ canUndo: boolean;
17
+ canRedo: boolean;
18
+ undoLabel: string | undefined;
19
+ redoLabel: string | undefined;
20
+ assetWarnings: AssetWarning[];
21
+ }
22
+ export interface DocumentStateStore {
23
+ getSnapshot(): DocumentState;
24
+ subscribe(listener: () => void): () => void;
25
+ destroy(): void;
26
+ }
27
+ export declare function createDocumentStateStore(engine: DocumentEngine): DocumentStateStore;
28
+ //#endregion
@@ -0,0 +1,114 @@
1
+ //#region src/state/document-state-store.ts
2
+ function initialState(engine) {
3
+ const saveState = engine.getSaveState();
4
+ const history = engine.getHistory();
5
+ return {
6
+ documentId: engine.getDocumentInfo().id,
7
+ isLoading: false,
8
+ loadError: void 0,
9
+ saveStatus: saveState.status,
10
+ isDirty: saveState.isDirty,
11
+ isSaving: saveState.isSaving,
12
+ revision: saveState.revision,
13
+ lastSavedAt: saveState.lastSavedAt,
14
+ saveError: saveState.error,
15
+ canUndo: history.undo.length > 0,
16
+ canRedo: history.redo.length > 0,
17
+ undoLabel: history.undo[0],
18
+ redoLabel: history.redo[0],
19
+ assetWarnings: []
20
+ };
21
+ }
22
+ function withLiveEngineValues(engine, previous) {
23
+ return {
24
+ ...previous,
25
+ ...initialState(engine),
26
+ isLoading: previous.isLoading,
27
+ loadError: previous.loadError,
28
+ assetWarnings: previous.assetWarnings
29
+ };
30
+ }
31
+ function isSameState(first, second) {
32
+ return Object.keys(first).every((key) => first[key] === second[key]);
33
+ }
34
+ function createDocumentStateStore(engine) {
35
+ let state = initialState(engine);
36
+ const listeners = /* @__PURE__ */ new Set();
37
+ let detachFromEngine;
38
+ function update(changes) {
39
+ const next = {
40
+ ...state,
41
+ ...changes
42
+ };
43
+ if (isSameState(state, next)) return;
44
+ state = next;
45
+ for (const listener of [...listeners]) listener();
46
+ }
47
+ function attachToEngine() {
48
+ const unsubscribers = [
49
+ engine.on("load:start", () => update({
50
+ isLoading: true,
51
+ loadError: void 0
52
+ })),
53
+ engine.on("load:success", ({ warnings }) => update({
54
+ isLoading: false,
55
+ assetWarnings: warnings
56
+ })),
57
+ engine.on("load:error", ({ error }) => {
58
+ if (error.code !== "LOAD_ABORTED") update({
59
+ isLoading: false,
60
+ loadError: error
61
+ });
62
+ }),
63
+ engine.on("document:change", ({ documentId }) => update({
64
+ documentId,
65
+ loadError: void 0
66
+ })),
67
+ engine.on("assets:warning", ({ warnings }) => update({ assetWarnings: warnings })),
68
+ engine.on("save:status", (saveState) => update({
69
+ saveStatus: saveState.status,
70
+ isDirty: saveState.isDirty,
71
+ isSaving: saveState.isSaving,
72
+ revision: saveState.revision,
73
+ lastSavedAt: saveState.lastSavedAt,
74
+ saveError: saveState.error
75
+ })),
76
+ engine.on("history:change", (history) => update({
77
+ canUndo: history.canUndo,
78
+ canRedo: history.canRedo,
79
+ undoLabel: history.undoLabel,
80
+ redoLabel: history.redoLabel
81
+ }))
82
+ ];
83
+ return () => unsubscribers.forEach((unsubscribe) => unsubscribe());
84
+ }
85
+ function refreshWhileDetached() {
86
+ if (detachFromEngine) return;
87
+ const fresh = withLiveEngineValues(engine, state);
88
+ if (!isSameState(state, fresh)) state = fresh;
89
+ }
90
+ return {
91
+ getSnapshot() {
92
+ refreshWhileDetached();
93
+ return state;
94
+ },
95
+ subscribe(listener) {
96
+ refreshWhileDetached();
97
+ listeners.add(listener);
98
+ detachFromEngine ?? (detachFromEngine = attachToEngine());
99
+ return () => {
100
+ listeners.delete(listener);
101
+ if (listeners.size > 0) return;
102
+ detachFromEngine?.();
103
+ detachFromEngine = void 0;
104
+ };
105
+ },
106
+ destroy() {
107
+ listeners.clear();
108
+ detachFromEngine?.();
109
+ detachFromEngine = void 0;
110
+ }
111
+ };
112
+ }
113
+ //#endregion
114
+ export { createDocumentStateStore };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabricjs-document-engine",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Turn an existing Fabric.js canvas into a dependable editable document: stable object ids, versioned save and load, undo and redo, safe autosave with conflict protection, custom objects and clear errors.",
5
5
  "keywords": [
6
6
  "fabric",
@@ -19,7 +19,9 @@
19
19
  "editor",
20
20
  "typescript",
21
21
  "storage",
22
- "recovery"
22
+ "recovery",
23
+ "react",
24
+ "react hooks"
23
25
  ],
24
26
  "license": "MIT",
25
27
  "author": "Sohail Khan (https://me.jscrate.dev)",
@@ -64,15 +66,25 @@
64
66
  "default": "./dist/recovery.cjs"
65
67
  }
66
68
  },
69
+ "./react": {
70
+ "import": {
71
+ "types": "./dist/react.d.ts",
72
+ "default": "./dist/react.js"
73
+ },
74
+ "require": {
75
+ "types": "./dist/react.d.cts",
76
+ "default": "./dist/react.cjs"
77
+ }
78
+ },
67
79
  "./package.json": "./package.json"
68
80
  },
69
81
  "scripts": {
70
- "build": "tsdown && npm run lint:publint && npm run lint:attw",
82
+ "build": "tsdown && node scripts/verify-build.mjs && npm run lint:publint && npm run lint:attw",
71
83
  "dev": "tsdown --watch",
72
84
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.tools.json",
73
85
  "test": "vitest run",
74
86
  "test:unit": "vitest run --project unit",
75
- "test:browser": "vitest run --project browser-fabric7 --project browser-fabric6",
87
+ "test:browser": "vitest run --project browser-fabric7 --project browser-fabric6 --project browser-firefox --project browser-webkit",
76
88
  "test:watch": "vitest",
77
89
  "lint:publint": "publint --strict",
78
90
  "lint:attw": "attw --pack . --profile node16",
@@ -80,7 +92,8 @@
80
92
  "release": "changeset publish"
81
93
  },
82
94
  "peerDependencies": {
83
- "fabric": "^6.0.0 || ^7.0.0"
95
+ "fabric": "^6.0.0 || ^7.0.0",
96
+ "react": "^18.0.0 || ^19.0.0"
84
97
  },
85
98
  "publishConfig": {
86
99
  "access": "public",
@@ -101,13 +114,22 @@
101
114
  "@arethetypeswrong/cli": "^0.18.5",
102
115
  "@changesets/cli": "^3.0.3",
103
116
  "@types/node": "^22.20.4",
117
+ "@types/react": "^19.3.0",
118
+ "@types/react-dom": "^19.3.0",
104
119
  "@vitest/browser-playwright": "^5.0.2",
105
120
  "fabric": "^7.4.0",
106
121
  "fabric6": "npm:fabric@^6.9.1",
107
122
  "playwright": "^1.63.0",
108
123
  "publint": "^0.3.24",
124
+ "react": "^19.3.0",
125
+ "react-dom": "^19.3.0",
109
126
  "tsdown": "^0.23.0",
110
127
  "typescript": "^5.9.3",
111
128
  "vitest": "^5.0.2"
129
+ },
130
+ "peerDependenciesMeta": {
131
+ "react": {
132
+ "optional": true
133
+ }
112
134
  }
113
135
  }