fabricjs-document-engine 0.6.0 → 0.7.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 (34) hide show
  1. package/README.md +46 -2
  2. package/dist/engine/create-document-engine.cjs +2 -0
  3. package/dist/engine/create-document-engine.d.cts +3 -0
  4. package/dist/engine/create-document-engine.d.ts +3 -0
  5. package/dist/engine/create-document-engine.js +2 -0
  6. package/dist/index.cjs +2 -0
  7. package/dist/index.d.cts +2 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.js +2 -1
  10. package/dist/react/engine-context.cjs +12 -0
  11. package/dist/react/engine-context.d.cts +10 -0
  12. package/dist/react/engine-context.d.ts +10 -0
  13. package/dist/react/engine-context.js +11 -0
  14. package/dist/react/use-document-engine.cjs +23 -0
  15. package/dist/react/use-document-engine.d.cts +6 -0
  16. package/dist/react/use-document-engine.d.ts +6 -0
  17. package/dist/react/use-document-engine.js +23 -0
  18. package/dist/react/use-document-event.cjs +12 -0
  19. package/dist/react/use-document-event.d.cts +4 -0
  20. package/dist/react/use-document-event.d.ts +4 -0
  21. package/dist/react/use-document-event.js +12 -0
  22. package/dist/react/use-document-state.cjs +11 -0
  23. package/dist/react/use-document-state.d.cts +5 -0
  24. package/dist/react/use-document-state.d.ts +5 -0
  25. package/dist/react/use-document-state.js +11 -0
  26. package/dist/react.cjs +11 -0
  27. package/dist/react.d.cts +6 -0
  28. package/dist/react.d.ts +6 -0
  29. package/dist/react.js +6 -0
  30. package/dist/state/document-state-store.cjs +114 -0
  31. package/dist/state/document-state-store.d.cts +28 -0
  32. package/dist/state/document-state-store.d.ts +28 -0
  33. package/dist/state/document-state-store.js +114 -0
  34. package/package.json +26 -4
package/README.md CHANGED
@@ -15,6 +15,7 @@ Fabric already draws objects, handles interaction and serializes to JSON. This p
15
15
  - **Dependable export.** PNG, JPEG, WebP, SVG and editable JSON. You choose the area, scale and background. A preflight check means an export either succeeds or tells you exactly which image or font prevents it.
16
16
  - **Versions and migration.** Keep named versions, restore any of them as a new revision, and open plain Fabric JSON or documents saved by older versions of this package.
17
17
  - **Reliable undo and redo.** One user action is one undo step. Transactions group several code changes into one labelled step, and ids survive undo and redo.
18
+ - **React ready, framework free.** Hooks for React, and a small state store for any other framework. Your toolbar and UI stay yours.
18
19
  - **Fabric 6 and 7.** Every release is tested against both.
19
20
 
20
21
  ## Install
@@ -40,6 +41,42 @@ localStorage.setItem(document.id, JSON.stringify(document));
40
41
  await engine.loadDocument(JSON.parse(localStorage.getItem(document.id)!));
41
42
  ```
42
43
 
44
+ ## React
45
+
46
+ ```tsx
47
+ import { useDocumentEngine, useDocumentState, DocumentEngineProvider, useEngine } from 'fabricjs-document-engine/react';
48
+
49
+ function Editor({ canvas }: { canvas: Canvas | null }) {
50
+ const engine = useDocumentEngine(canvas, { storage, autosave: true });
51
+ return (
52
+ <DocumentEngineProvider engine={engine}>
53
+ <YourToolbar />
54
+ </DocumentEngineProvider>
55
+ );
56
+ }
57
+
58
+ function YourToolbar() {
59
+ const engine = useEngine();
60
+ const state = useDocumentState(engine);
61
+ if (!engine || !state) return null;
62
+ return (
63
+ <>
64
+ <button disabled={!state.canUndo} onClick={() => engine.undo()}>Undo {state.undoLabel}</button>
65
+ <button disabled={!state.isDirty} onClick={() => engine.save()}>Save</button>
66
+ <span>{state.saveStatus}</span>
67
+ </>
68
+ );
69
+ }
70
+ ```
71
+
72
+ - `useDocumentEngine(canvas, options)` creates the engine once your Fabric canvas exists and destroys it on unmount. It returns `null` until then. Options are read when the engine is created.
73
+ - `useDocumentState(engine)` returns `{ documentId, isLoading, loadError, saveStatus, isDirty, isSaving, revision, lastSavedAt, saveError, canUndo, canRedo, undoLabel, redoLabel, assetWarnings }` and re-renders when any of them change.
74
+ - `useDocumentEvent(engine, 'save:error', handler)` subscribes to any event with the latest handler.
75
+ - `DocumentEngineProvider` and `useEngine()` pass the engine to deeply nested toolbars.
76
+ - The React entry is marked `'use client'` for Next.js. React is an optional peer dependency, so the core never imports it.
77
+
78
+ For other frameworks, `createDocumentStateStore(engine)` gives the same state as `{ getSnapshot, subscribe }`, which fits Svelte stores, Vue's `shallowRef` and similar tools.
79
+
43
80
  ## Saving and loading through storage
44
81
 
45
82
  The built-in adapters are the quickest way to start:
@@ -59,6 +96,8 @@ await engine.save();
59
96
 
60
97
  Both adapters also have `listDocuments()` and `deleteDocument(id)`. To build on another key-value store, use `createKeyValueStorage({ read, write, remove, keys }, prefix)`.
61
98
 
99
+ More adapters, including a REST API with revision checks and image uploads, are in [docs/storage-examples.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/storage-examples.md).
100
+
62
101
  ### Your own backend
63
102
 
64
103
  A storage adapter is two functions:
@@ -398,8 +437,9 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
398
437
  | `engine.getHistory()` | Returns `{ undo, redo }` label lists, newest first. |
399
438
  | `engine.clearHistory()` | Forgets all steps. Loading a document or starting a new one also does this. |
400
439
  | `bindKeyboardShortcuts(engine, { target? })` | Adds the undo and redo shortcuts. Returns an unbind function. |
401
- | `engine.on(event, handler)` | Listens to `load:start`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `save:retry`, `save:status`, `assets:warning`, `recovery:checkpoint`, `recovery:restored`, `recovery:error`, `export:success`, `export:error`, `version:created`, `version:restored`, `version:error`, `history:change` or `history:error`. Returns an unsubscribe function. |
440
+ | `engine.on(event, handler)` | Listens to `load:start`, `document:change`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `save:retry`, `save:status`, `assets:warning`, `recovery:checkpoint`, `recovery:restored`, `recovery:error`, `export:success`, `export:error`, `version:created`, `version:restored`, `version:error`, `history:change` or `history:error`. Returns an unsubscribe function. |
402
441
  | `engine.destroy()` | Stops listening to the canvas and cancels a running load. |
442
+ | `createDocumentStateStore(engine)` | Framework-free `{ getSnapshot, subscribe, destroy }` state for toolbars. |
403
443
  | `validateDocument(value)` | Returns a list of issues with the exact path of each problem. |
404
444
 
405
445
  ## Errors
@@ -449,10 +489,14 @@ A failed load never clears or half-fills your canvas.
449
489
  | 5 | Recovery after a refresh or crash | 0.4.0 |
450
490
  | 6 | PNG, JPEG, SVG and JSON export with preflight checks | 0.5.0 |
451
491
  | 7 | Named versions and schema migrations | 0.6.0 |
452
- | 8 | React adapter and examples | |
492
+ | 8 | React adapter and examples | 0.7.0 |
453
493
  | 9 | Hardening and benchmarks | |
454
494
  | 10 | Stable API | 1.0.0 |
455
495
 
496
+ ## Troubleshooting
497
+
498
+ Common problems and fixes are in [docs/troubleshooting.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/troubleshooting.md).
499
+
456
500
  ## License
457
501
 
458
502
  MIT
@@ -201,6 +201,7 @@ function createDocumentEngine(options) {
201
201
  saving.startSession(document.revision ?? 0);
202
202
  canvas.requestRenderAll();
203
203
  if (warnings.length > 0) events.emit("assets:warning", { warnings });
204
+ events.emit("document:change", { documentId: document.id });
204
205
  events.emit("load:success", {
205
206
  document,
206
207
  warnings,
@@ -338,6 +339,7 @@ function createDocumentEngine(options) {
338
339
  history.reset();
339
340
  recovery?.cancel();
340
341
  saving.startSession(0);
342
+ events.emit("document:change", { documentId: documentInfo.id });
341
343
  }
342
344
  function getObjectById(id) {
343
345
  ensureUsable();
@@ -48,6 +48,9 @@ export interface DocumentEngineEvents {
48
48
  "load:start": {
49
49
  documentId: string | undefined;
50
50
  };
51
+ "document:change": {
52
+ documentId: string;
53
+ };
51
54
  "load:success": {
52
55
  document: FabricDocument;
53
56
  warnings: AssetWarning[];
@@ -48,6 +48,9 @@ export interface DocumentEngineEvents {
48
48
  "load:start": {
49
49
  documentId: string | undefined;
50
50
  };
51
+ "document:change": {
52
+ documentId: string;
53
+ };
51
54
  "load:success": {
52
55
  document: FabricDocument;
53
56
  warnings: AssetWarning[];
@@ -201,6 +201,7 @@ function createDocumentEngine(options) {
201
201
  saving.startSession(document.revision ?? 0);
202
202
  canvas.requestRenderAll();
203
203
  if (warnings.length > 0) events.emit("assets:warning", { warnings });
204
+ events.emit("document:change", { documentId: document.id });
204
205
  events.emit("load:success", {
205
206
  document,
206
207
  warnings,
@@ -338,6 +339,7 @@ function createDocumentEngine(options) {
338
339
  history.reset();
339
340
  recovery?.cancel();
340
341
  saving.startSession(0);
342
+ events.emit("document:change", { documentId: documentInfo.id });
341
343
  }
342
344
  function getObjectById(id) {
343
345
  ensureUsable();
package/dist/index.cjs CHANGED
@@ -7,12 +7,14 @@ const require_create_document_engine = require("./engine/create-document-engine.
7
7
  const require_keyboard_shortcuts = require("./history/keyboard-shortcuts.cjs");
8
8
  const require_unsaved_changes_warning = require("./save/unsaved-changes-warning.cjs");
9
9
  const require_download_export = require("./export/download-export.cjs");
10
+ const require_document_state_store = require("./state/document-state-store.cjs");
10
11
  exports.CURRENT_SCHEMA_VERSION = require_document_format.CURRENT_SCHEMA_VERSION;
11
12
  exports.DocumentEngineError = require_errors.DocumentEngineError;
12
13
  exports.bindKeyboardShortcuts = require_keyboard_shortcuts.bindKeyboardShortcuts;
13
14
  exports.bindUnsavedChangesWarning = require_unsaved_changes_warning.bindUnsavedChangesWarning;
14
15
  exports.createConflictError = require_errors.createConflictError;
15
16
  exports.createDocumentEngine = require_create_document_engine.createDocumentEngine;
17
+ exports.createDocumentStateStore = require_document_state_store.createDocumentStateStore;
16
18
  exports.detectSchemaVersion = require_migrate_document.detectSchemaVersion;
17
19
  exports.downloadExport = require_download_export.downloadExport;
18
20
  exports.isDocumentEngineError = require_errors.isDocumentEngineError;
package/dist/index.d.cts CHANGED
@@ -22,4 +22,5 @@ import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "
22
22
  import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.cjs";
23
23
  import { DownloadableExport, downloadExport } from "./export/download-export.cjs";
24
24
  import { Migration, MigrationContext, MigrationResult, detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.cjs";
25
- export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
25
+ import { DocumentState, DocumentStateStore, createDocumentStateStore } from "./state/document-state-store.cjs";
26
+ export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
package/dist/index.d.ts CHANGED
@@ -22,4 +22,5 @@ import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "
22
22
  import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
23
23
  import { DownloadableExport, downloadExport } from "./export/download-export.js";
24
24
  import { Migration, MigrationContext, MigrationResult, detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.js";
25
- export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
25
+ import { DocumentState, DocumentStateStore, createDocumentStateStore } from "./state/document-state-store.js";
26
+ export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
package/dist/index.js CHANGED
@@ -6,4 +6,5 @@ import { createDocumentEngine } from "./engine/create-document-engine.js";
6
6
  import { bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
7
7
  import { bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
8
8
  import { downloadExport } from "./export/download-export.js";
9
- export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
9
+ import { createDocumentStateStore } from "./state/document-state-store.js";
10
+ export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, isDocumentEngineError, migrateDocument, validateDocument };
@@ -0,0 +1,12 @@
1
+ let react = require("react");
2
+ //#region src/react/engine-context.ts
3
+ const EngineContext = (0, react.createContext)(null);
4
+ function DocumentEngineProvider({ engine, children }) {
5
+ return (0, react.createElement)(EngineContext.Provider, { value: engine }, children);
6
+ }
7
+ function useEngine() {
8
+ return (0, react.useContext)(EngineContext);
9
+ }
10
+ //#endregion
11
+ exports.DocumentEngineProvider = DocumentEngineProvider;
12
+ exports.useEngine = useEngine;
@@ -0,0 +1,10 @@
1
+ import { DocumentEngine } from "../engine/create-document-engine.cjs";
2
+ import { ReactNode } from "react";
3
+ //#region src/react/engine-context.d.ts
4
+ export interface DocumentEngineProviderProps {
5
+ engine: DocumentEngine | null;
6
+ children?: ReactNode;
7
+ }
8
+ export declare function DocumentEngineProvider({ engine, children }: DocumentEngineProviderProps): ReactNode;
9
+ export declare function useEngine(): DocumentEngine | null;
10
+ //#endregion
@@ -0,0 +1,10 @@
1
+ import { DocumentEngine } from "../engine/create-document-engine.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/react/engine-context.d.ts
4
+ export interface DocumentEngineProviderProps {
5
+ engine: DocumentEngine | null;
6
+ children?: ReactNode;
7
+ }
8
+ export declare function DocumentEngineProvider({ engine, children }: DocumentEngineProviderProps): ReactNode;
9
+ export declare function useEngine(): DocumentEngine | null;
10
+ //#endregion
@@ -0,0 +1,11 @@
1
+ import { createContext, createElement, useContext } from "react";
2
+ //#region src/react/engine-context.ts
3
+ const EngineContext = createContext(null);
4
+ function DocumentEngineProvider({ engine, children }) {
5
+ return createElement(EngineContext.Provider, { value: engine }, children);
6
+ }
7
+ function useEngine() {
8
+ return useContext(EngineContext);
9
+ }
10
+ //#endregion
11
+ export { DocumentEngineProvider, useEngine };
@@ -0,0 +1,23 @@
1
+ const require_create_document_engine = require("../engine/create-document-engine.cjs");
2
+ let react = require("react");
3
+ //#region src/react/use-document-engine.ts
4
+ function useDocumentEngine(canvas, options = {}) {
5
+ const latestOptions = (0, react.useRef)(options);
6
+ latestOptions.current = options;
7
+ const [engine, setEngine] = (0, react.useState)(null);
8
+ (0, react.useEffect)(() => {
9
+ if (!canvas) return void 0;
10
+ const created = require_create_document_engine.createDocumentEngine({
11
+ ...latestOptions.current,
12
+ canvas
13
+ });
14
+ setEngine(created);
15
+ return () => {
16
+ created.destroy();
17
+ setEngine((current) => current === created ? null : current);
18
+ };
19
+ }, [canvas]);
20
+ return engine;
21
+ }
22
+ //#endregion
23
+ exports.useDocumentEngine = useDocumentEngine;
@@ -0,0 +1,6 @@
1
+ import { DocumentEngine, DocumentEngineOptions } from "../engine/create-document-engine.cjs";
2
+ import { StaticCanvas } from "fabric";
3
+ //#region src/react/use-document-engine.d.ts
4
+ export type ReactEngineOptions = Omit<DocumentEngineOptions, "canvas">;
5
+ export declare function useDocumentEngine(canvas: StaticCanvas | null | undefined, options?: ReactEngineOptions): DocumentEngine | null;
6
+ //#endregion
@@ -0,0 +1,6 @@
1
+ import { DocumentEngine, DocumentEngineOptions } from "../engine/create-document-engine.js";
2
+ import { StaticCanvas } from "fabric";
3
+ //#region src/react/use-document-engine.d.ts
4
+ export type ReactEngineOptions = Omit<DocumentEngineOptions, "canvas">;
5
+ export declare function useDocumentEngine(canvas: StaticCanvas | null | undefined, options?: ReactEngineOptions): DocumentEngine | null;
6
+ //#endregion
@@ -0,0 +1,23 @@
1
+ import { createDocumentEngine } from "../engine/create-document-engine.js";
2
+ import { useEffect, useRef, useState } from "react";
3
+ //#region src/react/use-document-engine.ts
4
+ function useDocumentEngine(canvas, options = {}) {
5
+ const latestOptions = useRef(options);
6
+ latestOptions.current = options;
7
+ const [engine, setEngine] = useState(null);
8
+ useEffect(() => {
9
+ if (!canvas) return void 0;
10
+ const created = createDocumentEngine({
11
+ ...latestOptions.current,
12
+ canvas
13
+ });
14
+ setEngine(created);
15
+ return () => {
16
+ created.destroy();
17
+ setEngine((current) => current === created ? null : current);
18
+ };
19
+ }, [canvas]);
20
+ return engine;
21
+ }
22
+ //#endregion
23
+ export { useDocumentEngine };
@@ -0,0 +1,12 @@
1
+ let react = require("react");
2
+ //#region src/react/use-document-event.ts
3
+ function useDocumentEvent(engine, name, handler) {
4
+ const latestHandler = (0, react.useRef)(handler);
5
+ latestHandler.current = handler;
6
+ (0, react.useEffect)(() => {
7
+ if (!engine) return void 0;
8
+ return engine.on(name, (payload) => latestHandler.current(payload));
9
+ }, [engine, name]);
10
+ }
11
+ //#endregion
12
+ exports.useDocumentEvent = useDocumentEvent;
@@ -0,0 +1,4 @@
1
+ import { DocumentEngine, DocumentEngineEvents } from "../engine/create-document-engine.cjs";
2
+ //#region src/react/use-document-event.d.ts
3
+ export declare function useDocumentEvent<Name extends keyof DocumentEngineEvents>(engine: DocumentEngine | null | undefined, name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): void;
4
+ //#endregion
@@ -0,0 +1,4 @@
1
+ import { DocumentEngine, DocumentEngineEvents } from "../engine/create-document-engine.js";
2
+ //#region src/react/use-document-event.d.ts
3
+ export declare function useDocumentEvent<Name extends keyof DocumentEngineEvents>(engine: DocumentEngine | null | undefined, name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): void;
4
+ //#endregion
@@ -0,0 +1,12 @@
1
+ import { useEffect, useRef } from "react";
2
+ //#region src/react/use-document-event.ts
3
+ function useDocumentEvent(engine, name, handler) {
4
+ const latestHandler = useRef(handler);
5
+ latestHandler.current = handler;
6
+ useEffect(() => {
7
+ if (!engine) return void 0;
8
+ return engine.on(name, (payload) => latestHandler.current(payload));
9
+ }, [engine, name]);
10
+ }
11
+ //#endregion
12
+ export { useDocumentEvent };
@@ -0,0 +1,11 @@
1
+ const require_document_state_store = require("../state/document-state-store.cjs");
2
+ let react = require("react");
3
+ //#region src/react/use-document-state.ts
4
+ const subscribeToNothing = () => () => void 0;
5
+ const nothing = () => null;
6
+ function useDocumentState(engine) {
7
+ const store = (0, react.useMemo)(() => engine ? require_document_state_store.createDocumentStateStore(engine) : null, [engine]);
8
+ return (0, react.useSyncExternalStore)(store ? store.subscribe : subscribeToNothing, store ? store.getSnapshot : nothing, nothing);
9
+ }
10
+ //#endregion
11
+ exports.useDocumentState = useDocumentState;
@@ -0,0 +1,5 @@
1
+ import { DocumentEngine } from "../engine/create-document-engine.cjs";
2
+ import { DocumentState } from "../state/document-state-store.cjs";
3
+ //#region src/react/use-document-state.d.ts
4
+ export declare function useDocumentState(engine: DocumentEngine | null | undefined): DocumentState | null;
5
+ //#endregion
@@ -0,0 +1,5 @@
1
+ import { DocumentEngine } from "../engine/create-document-engine.js";
2
+ import { DocumentState } from "../state/document-state-store.js";
3
+ //#region src/react/use-document-state.d.ts
4
+ export declare function useDocumentState(engine: DocumentEngine | null | undefined): DocumentState | null;
5
+ //#endregion
@@ -0,0 +1,11 @@
1
+ import { createDocumentStateStore } from "../state/document-state-store.js";
2
+ import { useMemo, useSyncExternalStore } from "react";
3
+ //#region src/react/use-document-state.ts
4
+ const subscribeToNothing = () => () => void 0;
5
+ const nothing = () => null;
6
+ function useDocumentState(engine) {
7
+ const store = useMemo(() => engine ? createDocumentStateStore(engine) : null, [engine]);
8
+ return useSyncExternalStore(store ? store.subscribe : subscribeToNothing, store ? store.getSnapshot : nothing, nothing);
9
+ }
10
+ //#endregion
11
+ export { useDocumentState };
package/dist/react.cjs ADDED
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_use_document_engine = require("./react/use-document-engine.cjs");
4
+ const require_use_document_state = require("./react/use-document-state.cjs");
5
+ const require_use_document_event = require("./react/use-document-event.cjs");
6
+ const require_engine_context = require("./react/engine-context.cjs");
7
+ exports.DocumentEngineProvider = require_engine_context.DocumentEngineProvider;
8
+ exports.useDocumentEngine = require_use_document_engine.useDocumentEngine;
9
+ exports.useDocumentEvent = require_use_document_event.useDocumentEvent;
10
+ exports.useDocumentState = require_use_document_state.useDocumentState;
11
+ exports.useEngine = require_engine_context.useEngine;
@@ -0,0 +1,6 @@
1
+ import { DocumentState } from "./state/document-state-store.cjs";
2
+ import { ReactEngineOptions, useDocumentEngine } from "./react/use-document-engine.cjs";
3
+ import { useDocumentState } from "./react/use-document-state.cjs";
4
+ import { useDocumentEvent } from "./react/use-document-event.cjs";
5
+ import { DocumentEngineProvider, DocumentEngineProviderProps, useEngine } from "./react/engine-context.cjs";
6
+ export { DocumentEngineProvider, type DocumentEngineProviderProps, type DocumentState, type ReactEngineOptions, useDocumentEngine, useDocumentEvent, useDocumentState, useEngine };
@@ -0,0 +1,6 @@
1
+ import { DocumentState } from "./state/document-state-store.js";
2
+ import { ReactEngineOptions, useDocumentEngine } from "./react/use-document-engine.js";
3
+ import { useDocumentState } from "./react/use-document-state.js";
4
+ import { useDocumentEvent } from "./react/use-document-event.js";
5
+ import { DocumentEngineProvider, DocumentEngineProviderProps, useEngine } from "./react/engine-context.js";
6
+ export { DocumentEngineProvider, type DocumentEngineProviderProps, type DocumentState, type ReactEngineOptions, useDocumentEngine, useDocumentEvent, useDocumentState, useEngine };
package/dist/react.js ADDED
@@ -0,0 +1,6 @@
1
+ "use client";
2
+ import { useDocumentEngine } from "./react/use-document-engine.js";
3
+ import { useDocumentState } from "./react/use-document-state.js";
4
+ import { useDocumentEvent } from "./react/use-document-event.js";
5
+ import { DocumentEngineProvider, useEngine } from "./react/engine-context.js";
6
+ export { DocumentEngineProvider, useDocumentEngine, useDocumentEvent, useDocumentState, useEngine };
@@ -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.7.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,10 +66,20 @@
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",
@@ -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
  }