fabricjs-document-engine 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -3
- package/dist/engine/create-document-engine.cjs +44 -3
- package/dist/engine/create-document-engine.d.cts +17 -0
- package/dist/engine/create-document-engine.d.ts +17 -0
- package/dist/engine/create-document-engine.js +44 -3
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -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 +191 -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 +190 -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 +2 -0
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Fabric already draws objects, handles interaction and serializes to JSON. This p
|
|
|
9
9
|
- **Safe loading.** Documents are validated first. Unknown object types are refused before the canvas is touched. A missing image fails the load instead of silently disappearing. When loads overlap, the newest one wins.
|
|
10
10
|
- **Custom objects.** Register your own Fabric classes and the extra properties they need to keep.
|
|
11
11
|
- **Your storage.** Plug in any backend with two functions. No hosted service is needed.
|
|
12
|
+
- **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.
|
|
12
13
|
- **Fabric 6 and 7.** Every release is tested against both.
|
|
13
14
|
|
|
14
15
|
## Install
|
|
@@ -68,6 +69,54 @@ const engine = createDocumentEngine({
|
|
|
68
69
|
|
|
69
70
|
If a document contains a type that has not been registered, loading fails with `UNKNOWN_OBJECT_TYPE` and lists the missing types. Your object is never turned into something else.
|
|
70
71
|
|
|
72
|
+
## Undo and redo
|
|
73
|
+
|
|
74
|
+
History is on by default. The engine records these automatically:
|
|
75
|
+
|
|
76
|
+
- adding and deleting objects, where several changes in the same tick become one step
|
|
77
|
+
- pointer moves, resizes and rotations (Fabric's `object:modified`)
|
|
78
|
+
- finished text editing
|
|
79
|
+
|
|
80
|
+
Fabric fires no events for changes your code makes directly, such as `object.set('fill', 'red')` or `canvas.bringObjectForward(object)`. Wrap them in a transaction or call `commit`:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
engine.transaction('Arrange furniture', () => {
|
|
84
|
+
chair.set({ left: 120, top: 80 });
|
|
85
|
+
table.set('fill', 'oak');
|
|
86
|
+
canvas.bringObjectToFront(table);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
canvas.sendObjectBackwards(rug);
|
|
90
|
+
engine.commit('Send rug backwards');
|
|
91
|
+
|
|
92
|
+
await engine.undo();
|
|
93
|
+
await engine.redo();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- Transactions can be nested, and the outermost label is used. They can also be async: `await engine.transaction('Import', async () => { ... })`.
|
|
97
|
+
- Grouping and ungrouping are ordinary changes to the object list. Do the remove and the add inside one transaction and they take one undo step.
|
|
98
|
+
- Undo and redo rebuild the changed objects from their saved state, so they come back as new instances with the same ids. Look them up again with `engine.getObjectById(id)` rather than keeping old references.
|
|
99
|
+
- Keep the last 50 steps with `createDocumentEngine({ canvas, history: { limit: 50 } })`. The default is 100.
|
|
100
|
+
|
|
101
|
+
### Keyboard shortcuts
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { bindKeyboardShortcuts } from 'fabricjs-document-engine';
|
|
105
|
+
|
|
106
|
+
const unbind = bindKeyboardShortcuts(engine);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Ctrl/Cmd + Z undoes. Ctrl/Cmd + Shift + Z and Ctrl + Y redo. Shortcuts are ignored while the user is typing in an input, a textarea, a contenteditable element or Fabric text, so native text undo keeps working there. Pass `{ target: element }` to listen somewhere other than `window`.
|
|
110
|
+
|
|
111
|
+
### Toolbar state
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
engine.on('history:change', ({ canUndo, canRedo, undoLabel, redoLabel }) => {
|
|
115
|
+
undoButton.disabled = !canUndo;
|
|
116
|
+
undoButton.title = undoLabel ? `Undo ${undoLabel}` : 'Undo';
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
71
120
|
## Document format
|
|
72
121
|
|
|
73
122
|
```ts
|
|
@@ -98,7 +147,14 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
98
147
|
| `engine.updateMetadata(changes)` | Merges changes into the document metadata. |
|
|
99
148
|
| `engine.getObjectById(id)` | Finds any object by id, including objects inside groups. |
|
|
100
149
|
| `engine.registerObject({ fabricClass, properties })` | Registers a custom class after the engine is created. |
|
|
101
|
-
| `engine.
|
|
150
|
+
| `engine.transaction(label, work)` | Runs `work` and records everything it changed as one undo step. Returns what `work` returns. |
|
|
151
|
+
| `engine.commit(label?)` | Records changes made since the last step. Returns `false` when nothing changed. |
|
|
152
|
+
| `engine.undo()` / `engine.redo()` | Resolves to `true` when a step was applied. Calls run one after another. |
|
|
153
|
+
| `engine.canUndo()` / `engine.canRedo()` | Tells you whether a step is available. |
|
|
154
|
+
| `engine.getHistory()` | Returns `{ undo, redo }` label lists, newest first. |
|
|
155
|
+
| `engine.clearHistory()` | Forgets all steps. Loading a document or starting a new one also does this. |
|
|
156
|
+
| `bindKeyboardShortcuts(engine, { target? })` | Adds the undo and redo shortcuts. Returns an unbind function. |
|
|
157
|
+
| `engine.on(event, handler)` | Listens to `load:start`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `history:change` or `history:error`. Returns an unsubscribe function. |
|
|
102
158
|
| `engine.destroy()` | Stops listening to the canvas and cancels a running load. |
|
|
103
159
|
| `validateDocument(value)` | Returns a list of issues with the exact path of each problem. |
|
|
104
160
|
|
|
@@ -114,6 +170,7 @@ Every failure is a `DocumentEngineError` with a `code` you can switch on:
|
|
|
114
170
|
| `LOAD_FAILED` | Fabric or your storage could not load the document, for example because an image is missing. `error.cause` holds the original error. |
|
|
115
171
|
| `LOAD_ABORTED` | A newer load started before this one finished. |
|
|
116
172
|
| `SAVE_FAILED` | Your storage adapter rejected the save. |
|
|
173
|
+
| `HISTORY_FAILED` | Undo or redo could not rebuild an object, for example because an image is gone. The step is kept and the canvas is unchanged. |
|
|
117
174
|
| `STORAGE_MISSING` | `load` or `save` was called without a storage adapter. |
|
|
118
175
|
| `INVALID_CUSTOM_OBJECT` | A registered class has no static `type`, or it does not extend a Fabric class. |
|
|
119
176
|
| `ENGINE_DESTROYED` | The engine was used after `destroy()`. |
|
|
@@ -124,8 +181,8 @@ A failed load never clears or half-fills your canvas.
|
|
|
124
181
|
|
|
125
182
|
| Version | Focus |
|
|
126
183
|
| --- | --- |
|
|
127
|
-
| 0.1 | Document foundation: ids, save and load, validation, custom objects |
|
|
128
|
-
| 0.2 | Undo and redo with transactions |
|
|
184
|
+
| 0.1 ✓ | Document foundation: ids, save and load, validation, custom objects |
|
|
185
|
+
| 0.2 ✓ | Undo and redo with transactions |
|
|
129
186
|
| 0.3 | Safe saving: dirty state, autosave, stale-response protection |
|
|
130
187
|
| 0.4 | Assets and fonts |
|
|
131
188
|
| 0.5 | Recovery after a refresh or crash |
|
|
@@ -7,6 +7,7 @@ const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
|
|
|
7
7
|
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
8
8
|
const require_errors = require("./errors.cjs");
|
|
9
9
|
const require_object_registry = require("../fabric/object-registry.cjs");
|
|
10
|
+
const require_create_history = require("../history/create-history.cjs");
|
|
10
11
|
const require_event_emitter = require("./event-emitter.cjs");
|
|
11
12
|
//#region src/engine/create-document-engine.ts
|
|
12
13
|
function describeDocumentId(value) {
|
|
@@ -50,6 +51,15 @@ function createDocumentEngine(options) {
|
|
|
50
51
|
canvas.on("object:added", handleObjectAdded);
|
|
51
52
|
canvas.on("object:removed", handleObjectRemoved);
|
|
52
53
|
rebuildIndex();
|
|
54
|
+
const history = require_create_history.createHistory({
|
|
55
|
+
canvas,
|
|
56
|
+
limit: options.history?.limit,
|
|
57
|
+
serializeObjects: () => {
|
|
58
|
+
rebuildIndex();
|
|
59
|
+
return require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude()).objects;
|
|
60
|
+
},
|
|
61
|
+
onChange: (state) => events.emit("history:change", state)
|
|
62
|
+
});
|
|
53
63
|
function toDocument() {
|
|
54
64
|
ensureUsable();
|
|
55
65
|
rebuildIndex();
|
|
@@ -99,11 +109,11 @@ function createDocumentEngine(options) {
|
|
|
99
109
|
events.emit("load:start", { documentId: describeDocumentId(input) });
|
|
100
110
|
try {
|
|
101
111
|
const document = checkDocument(input);
|
|
102
|
-
await require_fabric_adapter.loadIntoCanvas(canvas, {
|
|
112
|
+
await history.withoutRecording(() => require_fabric_adapter.loadIntoCanvas(canvas, {
|
|
103
113
|
version: document.fabricVersion,
|
|
104
114
|
background: document.canvas.background,
|
|
105
115
|
objects: document.objects
|
|
106
|
-
}, controller.signal);
|
|
116
|
+
}, controller.signal));
|
|
107
117
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
108
118
|
if (loadOptions.restoreCanvasSize ?? true) canvas.setDimensions({
|
|
109
119
|
width: document.canvas.width,
|
|
@@ -116,6 +126,7 @@ function createDocumentEngine(options) {
|
|
|
116
126
|
metadata: { ...document.metadata }
|
|
117
127
|
};
|
|
118
128
|
rebuildIndex();
|
|
129
|
+
history.reset();
|
|
119
130
|
canvas.requestRenderAll();
|
|
120
131
|
events.emit("load:success", { document });
|
|
121
132
|
return document;
|
|
@@ -164,9 +175,10 @@ function createDocumentEngine(options) {
|
|
|
164
175
|
function newDocument(newOptions) {
|
|
165
176
|
ensureUsable();
|
|
166
177
|
activeLoad?.abort();
|
|
167
|
-
canvas.clear();
|
|
178
|
+
history.withoutRecording(() => canvas.clear());
|
|
168
179
|
objectsById.clear();
|
|
169
180
|
documentInfo = require_create_document.createDocumentInfo(newOptions);
|
|
181
|
+
history.reset();
|
|
170
182
|
}
|
|
171
183
|
function getObjectById(id) {
|
|
172
184
|
ensureUsable();
|
|
@@ -175,10 +187,22 @@ function createDocumentEngine(options) {
|
|
|
175
187
|
rebuildIndex();
|
|
176
188
|
return objectsById.get(id);
|
|
177
189
|
}
|
|
190
|
+
async function travelThroughHistory(direction) {
|
|
191
|
+
ensureUsable();
|
|
192
|
+
try {
|
|
193
|
+
return await history[direction]();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
196
|
+
const engineError = new require_errors.DocumentEngineError("HISTORY_FAILED", `Could not ${direction}: ${reason}`, { cause: error });
|
|
197
|
+
events.emit("history:error", { error: engineError });
|
|
198
|
+
throw engineError;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
178
201
|
function destroy() {
|
|
179
202
|
if (destroyed) return;
|
|
180
203
|
destroyed = true;
|
|
181
204
|
activeLoad?.abort();
|
|
205
|
+
history.destroy();
|
|
182
206
|
canvas.off("object:added", handleObjectAdded);
|
|
183
207
|
canvas.off("object:removed", handleObjectRemoved);
|
|
184
208
|
objectsById.clear();
|
|
@@ -210,6 +234,23 @@ function createDocumentEngine(options) {
|
|
|
210
234
|
registry.register(definition);
|
|
211
235
|
},
|
|
212
236
|
getObjectById,
|
|
237
|
+
transaction(label, work) {
|
|
238
|
+
ensureUsable();
|
|
239
|
+
return history.transaction(label, work);
|
|
240
|
+
},
|
|
241
|
+
commit(label = "Edit") {
|
|
242
|
+
ensureUsable();
|
|
243
|
+
return history.commit(label);
|
|
244
|
+
},
|
|
245
|
+
undo: () => travelThroughHistory("undo"),
|
|
246
|
+
redo: () => travelThroughHistory("redo"),
|
|
247
|
+
canUndo: () => history.state().canUndo,
|
|
248
|
+
canRedo: () => history.state().canRedo,
|
|
249
|
+
getHistory: () => history.labels(),
|
|
250
|
+
clearHistory() {
|
|
251
|
+
ensureUsable();
|
|
252
|
+
history.reset();
|
|
253
|
+
},
|
|
213
254
|
on: (name, handler) => events.on(name, handler),
|
|
214
255
|
destroy
|
|
215
256
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DocumentInfo, FabricDocument } from "../document/document-format.cjs";
|
|
2
2
|
import { NewDocumentOptions } from "../document/create-document.cjs";
|
|
3
3
|
import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
|
|
4
|
+
import { HistoryOptions, HistoryState } from "../history/create-history.cjs";
|
|
4
5
|
import { DocumentEngineError } from "./errors.cjs";
|
|
5
6
|
import { Unsubscribe } from "./event-emitter.cjs";
|
|
6
7
|
import { FabricObject, StaticCanvas } from "fabric";
|
|
@@ -14,6 +15,7 @@ export interface DocumentEngineOptions {
|
|
|
14
15
|
storage?: DocumentStorage;
|
|
15
16
|
customObjects?: CustomObjectDefinition[];
|
|
16
17
|
document?: NewDocumentOptions;
|
|
18
|
+
history?: HistoryOptions;
|
|
17
19
|
}
|
|
18
20
|
export interface LoadOptions {
|
|
19
21
|
restoreCanvasSize?: boolean;
|
|
@@ -37,6 +39,10 @@ export interface DocumentEngineEvents {
|
|
|
37
39
|
"save:error": {
|
|
38
40
|
error: DocumentEngineError;
|
|
39
41
|
};
|
|
42
|
+
"history:change": HistoryState;
|
|
43
|
+
"history:error": {
|
|
44
|
+
error: DocumentEngineError;
|
|
45
|
+
};
|
|
40
46
|
}
|
|
41
47
|
export interface DocumentEngine {
|
|
42
48
|
readonly canvas: StaticCanvas;
|
|
@@ -48,6 +54,17 @@ export interface DocumentEngine {
|
|
|
48
54
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
49
55
|
save(): Promise<FabricDocument>;
|
|
50
56
|
registerObject(definition: CustomObjectDefinition): void;
|
|
57
|
+
transaction<Result>(label: string, work: () => Result): Result;
|
|
58
|
+
commit(label?: string): boolean;
|
|
59
|
+
undo(): Promise<boolean>;
|
|
60
|
+
redo(): Promise<boolean>;
|
|
61
|
+
canUndo(): boolean;
|
|
62
|
+
canRedo(): boolean;
|
|
63
|
+
getHistory(): {
|
|
64
|
+
undo: string[];
|
|
65
|
+
redo: string[];
|
|
66
|
+
};
|
|
67
|
+
clearHistory(): void;
|
|
51
68
|
getObjectById(id: string): FabricObject | undefined;
|
|
52
69
|
on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
|
|
53
70
|
destroy(): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DocumentInfo, FabricDocument } from "../document/document-format.js";
|
|
2
2
|
import { NewDocumentOptions } from "../document/create-document.js";
|
|
3
3
|
import { CustomObjectDefinition } from "../fabric/object-registry.js";
|
|
4
|
+
import { HistoryOptions, HistoryState } from "../history/create-history.js";
|
|
4
5
|
import { DocumentEngineError } from "./errors.js";
|
|
5
6
|
import { Unsubscribe } from "./event-emitter.js";
|
|
6
7
|
import { FabricObject, StaticCanvas } from "fabric";
|
|
@@ -14,6 +15,7 @@ export interface DocumentEngineOptions {
|
|
|
14
15
|
storage?: DocumentStorage;
|
|
15
16
|
customObjects?: CustomObjectDefinition[];
|
|
16
17
|
document?: NewDocumentOptions;
|
|
18
|
+
history?: HistoryOptions;
|
|
17
19
|
}
|
|
18
20
|
export interface LoadOptions {
|
|
19
21
|
restoreCanvasSize?: boolean;
|
|
@@ -37,6 +39,10 @@ export interface DocumentEngineEvents {
|
|
|
37
39
|
"save:error": {
|
|
38
40
|
error: DocumentEngineError;
|
|
39
41
|
};
|
|
42
|
+
"history:change": HistoryState;
|
|
43
|
+
"history:error": {
|
|
44
|
+
error: DocumentEngineError;
|
|
45
|
+
};
|
|
40
46
|
}
|
|
41
47
|
export interface DocumentEngine {
|
|
42
48
|
readonly canvas: StaticCanvas;
|
|
@@ -48,6 +54,17 @@ export interface DocumentEngine {
|
|
|
48
54
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
49
55
|
save(): Promise<FabricDocument>;
|
|
50
56
|
registerObject(definition: CustomObjectDefinition): void;
|
|
57
|
+
transaction<Result>(label: string, work: () => Result): Result;
|
|
58
|
+
commit(label?: string): boolean;
|
|
59
|
+
undo(): Promise<boolean>;
|
|
60
|
+
redo(): Promise<boolean>;
|
|
61
|
+
canUndo(): boolean;
|
|
62
|
+
canRedo(): boolean;
|
|
63
|
+
getHistory(): {
|
|
64
|
+
undo: string[];
|
|
65
|
+
redo: string[];
|
|
66
|
+
};
|
|
67
|
+
clearHistory(): void;
|
|
51
68
|
getObjectById(id: string): FabricObject | undefined;
|
|
52
69
|
on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
|
|
53
70
|
destroy(): void;
|
|
@@ -7,6 +7,7 @@ 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";
|
|
10
11
|
import { createEventEmitter } from "./event-emitter.js";
|
|
11
12
|
//#region src/engine/create-document-engine.ts
|
|
12
13
|
function describeDocumentId(value) {
|
|
@@ -50,6 +51,15 @@ function createDocumentEngine(options) {
|
|
|
50
51
|
canvas.on("object:added", handleObjectAdded);
|
|
51
52
|
canvas.on("object:removed", handleObjectRemoved);
|
|
52
53
|
rebuildIndex();
|
|
54
|
+
const history = createHistory({
|
|
55
|
+
canvas,
|
|
56
|
+
limit: options.history?.limit,
|
|
57
|
+
serializeObjects: () => {
|
|
58
|
+
rebuildIndex();
|
|
59
|
+
return serializeCanvas(canvas, registry.propertiesToInclude()).objects;
|
|
60
|
+
},
|
|
61
|
+
onChange: (state) => events.emit("history:change", state)
|
|
62
|
+
});
|
|
53
63
|
function toDocument() {
|
|
54
64
|
ensureUsable();
|
|
55
65
|
rebuildIndex();
|
|
@@ -99,11 +109,11 @@ function createDocumentEngine(options) {
|
|
|
99
109
|
events.emit("load:start", { documentId: describeDocumentId(input) });
|
|
100
110
|
try {
|
|
101
111
|
const document = checkDocument(input);
|
|
102
|
-
await loadIntoCanvas(canvas, {
|
|
112
|
+
await history.withoutRecording(() => loadIntoCanvas(canvas, {
|
|
103
113
|
version: document.fabricVersion,
|
|
104
114
|
background: document.canvas.background,
|
|
105
115
|
objects: document.objects
|
|
106
|
-
}, controller.signal);
|
|
116
|
+
}, controller.signal));
|
|
107
117
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
108
118
|
if (loadOptions.restoreCanvasSize ?? true) canvas.setDimensions({
|
|
109
119
|
width: document.canvas.width,
|
|
@@ -116,6 +126,7 @@ function createDocumentEngine(options) {
|
|
|
116
126
|
metadata: { ...document.metadata }
|
|
117
127
|
};
|
|
118
128
|
rebuildIndex();
|
|
129
|
+
history.reset();
|
|
119
130
|
canvas.requestRenderAll();
|
|
120
131
|
events.emit("load:success", { document });
|
|
121
132
|
return document;
|
|
@@ -164,9 +175,10 @@ function createDocumentEngine(options) {
|
|
|
164
175
|
function newDocument(newOptions) {
|
|
165
176
|
ensureUsable();
|
|
166
177
|
activeLoad?.abort();
|
|
167
|
-
canvas.clear();
|
|
178
|
+
history.withoutRecording(() => canvas.clear());
|
|
168
179
|
objectsById.clear();
|
|
169
180
|
documentInfo = createDocumentInfo(newOptions);
|
|
181
|
+
history.reset();
|
|
170
182
|
}
|
|
171
183
|
function getObjectById(id) {
|
|
172
184
|
ensureUsable();
|
|
@@ -175,10 +187,22 @@ function createDocumentEngine(options) {
|
|
|
175
187
|
rebuildIndex();
|
|
176
188
|
return objectsById.get(id);
|
|
177
189
|
}
|
|
190
|
+
async function travelThroughHistory(direction) {
|
|
191
|
+
ensureUsable();
|
|
192
|
+
try {
|
|
193
|
+
return await history[direction]();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
196
|
+
const engineError = new DocumentEngineError("HISTORY_FAILED", `Could not ${direction}: ${reason}`, { cause: error });
|
|
197
|
+
events.emit("history:error", { error: engineError });
|
|
198
|
+
throw engineError;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
178
201
|
function destroy() {
|
|
179
202
|
if (destroyed) return;
|
|
180
203
|
destroyed = true;
|
|
181
204
|
activeLoad?.abort();
|
|
205
|
+
history.destroy();
|
|
182
206
|
canvas.off("object:added", handleObjectAdded);
|
|
183
207
|
canvas.off("object:removed", handleObjectRemoved);
|
|
184
208
|
objectsById.clear();
|
|
@@ -210,6 +234,23 @@ function createDocumentEngine(options) {
|
|
|
210
234
|
registry.register(definition);
|
|
211
235
|
},
|
|
212
236
|
getObjectById,
|
|
237
|
+
transaction(label, work) {
|
|
238
|
+
ensureUsable();
|
|
239
|
+
return history.transaction(label, work);
|
|
240
|
+
},
|
|
241
|
+
commit(label = "Edit") {
|
|
242
|
+
ensureUsable();
|
|
243
|
+
return history.commit(label);
|
|
244
|
+
},
|
|
245
|
+
undo: () => travelThroughHistory("undo"),
|
|
246
|
+
redo: () => travelThroughHistory("redo"),
|
|
247
|
+
canUndo: () => history.state().canUndo,
|
|
248
|
+
canRedo: () => history.state().canRedo,
|
|
249
|
+
getHistory: () => history.labels(),
|
|
250
|
+
clearHistory() {
|
|
251
|
+
ensureUsable();
|
|
252
|
+
history.reset();
|
|
253
|
+
},
|
|
213
254
|
on: (name, handler) => events.on(name, handler),
|
|
214
255
|
destroy
|
|
215
256
|
};
|
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" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
|
|
3
3
|
export interface DocumentIssue {
|
|
4
4
|
code: DocumentErrorCode;
|
|
5
5
|
path: string;
|
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" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
|
|
3
3
|
export interface DocumentIssue {
|
|
4
4
|
code: DocumentErrorCode;
|
|
5
5
|
path: string;
|
|
@@ -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,191 @@
|
|
|
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 } = 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
|
+
onChange(state());
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function isRecording() {
|
|
60
|
+
return !applying && pausedDepth === 0 && transactionDepth === 0;
|
|
61
|
+
}
|
|
62
|
+
function commitPendingChanges() {
|
|
63
|
+
flushScheduled = false;
|
|
64
|
+
if (pendingChanges.length === 0) return;
|
|
65
|
+
if (isRecording()) commit(describePendingChanges(pendingChanges));
|
|
66
|
+
pendingChanges = [];
|
|
67
|
+
}
|
|
68
|
+
function notice(verb, target) {
|
|
69
|
+
if (!isRecording()) return;
|
|
70
|
+
pendingChanges.push({
|
|
71
|
+
verb,
|
|
72
|
+
noun: nameOf(target)
|
|
73
|
+
});
|
|
74
|
+
if (flushScheduled) return;
|
|
75
|
+
flushScheduled = true;
|
|
76
|
+
queueMicrotask(commitPendingChanges);
|
|
77
|
+
}
|
|
78
|
+
const handleAdded = ({ target }) => notice("Add", target);
|
|
79
|
+
const handleRemoved = ({ target }) => notice("Delete", target);
|
|
80
|
+
const handleModified = (event) => {
|
|
81
|
+
const action = event.action ?? event.transform?.action ?? "";
|
|
82
|
+
notice(verbsByAction[action] ?? "Transform", event.target);
|
|
83
|
+
};
|
|
84
|
+
const handleTextEdited = ({ target }) => notice("Edit", target);
|
|
85
|
+
const listeners = [
|
|
86
|
+
["object:added", handleAdded],
|
|
87
|
+
["object:removed", handleRemoved],
|
|
88
|
+
["object:modified", handleModified],
|
|
89
|
+
["text:editing:exited", handleTextEdited]
|
|
90
|
+
];
|
|
91
|
+
const eventTarget = canvas;
|
|
92
|
+
listeners.forEach(([name, handler]) => eventTarget.on(name, handler));
|
|
93
|
+
function transaction(label, work) {
|
|
94
|
+
if (transactionDepth === 0) commitPendingChanges();
|
|
95
|
+
transactionDepth += 1;
|
|
96
|
+
const finish = () => {
|
|
97
|
+
transactionDepth -= 1;
|
|
98
|
+
if (transactionDepth === 0) commit(label);
|
|
99
|
+
};
|
|
100
|
+
let result;
|
|
101
|
+
try {
|
|
102
|
+
result = work();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
finish();
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
if (!isPromiseLike(result)) {
|
|
108
|
+
finish();
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
return Promise.resolve(result).then((value) => {
|
|
112
|
+
finish();
|
|
113
|
+
return value;
|
|
114
|
+
}, (error) => {
|
|
115
|
+
finish();
|
|
116
|
+
throw error;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function withoutRecording(work) {
|
|
120
|
+
pausedDepth += 1;
|
|
121
|
+
let result;
|
|
122
|
+
try {
|
|
123
|
+
result = work();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
pausedDepth -= 1;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
if (!isPromiseLike(result)) {
|
|
129
|
+
pausedDepth -= 1;
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
return Promise.resolve(result).finally(() => {
|
|
133
|
+
pausedDepth -= 1;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function runInOrder(task) {
|
|
137
|
+
const run = queue.then(task);
|
|
138
|
+
queue = run.catch(() => void 0);
|
|
139
|
+
return run;
|
|
140
|
+
}
|
|
141
|
+
function travel(takeStep, pickChange, putBack, moveTo) {
|
|
142
|
+
return runInOrder(async () => {
|
|
143
|
+
if (transactionDepth > 0) return false;
|
|
144
|
+
commitPendingChanges();
|
|
145
|
+
const step = takeStep();
|
|
146
|
+
if (step === void 0) return false;
|
|
147
|
+
applying = true;
|
|
148
|
+
try {
|
|
149
|
+
await require_apply_state.applyStateChange(canvas, pickChange(step));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
putBack(step);
|
|
152
|
+
throw error;
|
|
153
|
+
} finally {
|
|
154
|
+
applying = false;
|
|
155
|
+
}
|
|
156
|
+
snapshot = require_snapshot.createSnapshot(serializeObjects());
|
|
157
|
+
moveTo(step);
|
|
158
|
+
onChange(state());
|
|
159
|
+
return true;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
commit(label) {
|
|
164
|
+
if (applying) return false;
|
|
165
|
+
return commit(label);
|
|
166
|
+
},
|
|
167
|
+
transaction,
|
|
168
|
+
undo: () => travel(() => stack.takeUndo(), (step) => step.before, (step) => stack.returnUndo(step), (step) => stack.returnRedo(step)),
|
|
169
|
+
redo: () => travel(() => stack.takeRedo(), (step) => step.after, (step) => stack.returnRedo(step), (step) => stack.returnUndo(step)),
|
|
170
|
+
state,
|
|
171
|
+
labels: () => ({
|
|
172
|
+
undo: stack.undoLabels(),
|
|
173
|
+
redo: stack.redoLabels()
|
|
174
|
+
}),
|
|
175
|
+
withoutRecording,
|
|
176
|
+
reset() {
|
|
177
|
+
pendingChanges = [];
|
|
178
|
+
stack.clear();
|
|
179
|
+
snapshot = require_snapshot.createSnapshot(serializeObjects());
|
|
180
|
+
onChange(state());
|
|
181
|
+
},
|
|
182
|
+
destroy() {
|
|
183
|
+
listeners.forEach(([name, handler]) => eventTarget.off(name, handler));
|
|
184
|
+
pendingChanges = [];
|
|
185
|
+
stack.clear();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
exports.createHistory = createHistory;
|
|
191
|
+
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
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { applyStateChange } from "./apply-state.js";
|
|
2
|
+
import { createHistoryStack } from "./history-stack.js";
|
|
3
|
+
import { createSnapshot, diffSnapshots } from "./snapshot.js";
|
|
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 } = options;
|
|
28
|
+
const stack = createHistoryStack(Math.max(1, options.limit ?? 100));
|
|
29
|
+
let 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 = createSnapshot(serializeObjects());
|
|
49
|
+
const difference = diffSnapshots(snapshot, nextSnapshot);
|
|
50
|
+
snapshot = nextSnapshot;
|
|
51
|
+
if (difference === null) return false;
|
|
52
|
+
stack.record({
|
|
53
|
+
label,
|
|
54
|
+
...difference
|
|
55
|
+
});
|
|
56
|
+
onChange(state());
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function isRecording() {
|
|
60
|
+
return !applying && pausedDepth === 0 && transactionDepth === 0;
|
|
61
|
+
}
|
|
62
|
+
function commitPendingChanges() {
|
|
63
|
+
flushScheduled = false;
|
|
64
|
+
if (pendingChanges.length === 0) return;
|
|
65
|
+
if (isRecording()) commit(describePendingChanges(pendingChanges));
|
|
66
|
+
pendingChanges = [];
|
|
67
|
+
}
|
|
68
|
+
function notice(verb, target) {
|
|
69
|
+
if (!isRecording()) return;
|
|
70
|
+
pendingChanges.push({
|
|
71
|
+
verb,
|
|
72
|
+
noun: nameOf(target)
|
|
73
|
+
});
|
|
74
|
+
if (flushScheduled) return;
|
|
75
|
+
flushScheduled = true;
|
|
76
|
+
queueMicrotask(commitPendingChanges);
|
|
77
|
+
}
|
|
78
|
+
const handleAdded = ({ target }) => notice("Add", target);
|
|
79
|
+
const handleRemoved = ({ target }) => notice("Delete", target);
|
|
80
|
+
const handleModified = (event) => {
|
|
81
|
+
const action = event.action ?? event.transform?.action ?? "";
|
|
82
|
+
notice(verbsByAction[action] ?? "Transform", event.target);
|
|
83
|
+
};
|
|
84
|
+
const handleTextEdited = ({ target }) => notice("Edit", target);
|
|
85
|
+
const listeners = [
|
|
86
|
+
["object:added", handleAdded],
|
|
87
|
+
["object:removed", handleRemoved],
|
|
88
|
+
["object:modified", handleModified],
|
|
89
|
+
["text:editing:exited", handleTextEdited]
|
|
90
|
+
];
|
|
91
|
+
const eventTarget = canvas;
|
|
92
|
+
listeners.forEach(([name, handler]) => eventTarget.on(name, handler));
|
|
93
|
+
function transaction(label, work) {
|
|
94
|
+
if (transactionDepth === 0) commitPendingChanges();
|
|
95
|
+
transactionDepth += 1;
|
|
96
|
+
const finish = () => {
|
|
97
|
+
transactionDepth -= 1;
|
|
98
|
+
if (transactionDepth === 0) commit(label);
|
|
99
|
+
};
|
|
100
|
+
let result;
|
|
101
|
+
try {
|
|
102
|
+
result = work();
|
|
103
|
+
} catch (error) {
|
|
104
|
+
finish();
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
if (!isPromiseLike(result)) {
|
|
108
|
+
finish();
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
return Promise.resolve(result).then((value) => {
|
|
112
|
+
finish();
|
|
113
|
+
return value;
|
|
114
|
+
}, (error) => {
|
|
115
|
+
finish();
|
|
116
|
+
throw error;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function withoutRecording(work) {
|
|
120
|
+
pausedDepth += 1;
|
|
121
|
+
let result;
|
|
122
|
+
try {
|
|
123
|
+
result = work();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
pausedDepth -= 1;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
if (!isPromiseLike(result)) {
|
|
129
|
+
pausedDepth -= 1;
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
return Promise.resolve(result).finally(() => {
|
|
133
|
+
pausedDepth -= 1;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function runInOrder(task) {
|
|
137
|
+
const run = queue.then(task);
|
|
138
|
+
queue = run.catch(() => void 0);
|
|
139
|
+
return run;
|
|
140
|
+
}
|
|
141
|
+
function travel(takeStep, pickChange, putBack, moveTo) {
|
|
142
|
+
return runInOrder(async () => {
|
|
143
|
+
if (transactionDepth > 0) return false;
|
|
144
|
+
commitPendingChanges();
|
|
145
|
+
const step = takeStep();
|
|
146
|
+
if (step === void 0) return false;
|
|
147
|
+
applying = true;
|
|
148
|
+
try {
|
|
149
|
+
await applyStateChange(canvas, pickChange(step));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
putBack(step);
|
|
152
|
+
throw error;
|
|
153
|
+
} finally {
|
|
154
|
+
applying = false;
|
|
155
|
+
}
|
|
156
|
+
snapshot = createSnapshot(serializeObjects());
|
|
157
|
+
moveTo(step);
|
|
158
|
+
onChange(state());
|
|
159
|
+
return true;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
commit(label) {
|
|
164
|
+
if (applying) return false;
|
|
165
|
+
return commit(label);
|
|
166
|
+
},
|
|
167
|
+
transaction,
|
|
168
|
+
undo: () => travel(() => stack.takeUndo(), (step) => step.before, (step) => stack.returnUndo(step), (step) => stack.returnRedo(step)),
|
|
169
|
+
redo: () => travel(() => stack.takeRedo(), (step) => step.after, (step) => stack.returnRedo(step), (step) => stack.returnUndo(step)),
|
|
170
|
+
state,
|
|
171
|
+
labels: () => ({
|
|
172
|
+
undo: stack.undoLabels(),
|
|
173
|
+
redo: stack.redoLabels()
|
|
174
|
+
}),
|
|
175
|
+
withoutRecording,
|
|
176
|
+
reset() {
|
|
177
|
+
pendingChanges = [];
|
|
178
|
+
stack.clear();
|
|
179
|
+
snapshot = createSnapshot(serializeObjects());
|
|
180
|
+
onChange(state());
|
|
181
|
+
},
|
|
182
|
+
destroy() {
|
|
183
|
+
listeners.forEach(([name, handler]) => eventTarget.off(name, handler));
|
|
184
|
+
pendingChanges = [];
|
|
185
|
+
stack.clear();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
export { createHistory, describePendingChanges };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/history/history-stack.ts
|
|
2
|
+
function createHistoryStack(limit) {
|
|
3
|
+
const undoSteps = [];
|
|
4
|
+
const redoSteps = [];
|
|
5
|
+
function pushWithinLimit(steps, step) {
|
|
6
|
+
steps.push(step);
|
|
7
|
+
if (steps.length > limit) steps.shift();
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
record(step) {
|
|
11
|
+
pushWithinLimit(undoSteps, step);
|
|
12
|
+
redoSteps.length = 0;
|
|
13
|
+
},
|
|
14
|
+
takeUndo: () => undoSteps.pop(),
|
|
15
|
+
takeRedo: () => redoSteps.pop(),
|
|
16
|
+
returnUndo: (step) => pushWithinLimit(undoSteps, step),
|
|
17
|
+
returnRedo: (step) => pushWithinLimit(redoSteps, step),
|
|
18
|
+
undoLabels: () => undoSteps.map((step) => step.label).reverse(),
|
|
19
|
+
redoLabels: () => redoSteps.map((step) => step.label).reverse(),
|
|
20
|
+
clear() {
|
|
21
|
+
undoSteps.length = 0;
|
|
22
|
+
redoSteps.length = 0;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
exports.createHistoryStack = createHistoryStack;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/history/history-stack.ts
|
|
2
|
+
function createHistoryStack(limit) {
|
|
3
|
+
const undoSteps = [];
|
|
4
|
+
const redoSteps = [];
|
|
5
|
+
function pushWithinLimit(steps, step) {
|
|
6
|
+
steps.push(step);
|
|
7
|
+
if (steps.length > limit) steps.shift();
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
record(step) {
|
|
11
|
+
pushWithinLimit(undoSteps, step);
|
|
12
|
+
redoSteps.length = 0;
|
|
13
|
+
},
|
|
14
|
+
takeUndo: () => undoSteps.pop(),
|
|
15
|
+
takeRedo: () => redoSteps.pop(),
|
|
16
|
+
returnUndo: (step) => pushWithinLimit(undoSteps, step),
|
|
17
|
+
returnRedo: (step) => pushWithinLimit(redoSteps, step),
|
|
18
|
+
undoLabels: () => undoSteps.map((step) => step.label).reverse(),
|
|
19
|
+
redoLabels: () => redoSteps.map((step) => step.label).reverse(),
|
|
20
|
+
clear() {
|
|
21
|
+
undoSteps.length = 0;
|
|
22
|
+
redoSteps.length = 0;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { createHistoryStack };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/history/keyboard-shortcuts.ts
|
|
2
|
+
function isTypingInto(element) {
|
|
3
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
4
|
+
if (element.isContentEditable) return true;
|
|
5
|
+
return [
|
|
6
|
+
"INPUT",
|
|
7
|
+
"TEXTAREA",
|
|
8
|
+
"SELECT"
|
|
9
|
+
].includes(element.tagName);
|
|
10
|
+
}
|
|
11
|
+
function chooseAction(event) {
|
|
12
|
+
if (!(event.metaKey || event.ctrlKey) || event.altKey) return null;
|
|
13
|
+
const key = event.key.toLowerCase();
|
|
14
|
+
if (key === "z") return event.shiftKey ? "redo" : "undo";
|
|
15
|
+
if (key === "y" && event.ctrlKey && !event.shiftKey) return "redo";
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function bindKeyboardShortcuts(engine, options = {}) {
|
|
19
|
+
const target = options.target ?? globalThis.window;
|
|
20
|
+
const handleKeyDown = (event) => {
|
|
21
|
+
const keyboardEvent = event;
|
|
22
|
+
if (keyboardEvent.defaultPrevented || isTypingInto(keyboardEvent.target)) return;
|
|
23
|
+
const action = chooseAction(keyboardEvent);
|
|
24
|
+
if (action === null) return;
|
|
25
|
+
keyboardEvent.preventDefault();
|
|
26
|
+
engine[action]().catch(() => void 0);
|
|
27
|
+
};
|
|
28
|
+
target.addEventListener("keydown", handleKeyDown);
|
|
29
|
+
return () => target.removeEventListener("keydown", handleKeyDown);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
exports.bindKeyboardShortcuts = bindKeyboardShortcuts;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/history/keyboard-shortcuts.d.ts
|
|
2
|
+
export interface UndoRedoTarget {
|
|
3
|
+
undo(): Promise<boolean>;
|
|
4
|
+
redo(): Promise<boolean>;
|
|
5
|
+
}
|
|
6
|
+
export interface KeyboardShortcutOptions {
|
|
7
|
+
target?: EventTarget;
|
|
8
|
+
}
|
|
9
|
+
export declare function bindKeyboardShortcuts(engine: UndoRedoTarget, options?: KeyboardShortcutOptions): () => void;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/history/keyboard-shortcuts.d.ts
|
|
2
|
+
export interface UndoRedoTarget {
|
|
3
|
+
undo(): Promise<boolean>;
|
|
4
|
+
redo(): Promise<boolean>;
|
|
5
|
+
}
|
|
6
|
+
export interface KeyboardShortcutOptions {
|
|
7
|
+
target?: EventTarget;
|
|
8
|
+
}
|
|
9
|
+
export declare function bindKeyboardShortcuts(engine: UndoRedoTarget, options?: KeyboardShortcutOptions): () => void;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/history/keyboard-shortcuts.ts
|
|
2
|
+
function isTypingInto(element) {
|
|
3
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
4
|
+
if (element.isContentEditable) return true;
|
|
5
|
+
return [
|
|
6
|
+
"INPUT",
|
|
7
|
+
"TEXTAREA",
|
|
8
|
+
"SELECT"
|
|
9
|
+
].includes(element.tagName);
|
|
10
|
+
}
|
|
11
|
+
function chooseAction(event) {
|
|
12
|
+
if (!(event.metaKey || event.ctrlKey) || event.altKey) return null;
|
|
13
|
+
const key = event.key.toLowerCase();
|
|
14
|
+
if (key === "z") return event.shiftKey ? "redo" : "undo";
|
|
15
|
+
if (key === "y" && event.ctrlKey && !event.shiftKey) return "redo";
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function bindKeyboardShortcuts(engine, options = {}) {
|
|
19
|
+
const target = options.target ?? globalThis.window;
|
|
20
|
+
const handleKeyDown = (event) => {
|
|
21
|
+
const keyboardEvent = event;
|
|
22
|
+
if (keyboardEvent.defaultPrevented || isTypingInto(keyboardEvent.target)) return;
|
|
23
|
+
const action = chooseAction(keyboardEvent);
|
|
24
|
+
if (action === null) return;
|
|
25
|
+
keyboardEvent.preventDefault();
|
|
26
|
+
engine[action]().catch(() => void 0);
|
|
27
|
+
};
|
|
28
|
+
target.addEventListener("keydown", handleKeyDown);
|
|
29
|
+
return () => target.removeEventListener("keydown", handleKeyDown);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { bindKeyboardShortcuts };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/history/snapshot.ts
|
|
2
|
+
function createSnapshot(objects) {
|
|
3
|
+
const objectsById = /* @__PURE__ */ new Map();
|
|
4
|
+
const order = [];
|
|
5
|
+
for (const object of objects) {
|
|
6
|
+
const id = object.id ?? "";
|
|
7
|
+
objectsById.set(id, JSON.stringify(object));
|
|
8
|
+
order.push(id);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
objectsById,
|
|
12
|
+
order
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function haveSameOrder(first, second) {
|
|
16
|
+
return first.length === second.length && first.every((id, index) => id === second[index]);
|
|
17
|
+
}
|
|
18
|
+
function diffSnapshots(previous, next) {
|
|
19
|
+
const before = /* @__PURE__ */ new Map();
|
|
20
|
+
const after = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const [id, json] of next.objectsById) {
|
|
22
|
+
const previousJson = previous.objectsById.get(id);
|
|
23
|
+
if (previousJson === json) continue;
|
|
24
|
+
before.set(id, previousJson ?? null);
|
|
25
|
+
after.set(id, json);
|
|
26
|
+
}
|
|
27
|
+
for (const [id, json] of previous.objectsById) {
|
|
28
|
+
if (next.objectsById.has(id)) continue;
|
|
29
|
+
before.set(id, json);
|
|
30
|
+
after.set(id, null);
|
|
31
|
+
}
|
|
32
|
+
const orderChanged = !haveSameOrder(previous.order, next.order);
|
|
33
|
+
if (before.size === 0 && !orderChanged) return null;
|
|
34
|
+
return {
|
|
35
|
+
before: {
|
|
36
|
+
objects: before,
|
|
37
|
+
order: orderChanged ? previous.order : null
|
|
38
|
+
},
|
|
39
|
+
after: {
|
|
40
|
+
objects: after,
|
|
41
|
+
order: orderChanged ? next.order : null
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
exports.createSnapshot = createSnapshot;
|
|
47
|
+
exports.diffSnapshots = diffSnapshots;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/history/snapshot.ts
|
|
2
|
+
function createSnapshot(objects) {
|
|
3
|
+
const objectsById = /* @__PURE__ */ new Map();
|
|
4
|
+
const order = [];
|
|
5
|
+
for (const object of objects) {
|
|
6
|
+
const id = object.id ?? "";
|
|
7
|
+
objectsById.set(id, JSON.stringify(object));
|
|
8
|
+
order.push(id);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
objectsById,
|
|
12
|
+
order
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function haveSameOrder(first, second) {
|
|
16
|
+
return first.length === second.length && first.every((id, index) => id === second[index]);
|
|
17
|
+
}
|
|
18
|
+
function diffSnapshots(previous, next) {
|
|
19
|
+
const before = /* @__PURE__ */ new Map();
|
|
20
|
+
const after = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const [id, json] of next.objectsById) {
|
|
22
|
+
const previousJson = previous.objectsById.get(id);
|
|
23
|
+
if (previousJson === json) continue;
|
|
24
|
+
before.set(id, previousJson ?? null);
|
|
25
|
+
after.set(id, json);
|
|
26
|
+
}
|
|
27
|
+
for (const [id, json] of previous.objectsById) {
|
|
28
|
+
if (next.objectsById.has(id)) continue;
|
|
29
|
+
before.set(id, json);
|
|
30
|
+
after.set(id, null);
|
|
31
|
+
}
|
|
32
|
+
const orderChanged = !haveSameOrder(previous.order, next.order);
|
|
33
|
+
if (before.size === 0 && !orderChanged) return null;
|
|
34
|
+
return {
|
|
35
|
+
before: {
|
|
36
|
+
objects: before,
|
|
37
|
+
order: orderChanged ? previous.order : null
|
|
38
|
+
},
|
|
39
|
+
after: {
|
|
40
|
+
objects: after,
|
|
41
|
+
order: orderChanged ? next.order : null
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { createSnapshot, diffSnapshots };
|
package/dist/index.cjs
CHANGED
|
@@ -3,8 +3,10 @@ const require_document_format = require("./document/document-format.cjs");
|
|
|
3
3
|
const require_validate_document = require("./document/validate-document.cjs");
|
|
4
4
|
const require_errors = require("./engine/errors.cjs");
|
|
5
5
|
const require_create_document_engine = require("./engine/create-document-engine.cjs");
|
|
6
|
+
const require_keyboard_shortcuts = require("./history/keyboard-shortcuts.cjs");
|
|
6
7
|
exports.CURRENT_SCHEMA_VERSION = require_document_format.CURRENT_SCHEMA_VERSION;
|
|
7
8
|
exports.DocumentEngineError = require_errors.DocumentEngineError;
|
|
9
|
+
exports.bindKeyboardShortcuts = require_keyboard_shortcuts.bindKeyboardShortcuts;
|
|
8
10
|
exports.createDocumentEngine = require_create_document_engine.createDocumentEngine;
|
|
9
11
|
exports.isDocumentEngineError = require_errors.isDocumentEngineError;
|
|
10
12
|
exports.validateDocument = require_validate_document.validateDocument;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { CURRENT_SCHEMA_VERSION, DocumentCanvas, DocumentInfo, FabricDocument, SerializedFabricObject } from "./document/document-format.cjs";
|
|
2
2
|
import { NewDocumentOptions } from "./document/create-document.cjs";
|
|
3
3
|
import { CustomObjectDefinition } from "./fabric/object-registry.cjs";
|
|
4
|
+
import { HistoryOptions, HistoryState } from "./history/create-history.cjs";
|
|
4
5
|
import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, isDocumentEngineError } from "./engine/errors.cjs";
|
|
5
6
|
import { Unsubscribe } from "./engine/event-emitter.cjs";
|
|
6
7
|
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, DocumentStorage, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
7
8
|
import { validateDocument } from "./document/validate-document.cjs";
|
|
8
|
-
|
|
9
|
+
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.cjs";
|
|
10
|
+
export { 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 FabricDocument, type HistoryOptions, type HistoryState, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type SerializedFabricObject, type UndoRedoTarget, type Unsubscribe, bindKeyboardShortcuts, createDocumentEngine, isDocumentEngineError, validateDocument };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { CURRENT_SCHEMA_VERSION, DocumentCanvas, DocumentInfo, FabricDocument, SerializedFabricObject } from "./document/document-format.js";
|
|
2
2
|
import { NewDocumentOptions } from "./document/create-document.js";
|
|
3
3
|
import { CustomObjectDefinition } from "./fabric/object-registry.js";
|
|
4
|
+
import { HistoryOptions, HistoryState } from "./history/create-history.js";
|
|
4
5
|
import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, isDocumentEngineError } from "./engine/errors.js";
|
|
5
6
|
import { Unsubscribe } from "./engine/event-emitter.js";
|
|
6
7
|
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, DocumentStorage, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
|
|
7
8
|
import { validateDocument } from "./document/validate-document.js";
|
|
8
|
-
|
|
9
|
+
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
10
|
+
export { 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 FabricDocument, type HistoryOptions, type HistoryState, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type SerializedFabricObject, type UndoRedoTarget, type Unsubscribe, bindKeyboardShortcuts, createDocumentEngine, isDocumentEngineError, validateDocument };
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,5 @@ import { CURRENT_SCHEMA_VERSION } from "./document/document-format.js";
|
|
|
2
2
|
import { validateDocument } from "./document/validate-document.js";
|
|
3
3
|
import { DocumentEngineError, isDocumentEngineError } from "./engine/errors.js";
|
|
4
4
|
import { createDocumentEngine } from "./engine/create-document-engine.js";
|
|
5
|
-
|
|
5
|
+
import { bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
6
|
+
export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, createDocumentEngine, isDocumentEngineError, validateDocument };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabricjs-document-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Turn an existing Fabric.js canvas into a dependable editable document: stable object ids, versioned save and load, custom object registration and clear load errors.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fabric",
|