fabricjs-document-engine 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +146 -12
  2. package/dist/document/document-format.d.cts +1 -0
  3. package/dist/document/document-format.d.ts +1 -0
  4. package/dist/document/validate-document.cjs +1 -0
  5. package/dist/document/validate-document.js +1 -0
  6. package/dist/engine/create-document-engine.cjs +70 -16
  7. package/dist/engine/create-document-engine.d.cts +28 -5
  8. package/dist/engine/create-document-engine.d.ts +28 -5
  9. package/dist/engine/create-document-engine.js +70 -16
  10. package/dist/engine/errors.cjs +5 -0
  11. package/dist/engine/errors.d.cts +4 -1
  12. package/dist/engine/errors.d.ts +4 -1
  13. package/dist/engine/errors.js +5 -1
  14. package/dist/fabric/fabric-adapter.cjs +7 -2
  15. package/dist/fabric/fabric-adapter.js +7 -3
  16. package/dist/history/apply-state.cjs +48 -0
  17. package/dist/history/apply-state.js +48 -0
  18. package/dist/history/create-history.cjs +193 -0
  19. package/dist/history/create-history.d.cts +12 -0
  20. package/dist/history/create-history.d.ts +12 -0
  21. package/dist/history/create-history.js +192 -0
  22. package/dist/history/history-stack.cjs +27 -0
  23. package/dist/history/history-stack.js +27 -0
  24. package/dist/history/keyboard-shortcuts.cjs +32 -0
  25. package/dist/history/keyboard-shortcuts.d.cts +10 -0
  26. package/dist/history/keyboard-shortcuts.d.ts +10 -0
  27. package/dist/history/keyboard-shortcuts.js +32 -0
  28. package/dist/history/snapshot.cjs +47 -0
  29. package/dist/history/snapshot.js +46 -0
  30. package/dist/index.cjs +5 -0
  31. package/dist/index.d.cts +10 -3
  32. package/dist/index.d.ts +10 -3
  33. package/dist/index.js +4 -2
  34. package/dist/save/autosave-scheduler.cjs +28 -0
  35. package/dist/save/autosave-scheduler.d.cts +6 -0
  36. package/dist/save/autosave-scheduler.d.ts +6 -0
  37. package/dist/save/autosave-scheduler.js +28 -0
  38. package/dist/save/retry.cjs +33 -0
  39. package/dist/save/retry.d.cts +7 -0
  40. package/dist/save/retry.d.ts +7 -0
  41. package/dist/save/retry.js +31 -0
  42. package/dist/save/save-controller.cjs +148 -0
  43. package/dist/save/save-controller.d.cts +20 -0
  44. package/dist/save/save-controller.d.ts +20 -0
  45. package/dist/save/save-controller.js +148 -0
  46. package/dist/save/unsaved-changes-warning.cjs +12 -0
  47. package/dist/save/unsaved-changes-warning.d.cts +6 -0
  48. package/dist/save/unsaved-changes-warning.d.ts +6 -0
  49. package/dist/save/unsaved-changes-warning.js +12 -0
  50. package/dist/storage/key-value-storage.cjs +35 -0
  51. package/dist/storage/key-value-storage.d.cts +14 -0
  52. package/dist/storage/key-value-storage.d.ts +14 -0
  53. package/dist/storage/key-value-storage.js +35 -0
  54. package/dist/storage/local-storage.cjs +24 -0
  55. package/dist/storage/local-storage.d.cts +8 -0
  56. package/dist/storage/local-storage.d.ts +8 -0
  57. package/dist/storage/local-storage.js +24 -0
  58. package/dist/storage/memory-storage.cjs +17 -0
  59. package/dist/storage/memory-storage.d.cts +4 -0
  60. package/dist/storage/memory-storage.d.ts +4 -0
  61. package/dist/storage/memory-storage.js +17 -0
  62. package/dist/storage/storage-contract.d.cts +14 -0
  63. package/dist/storage/storage-contract.d.ts +14 -0
  64. package/dist/storage.cjs +7 -0
  65. package/dist/storage.d.cts +5 -0
  66. package/dist/storage.d.ts +5 -0
  67. package/dist/storage.js +4 -0
  68. package/package.json +14 -3
@@ -7,6 +7,8 @@ import { loadIntoCanvas, serializeCanvas } from "../fabric/fabric-adapter.js";
7
7
  import { readObjectId, writeObjectId } from "../fabric/object-ids.js";
8
8
  import { DocumentEngineError, isDocumentEngineError } from "./errors.js";
9
9
  import { createObjectRegistry } from "../fabric/object-registry.js";
10
+ import { createHistory } from "../history/create-history.js";
11
+ import { createSaveController } from "../save/save-controller.js";
10
12
  import { createEventEmitter } from "./event-emitter.js";
11
13
  //#region src/engine/create-document-engine.ts
12
14
  function describeDocumentId(value) {
@@ -15,6 +17,7 @@ function describeDocumentId(value) {
15
17
  }
16
18
  function createDocumentEngine(options) {
17
19
  const { canvas, storage } = options;
20
+ if (options.autosave && !storage) throw new DocumentEngineError("STORAGE_MISSING", "Autosave needs a storage adapter passed to createDocumentEngine");
18
21
  const registry = createObjectRegistry(options.customObjects);
19
22
  const events = createEventEmitter();
20
23
  const objectsById = /* @__PURE__ */ new Map();
@@ -50,6 +53,27 @@ function createDocumentEngine(options) {
50
53
  canvas.on("object:added", handleObjectAdded);
51
54
  canvas.on("object:removed", handleObjectRemoved);
52
55
  rebuildIndex();
56
+ const history = createHistory({
57
+ canvas,
58
+ limit: options.history?.limit,
59
+ serializeObjects: () => {
60
+ rebuildIndex();
61
+ return serializeCanvas(canvas, registry.propertiesToInclude()).objects;
62
+ },
63
+ onChange: (state) => events.emit("history:change", state),
64
+ onContentChange: () => saving.noteContentChange()
65
+ });
66
+ const saving = createSaveController({
67
+ getStorage: () => requireStorage(),
68
+ createDocument: () => toDocument(),
69
+ retry: options.saveRetry,
70
+ autosave: options.autosave === true ? {} : options.autosave ?? false,
71
+ onStateChange: (state) => events.emit("save:status", state),
72
+ onStart: (document) => events.emit("save:start", { document }),
73
+ onSuccess: (document) => events.emit("save:success", { document }),
74
+ onError: (error) => events.emit("save:error", { error }),
75
+ onRetry: (event) => events.emit("save:retry", event)
76
+ });
53
77
  function toDocument() {
54
78
  ensureUsable();
55
79
  rebuildIndex();
@@ -63,6 +87,7 @@ function createDocumentEngine(options) {
63
87
  id: documentInfo.id,
64
88
  createdAt: documentInfo.createdAt,
65
89
  updatedAt: documentInfo.updatedAt,
90
+ revision: saving.state().revision,
66
91
  canvas: {
67
92
  width: canvas.getWidth(),
68
93
  height: canvas.getHeight()
@@ -99,11 +124,11 @@ function createDocumentEngine(options) {
99
124
  events.emit("load:start", { documentId: describeDocumentId(input) });
100
125
  try {
101
126
  const document = checkDocument(input);
102
- await loadIntoCanvas(canvas, {
127
+ await history.withoutRecording(() => loadIntoCanvas(canvas, {
103
128
  version: document.fabricVersion,
104
129
  background: document.canvas.background,
105
130
  objects: document.objects
106
- }, controller.signal);
131
+ }, controller.signal));
107
132
  if (controller.signal.aborted) throw new Error("aborted");
108
133
  if (loadOptions.restoreCanvasSize ?? true) canvas.setDimensions({
109
134
  width: document.canvas.width,
@@ -116,6 +141,8 @@ function createDocumentEngine(options) {
116
141
  metadata: { ...document.metadata }
117
142
  };
118
143
  rebuildIndex();
144
+ history.reset();
145
+ saving.startSession(document.revision ?? 0);
119
146
  canvas.requestRenderAll();
120
147
  events.emit("load:success", { document });
121
148
  return document;
@@ -139,34 +166,28 @@ function createDocumentEngine(options) {
139
166
  stored = await source.loadDocument(documentId);
140
167
  } catch (error) {
141
168
  const reason = error instanceof Error ? error.message : String(error);
142
- const engineError = new DocumentEngineError("LOAD_FAILED", `Storage could not load "${documentId}": ${reason}`, { cause: error });
169
+ const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("LOAD_FAILED", `Storage could not load "${documentId}": ${reason}`, { cause: error });
143
170
  events.emit("load:error", { error: engineError });
144
171
  throw engineError;
145
172
  }
146
173
  return loadDocument(stored, loadOptions);
147
174
  }
148
- async function save() {
149
- ensureUsable();
150
- const target = requireStorage();
151
- const document = toDocument();
152
- events.emit("save:start", { document });
175
+ function save(saveOptions) {
153
176
  try {
154
- await target.saveDocument(document);
177
+ ensureUsable();
155
178
  } catch (error) {
156
- const reason = error instanceof Error ? error.message : String(error);
157
- const engineError = new DocumentEngineError("SAVE_FAILED", `Storage could not save the document: ${reason}`, { cause: error });
158
- events.emit("save:error", { error: engineError });
159
- throw engineError;
179
+ return Promise.reject(error);
160
180
  }
161
- events.emit("save:success", { document });
162
- return document;
181
+ return saving.save(saveOptions);
163
182
  }
164
183
  function newDocument(newOptions) {
165
184
  ensureUsable();
166
185
  activeLoad?.abort();
167
- canvas.clear();
186
+ history.withoutRecording(() => canvas.clear());
168
187
  objectsById.clear();
169
188
  documentInfo = createDocumentInfo(newOptions);
189
+ history.reset();
190
+ saving.startSession(0);
170
191
  }
171
192
  function getObjectById(id) {
172
193
  ensureUsable();
@@ -175,10 +196,23 @@ function createDocumentEngine(options) {
175
196
  rebuildIndex();
176
197
  return objectsById.get(id);
177
198
  }
199
+ async function travelThroughHistory(direction) {
200
+ ensureUsable();
201
+ try {
202
+ return await history[direction]();
203
+ } catch (error) {
204
+ const reason = error instanceof Error ? error.message : String(error);
205
+ const engineError = new DocumentEngineError("HISTORY_FAILED", `Could not ${direction}: ${reason}`, { cause: error });
206
+ events.emit("history:error", { error: engineError });
207
+ throw engineError;
208
+ }
209
+ }
178
210
  function destroy() {
179
211
  if (destroyed) return;
180
212
  destroyed = true;
181
213
  activeLoad?.abort();
214
+ history.destroy();
215
+ saving.destroy();
182
216
  canvas.off("object:added", handleObjectAdded);
183
217
  canvas.off("object:removed", handleObjectRemoved);
184
218
  objectsById.clear();
@@ -199,17 +233,37 @@ function createDocumentEngine(options) {
199
233
  ...changes
200
234
  }
201
235
  };
236
+ saving.noteContentChange();
202
237
  },
203
238
  newDocument,
204
239
  toDocument,
205
240
  loadDocument,
206
241
  load,
207
242
  save,
243
+ isDirty: () => saving.state().isDirty,
244
+ getSaveState: () => saving.state(),
208
245
  registerObject(definition) {
209
246
  ensureUsable();
210
247
  registry.register(definition);
211
248
  },
212
249
  getObjectById,
250
+ transaction(label, work) {
251
+ ensureUsable();
252
+ return history.transaction(label, work);
253
+ },
254
+ commit(label = "Edit") {
255
+ ensureUsable();
256
+ return history.commit(label);
257
+ },
258
+ undo: () => travelThroughHistory("undo"),
259
+ redo: () => travelThroughHistory("redo"),
260
+ canUndo: () => history.state().canUndo,
261
+ canRedo: () => history.state().canRedo,
262
+ getHistory: () => history.labels(),
263
+ clearHistory() {
264
+ ensureUsable();
265
+ history.reset();
266
+ },
213
267
  on: (name, handler) => events.on(name, handler),
214
268
  destroy
215
269
  };
@@ -7,11 +7,16 @@ var DocumentEngineError = class extends Error {
7
7
  this.issues = details.issues ?? [];
8
8
  this.unknownTypes = details.unknownTypes ?? [];
9
9
  this.cause = details.cause;
10
+ this.retryable = details.retryable ?? false;
10
11
  }
11
12
  };
12
13
  function isDocumentEngineError(value) {
13
14
  return value instanceof DocumentEngineError;
14
15
  }
16
+ function createConflictError(documentId, expectedRevision, actualRevision) {
17
+ return new DocumentEngineError("SAVE_CONFLICT", `Document "${documentId}" was saved somewhere else: expected revision ${expectedRevision} but storage has ${actualRevision}`);
18
+ }
15
19
  //#endregion
16
20
  exports.DocumentEngineError = DocumentEngineError;
21
+ exports.createConflictError = createConflictError;
17
22
  exports.isDocumentEngineError = isDocumentEngineError;
@@ -1,5 +1,5 @@
1
1
  //#region src/engine/errors.d.ts
2
- export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "ENGINE_DESTROYED";
2
+ export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
3
3
  export interface DocumentIssue {
4
4
  code: DocumentErrorCode;
5
5
  path: string;
@@ -9,13 +9,16 @@ export interface DocumentEngineErrorDetails {
9
9
  issues?: DocumentIssue[];
10
10
  unknownTypes?: string[];
11
11
  cause?: unknown;
12
+ retryable?: boolean;
12
13
  }
13
14
  export declare class DocumentEngineError extends Error {
14
15
  readonly code: DocumentErrorCode;
15
16
  readonly issues: DocumentIssue[];
16
17
  readonly unknownTypes: string[];
17
18
  readonly cause: unknown;
19
+ readonly retryable: boolean;
18
20
  constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
19
21
  }
20
22
  export declare function isDocumentEngineError(value: unknown): value is DocumentEngineError;
23
+ export declare function createConflictError(documentId: string, expectedRevision: number, actualRevision: number): DocumentEngineError;
21
24
  //#endregion
@@ -1,5 +1,5 @@
1
1
  //#region src/engine/errors.d.ts
2
- export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "ENGINE_DESTROYED";
2
+ export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
3
3
  export interface DocumentIssue {
4
4
  code: DocumentErrorCode;
5
5
  path: string;
@@ -9,13 +9,16 @@ export interface DocumentEngineErrorDetails {
9
9
  issues?: DocumentIssue[];
10
10
  unknownTypes?: string[];
11
11
  cause?: unknown;
12
+ retryable?: boolean;
12
13
  }
13
14
  export declare class DocumentEngineError extends Error {
14
15
  readonly code: DocumentErrorCode;
15
16
  readonly issues: DocumentIssue[];
16
17
  readonly unknownTypes: string[];
17
18
  readonly cause: unknown;
19
+ readonly retryable: boolean;
18
20
  constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
19
21
  }
20
22
  export declare function isDocumentEngineError(value: unknown): value is DocumentEngineError;
23
+ export declare function createConflictError(documentId: string, expectedRevision: number, actualRevision: number): DocumentEngineError;
21
24
  //#endregion
@@ -7,10 +7,14 @@ var DocumentEngineError = class extends Error {
7
7
  this.issues = details.issues ?? [];
8
8
  this.unknownTypes = details.unknownTypes ?? [];
9
9
  this.cause = details.cause;
10
+ this.retryable = details.retryable ?? false;
10
11
  }
11
12
  };
12
13
  function isDocumentEngineError(value) {
13
14
  return value instanceof DocumentEngineError;
14
15
  }
16
+ function createConflictError(documentId, expectedRevision, actualRevision) {
17
+ return new DocumentEngineError("SAVE_CONFLICT", `Document "${documentId}" was saved somewhere else: expected revision ${expectedRevision} but storage has ${actualRevision}`);
18
+ }
15
19
  //#endregion
16
- export { DocumentEngineError, isDocumentEngineError };
20
+ export { DocumentEngineError, createConflictError, isDocumentEngineError };
@@ -1,4 +1,5 @@
1
1
  const require_walk_objects = require("./walk-objects.cjs");
2
+ let fabric = require("fabric");
2
3
  //#region src/fabric/fabric-adapter.ts
3
4
  function keepOriginExplicit(liveObjects, serializedObjects) {
4
5
  const pending = [];
@@ -56,13 +57,17 @@ function refuseMissingObjects(serialized, created, error) {
56
57
  const actual = countLiveTree(created);
57
58
  if (actual !== expected) throw new Error(`Only ${actual} of ${expected} objects inside the ${describeObject(serialized)} could be created`);
58
59
  }
60
+ const strictReviver = refuseMissingObjects;
61
+ function createObjects(serializedObjects) {
62
+ return fabric.util.enlivenObjects(serializedObjects, { reviver: strictReviver });
63
+ }
59
64
  async function loadIntoCanvas(canvas, content, signal) {
60
65
  const json = { objects: content.objects };
61
66
  if (content.version !== void 0) json.version = content.version;
62
67
  if (content.background !== void 0) json.background = content.background;
63
- const reviver = refuseMissingObjects;
64
- await canvas.loadFromJSON(json, reviver, { signal });
68
+ await canvas.loadFromJSON(json, strictReviver, { signal });
65
69
  }
66
70
  //#endregion
71
+ exports.createObjects = createObjects;
67
72
  exports.loadIntoCanvas = loadIntoCanvas;
68
73
  exports.serializeCanvas = serializeCanvas;
@@ -1,4 +1,5 @@
1
1
  import { childrenOf, walkObjects } from "./walk-objects.js";
2
+ import { util } from "fabric";
2
3
  //#region src/fabric/fabric-adapter.ts
3
4
  function keepOriginExplicit(liveObjects, serializedObjects) {
4
5
  const pending = [];
@@ -56,12 +57,15 @@ function refuseMissingObjects(serialized, created, error) {
56
57
  const actual = countLiveTree(created);
57
58
  if (actual !== expected) throw new Error(`Only ${actual} of ${expected} objects inside the ${describeObject(serialized)} could be created`);
58
59
  }
60
+ const strictReviver = refuseMissingObjects;
61
+ function createObjects(serializedObjects) {
62
+ return util.enlivenObjects(serializedObjects, { reviver: strictReviver });
63
+ }
59
64
  async function loadIntoCanvas(canvas, content, signal) {
60
65
  const json = { objects: content.objects };
61
66
  if (content.version !== void 0) json.version = content.version;
62
67
  if (content.background !== void 0) json.background = content.background;
63
- const reviver = refuseMissingObjects;
64
- await canvas.loadFromJSON(json, reviver, { signal });
68
+ await canvas.loadFromJSON(json, strictReviver, { signal });
65
69
  }
66
70
  //#endregion
67
- export { loadIntoCanvas, serializeCanvas };
71
+ export { createObjects, loadIntoCanvas, serializeCanvas };
@@ -0,0 +1,48 @@
1
+ const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
2
+ const require_object_ids = require("../fabric/object-ids.cjs");
3
+ //#region src/history/apply-state.ts
4
+ function arrangeObjects(current, change, created) {
5
+ const arranged = [];
6
+ for (const object of current) {
7
+ const id = require_object_ids.readObjectId(object) ?? "";
8
+ if (!change.objects.has(id)) {
9
+ arranged.push(object);
10
+ continue;
11
+ }
12
+ const replacement = created.get(id);
13
+ if (replacement) {
14
+ arranged.push(replacement);
15
+ created.delete(id);
16
+ }
17
+ }
18
+ arranged.push(...created.values());
19
+ if (change.order === null) return arranged;
20
+ const position = new Map(change.order.map((id, index) => [id, index]));
21
+ return arranged.sort((first, second) => (position.get(require_object_ids.readObjectId(first) ?? "") ?? Infinity) - (position.get(require_object_ids.readObjectId(second) ?? "") ?? Infinity));
22
+ }
23
+ async function applyStateChange(canvas, change) {
24
+ const serializedObjects = [];
25
+ for (const json of change.objects.values()) if (json !== null) serializedObjects.push(JSON.parse(json));
26
+ const createdObjects = await require_fabric_adapter.createObjects(serializedObjects);
27
+ const createdById = new Map(createdObjects.map((object) => [require_object_ids.readObjectId(object) ?? "", object]));
28
+ canvas.discardActiveObject?.();
29
+ const current = canvas.getObjects();
30
+ const arranged = arrangeObjects(current, change, createdById);
31
+ const kept = new Set(arranged);
32
+ const renderOnAddRemove = canvas.renderOnAddRemove;
33
+ canvas.renderOnAddRemove = false;
34
+ try {
35
+ const outgoing = current.filter((object) => !kept.has(object));
36
+ if (outgoing.length > 0) canvas.remove(...outgoing);
37
+ arranged.forEach((object, index) => {
38
+ if (canvas.getObjects()[index] === object) return;
39
+ if (object.canvas === canvas) canvas.moveObjectTo(object, index);
40
+ else canvas.insertAt(index, object);
41
+ });
42
+ } finally {
43
+ canvas.renderOnAddRemove = renderOnAddRemove;
44
+ }
45
+ canvas.requestRenderAll();
46
+ }
47
+ //#endregion
48
+ exports.applyStateChange = applyStateChange;
@@ -0,0 +1,48 @@
1
+ import { createObjects } from "../fabric/fabric-adapter.js";
2
+ import { readObjectId } from "../fabric/object-ids.js";
3
+ //#region src/history/apply-state.ts
4
+ function arrangeObjects(current, change, created) {
5
+ const arranged = [];
6
+ for (const object of current) {
7
+ const id = readObjectId(object) ?? "";
8
+ if (!change.objects.has(id)) {
9
+ arranged.push(object);
10
+ continue;
11
+ }
12
+ const replacement = created.get(id);
13
+ if (replacement) {
14
+ arranged.push(replacement);
15
+ created.delete(id);
16
+ }
17
+ }
18
+ arranged.push(...created.values());
19
+ if (change.order === null) return arranged;
20
+ const position = new Map(change.order.map((id, index) => [id, index]));
21
+ return arranged.sort((first, second) => (position.get(readObjectId(first) ?? "") ?? Infinity) - (position.get(readObjectId(second) ?? "") ?? Infinity));
22
+ }
23
+ async function applyStateChange(canvas, change) {
24
+ const serializedObjects = [];
25
+ for (const json of change.objects.values()) if (json !== null) serializedObjects.push(JSON.parse(json));
26
+ const createdObjects = await createObjects(serializedObjects);
27
+ const createdById = new Map(createdObjects.map((object) => [readObjectId(object) ?? "", object]));
28
+ canvas.discardActiveObject?.();
29
+ const current = canvas.getObjects();
30
+ const arranged = arrangeObjects(current, change, createdById);
31
+ const kept = new Set(arranged);
32
+ const renderOnAddRemove = canvas.renderOnAddRemove;
33
+ canvas.renderOnAddRemove = false;
34
+ try {
35
+ const outgoing = current.filter((object) => !kept.has(object));
36
+ if (outgoing.length > 0) canvas.remove(...outgoing);
37
+ arranged.forEach((object, index) => {
38
+ if (canvas.getObjects()[index] === object) return;
39
+ if (object.canvas === canvas) canvas.moveObjectTo(object, index);
40
+ else canvas.insertAt(index, object);
41
+ });
42
+ } finally {
43
+ canvas.renderOnAddRemove = renderOnAddRemove;
44
+ }
45
+ canvas.requestRenderAll();
46
+ }
47
+ //#endregion
48
+ export { applyStateChange };
@@ -0,0 +1,193 @@
1
+ const require_apply_state = require("./apply-state.cjs");
2
+ const require_history_stack = require("./history-stack.cjs");
3
+ const require_snapshot = require("./snapshot.cjs");
4
+ //#region src/history/create-history.ts
5
+ const verbsByAction = {
6
+ drag: "Move",
7
+ rotate: "Rotate",
8
+ scale: "Resize",
9
+ scaleX: "Resize",
10
+ scaleY: "Resize",
11
+ resizing: "Resize",
12
+ skewX: "Skew",
13
+ skewY: "Skew"
14
+ };
15
+ function nameOf(object) {
16
+ return (object.constructor.type ?? object.type).toLowerCase();
17
+ }
18
+ function isPromiseLike(value) {
19
+ return typeof value?.then === "function";
20
+ }
21
+ function describePendingChanges(changes) {
22
+ if (new Set(changes.map((change) => change.verb)).size > 1) return "Edit objects";
23
+ const verb = changes[0]?.verb ?? "Edit";
24
+ return changes.length === 1 ? `${verb} ${changes[0].noun}` : `${verb} ${changes.length} objects`;
25
+ }
26
+ function createHistory(options) {
27
+ const { canvas, serializeObjects, onChange, onContentChange } = options;
28
+ const stack = require_history_stack.createHistoryStack(Math.max(1, options.limit ?? 100));
29
+ let snapshot = require_snapshot.createSnapshot(serializeObjects());
30
+ let pendingChanges = [];
31
+ let flushScheduled = false;
32
+ let transactionDepth = 0;
33
+ let pausedDepth = 0;
34
+ let applying = false;
35
+ let queue = Promise.resolve();
36
+ function state() {
37
+ const undoLabels = stack.undoLabels();
38
+ const redoLabels = stack.redoLabels();
39
+ return {
40
+ canUndo: undoLabels.length > 0,
41
+ canRedo: redoLabels.length > 0,
42
+ undoLabel: undoLabels[0],
43
+ redoLabel: redoLabels[0]
44
+ };
45
+ }
46
+ function commit(label) {
47
+ pendingChanges = [];
48
+ const nextSnapshot = require_snapshot.createSnapshot(serializeObjects());
49
+ const difference = require_snapshot.diffSnapshots(snapshot, nextSnapshot);
50
+ snapshot = nextSnapshot;
51
+ if (difference === null) return false;
52
+ stack.record({
53
+ label,
54
+ ...difference
55
+ });
56
+ onContentChange();
57
+ onChange(state());
58
+ return true;
59
+ }
60
+ function isRecording() {
61
+ return !applying && pausedDepth === 0 && transactionDepth === 0;
62
+ }
63
+ function commitPendingChanges() {
64
+ flushScheduled = false;
65
+ if (pendingChanges.length === 0) return;
66
+ if (isRecording()) commit(describePendingChanges(pendingChanges));
67
+ pendingChanges = [];
68
+ }
69
+ function notice(verb, target) {
70
+ if (!isRecording()) return;
71
+ pendingChanges.push({
72
+ verb,
73
+ noun: nameOf(target)
74
+ });
75
+ if (flushScheduled) return;
76
+ flushScheduled = true;
77
+ queueMicrotask(commitPendingChanges);
78
+ }
79
+ const handleAdded = ({ target }) => notice("Add", target);
80
+ const handleRemoved = ({ target }) => notice("Delete", target);
81
+ const handleModified = (event) => {
82
+ const action = event.action ?? event.transform?.action ?? "";
83
+ notice(verbsByAction[action] ?? "Transform", event.target);
84
+ };
85
+ const handleTextEdited = ({ target }) => notice("Edit", target);
86
+ const listeners = [
87
+ ["object:added", handleAdded],
88
+ ["object:removed", handleRemoved],
89
+ ["object:modified", handleModified],
90
+ ["text:editing:exited", handleTextEdited]
91
+ ];
92
+ const eventTarget = canvas;
93
+ listeners.forEach(([name, handler]) => eventTarget.on(name, handler));
94
+ function transaction(label, work) {
95
+ if (transactionDepth === 0) commitPendingChanges();
96
+ transactionDepth += 1;
97
+ const finish = () => {
98
+ transactionDepth -= 1;
99
+ if (transactionDepth === 0) commit(label);
100
+ };
101
+ let result;
102
+ try {
103
+ result = work();
104
+ } catch (error) {
105
+ finish();
106
+ throw error;
107
+ }
108
+ if (!isPromiseLike(result)) {
109
+ finish();
110
+ return result;
111
+ }
112
+ return Promise.resolve(result).then((value) => {
113
+ finish();
114
+ return value;
115
+ }, (error) => {
116
+ finish();
117
+ throw error;
118
+ });
119
+ }
120
+ function withoutRecording(work) {
121
+ pausedDepth += 1;
122
+ let result;
123
+ try {
124
+ result = work();
125
+ } catch (error) {
126
+ pausedDepth -= 1;
127
+ throw error;
128
+ }
129
+ if (!isPromiseLike(result)) {
130
+ pausedDepth -= 1;
131
+ return result;
132
+ }
133
+ return Promise.resolve(result).finally(() => {
134
+ pausedDepth -= 1;
135
+ });
136
+ }
137
+ function runInOrder(task) {
138
+ const run = queue.then(task);
139
+ queue = run.catch(() => void 0);
140
+ return run;
141
+ }
142
+ function travel(takeStep, pickChange, putBack, moveTo) {
143
+ return runInOrder(async () => {
144
+ if (transactionDepth > 0) return false;
145
+ commitPendingChanges();
146
+ const step = takeStep();
147
+ if (step === void 0) return false;
148
+ applying = true;
149
+ try {
150
+ await require_apply_state.applyStateChange(canvas, pickChange(step));
151
+ } catch (error) {
152
+ putBack(step);
153
+ throw error;
154
+ } finally {
155
+ applying = false;
156
+ }
157
+ snapshot = require_snapshot.createSnapshot(serializeObjects());
158
+ moveTo(step);
159
+ onContentChange();
160
+ onChange(state());
161
+ return true;
162
+ });
163
+ }
164
+ return {
165
+ commit(label) {
166
+ if (applying) return false;
167
+ return commit(label);
168
+ },
169
+ transaction,
170
+ undo: () => travel(() => stack.takeUndo(), (step) => step.before, (step) => stack.returnUndo(step), (step) => stack.returnRedo(step)),
171
+ redo: () => travel(() => stack.takeRedo(), (step) => step.after, (step) => stack.returnRedo(step), (step) => stack.returnUndo(step)),
172
+ state,
173
+ labels: () => ({
174
+ undo: stack.undoLabels(),
175
+ redo: stack.redoLabels()
176
+ }),
177
+ withoutRecording,
178
+ reset() {
179
+ pendingChanges = [];
180
+ stack.clear();
181
+ snapshot = require_snapshot.createSnapshot(serializeObjects());
182
+ onChange(state());
183
+ },
184
+ destroy() {
185
+ listeners.forEach(([name, handler]) => eventTarget.off(name, handler));
186
+ pendingChanges = [];
187
+ stack.clear();
188
+ }
189
+ };
190
+ }
191
+ //#endregion
192
+ exports.createHistory = createHistory;
193
+ exports.describePendingChanges = describePendingChanges;
@@ -0,0 +1,12 @@
1
+ import { StaticCanvas } from "fabric";
2
+ //#region src/history/create-history.d.ts
3
+ export interface HistoryOptions {
4
+ limit?: number;
5
+ }
6
+ export interface HistoryState {
7
+ canUndo: boolean;
8
+ canRedo: boolean;
9
+ undoLabel: string | undefined;
10
+ redoLabel: string | undefined;
11
+ }
12
+ //#endregion
@@ -0,0 +1,12 @@
1
+ import { StaticCanvas } from "fabric";
2
+ //#region src/history/create-history.d.ts
3
+ export interface HistoryOptions {
4
+ limit?: number;
5
+ }
6
+ export interface HistoryState {
7
+ canUndo: boolean;
8
+ canRedo: boolean;
9
+ undoLabel: string | undefined;
10
+ redoLabel: string | undefined;
11
+ }
12
+ //#endregion