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.
- package/README.md +146 -12
- package/dist/document/document-format.d.cts +1 -0
- package/dist/document/document-format.d.ts +1 -0
- package/dist/document/validate-document.cjs +1 -0
- package/dist/document/validate-document.js +1 -0
- package/dist/engine/create-document-engine.cjs +70 -16
- package/dist/engine/create-document-engine.d.cts +28 -5
- package/dist/engine/create-document-engine.d.ts +28 -5
- package/dist/engine/create-document-engine.js +70 -16
- package/dist/engine/errors.cjs +5 -0
- package/dist/engine/errors.d.cts +4 -1
- package/dist/engine/errors.d.ts +4 -1
- package/dist/engine/errors.js +5 -1
- package/dist/fabric/fabric-adapter.cjs +7 -2
- package/dist/fabric/fabric-adapter.js +7 -3
- package/dist/history/apply-state.cjs +48 -0
- package/dist/history/apply-state.js +48 -0
- package/dist/history/create-history.cjs +193 -0
- package/dist/history/create-history.d.cts +12 -0
- package/dist/history/create-history.d.ts +12 -0
- package/dist/history/create-history.js +192 -0
- package/dist/history/history-stack.cjs +27 -0
- package/dist/history/history-stack.js +27 -0
- package/dist/history/keyboard-shortcuts.cjs +32 -0
- package/dist/history/keyboard-shortcuts.d.cts +10 -0
- package/dist/history/keyboard-shortcuts.d.ts +10 -0
- package/dist/history/keyboard-shortcuts.js +32 -0
- package/dist/history/snapshot.cjs +47 -0
- package/dist/history/snapshot.js +46 -0
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +4 -2
- package/dist/save/autosave-scheduler.cjs +28 -0
- package/dist/save/autosave-scheduler.d.cts +6 -0
- package/dist/save/autosave-scheduler.d.ts +6 -0
- package/dist/save/autosave-scheduler.js +28 -0
- package/dist/save/retry.cjs +33 -0
- package/dist/save/retry.d.cts +7 -0
- package/dist/save/retry.d.ts +7 -0
- package/dist/save/retry.js +31 -0
- package/dist/save/save-controller.cjs +148 -0
- package/dist/save/save-controller.d.cts +20 -0
- package/dist/save/save-controller.d.ts +20 -0
- package/dist/save/save-controller.js +148 -0
- package/dist/save/unsaved-changes-warning.cjs +12 -0
- package/dist/save/unsaved-changes-warning.d.cts +6 -0
- package/dist/save/unsaved-changes-warning.d.ts +6 -0
- package/dist/save/unsaved-changes-warning.js +12 -0
- package/dist/storage/key-value-storage.cjs +35 -0
- package/dist/storage/key-value-storage.d.cts +14 -0
- package/dist/storage/key-value-storage.d.ts +14 -0
- package/dist/storage/key-value-storage.js +35 -0
- package/dist/storage/local-storage.cjs +24 -0
- package/dist/storage/local-storage.d.cts +8 -0
- package/dist/storage/local-storage.d.ts +8 -0
- package/dist/storage/local-storage.js +24 -0
- package/dist/storage/memory-storage.cjs +17 -0
- package/dist/storage/memory-storage.d.cts +4 -0
- package/dist/storage/memory-storage.d.ts +4 -0
- package/dist/storage/memory-storage.js +17 -0
- package/dist/storage/storage-contract.d.cts +14 -0
- package/dist/storage/storage-contract.d.ts +14 -0
- package/dist/storage.cjs +7 -0
- package/dist/storage.d.cts +5 -0
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +4 -0
- 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
|
-
|
|
149
|
-
ensureUsable();
|
|
150
|
-
const target = requireStorage();
|
|
151
|
-
const document = toDocument();
|
|
152
|
-
events.emit("save:start", { document });
|
|
175
|
+
function save(saveOptions) {
|
|
153
176
|
try {
|
|
154
|
-
|
|
177
|
+
ensureUsable();
|
|
155
178
|
} catch (error) {
|
|
156
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/engine/errors.cjs
CHANGED
|
@@ -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;
|
package/dist/engine/errors.d.cts
CHANGED
|
@@ -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
|
package/dist/engine/errors.d.ts
CHANGED
|
@@ -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
|
package/dist/engine/errors.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|