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
|
@@ -0,0 +1,192 @@
|
|
|
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, onContentChange } = 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
|
+
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 applyStateChange(canvas, pickChange(step));
|
|
151
|
+
} catch (error) {
|
|
152
|
+
putBack(step);
|
|
153
|
+
throw error;
|
|
154
|
+
} finally {
|
|
155
|
+
applying = false;
|
|
156
|
+
}
|
|
157
|
+
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 = 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
|
+
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,13 @@ 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");
|
|
7
|
+
const require_unsaved_changes_warning = require("./save/unsaved-changes-warning.cjs");
|
|
6
8
|
exports.CURRENT_SCHEMA_VERSION = require_document_format.CURRENT_SCHEMA_VERSION;
|
|
7
9
|
exports.DocumentEngineError = require_errors.DocumentEngineError;
|
|
10
|
+
exports.bindKeyboardShortcuts = require_keyboard_shortcuts.bindKeyboardShortcuts;
|
|
11
|
+
exports.bindUnsavedChangesWarning = require_unsaved_changes_warning.bindUnsavedChangesWarning;
|
|
12
|
+
exports.createConflictError = require_errors.createConflictError;
|
|
8
13
|
exports.createDocumentEngine = require_create_document_engine.createDocumentEngine;
|
|
9
14
|
exports.isDocumentEngineError = require_errors.isDocumentEngineError;
|
|
10
15
|
exports.validateDocument = require_validate_document.validateDocument;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
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 {
|
|
4
|
+
import { HistoryOptions, HistoryState } from "./history/create-history.cjs";
|
|
5
|
+
import { AutosaveOptions } from "./save/autosave-scheduler.cjs";
|
|
6
|
+
import { RetryOptions } from "./save/retry.cjs";
|
|
7
|
+
import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, createConflictError, isDocumentEngineError } from "./engine/errors.cjs";
|
|
8
|
+
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.cjs";
|
|
9
|
+
import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.cjs";
|
|
5
10
|
import { Unsubscribe } from "./engine/event-emitter.cjs";
|
|
6
|
-
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions,
|
|
11
|
+
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
7
12
|
import { validateDocument } from "./document/validate-document.cjs";
|
|
8
|
-
|
|
13
|
+
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.cjs";
|
|
14
|
+
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.cjs";
|
|
15
|
+
export { type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type HistoryOptions, type HistoryState, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
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 {
|
|
4
|
+
import { HistoryOptions, HistoryState } from "./history/create-history.js";
|
|
5
|
+
import { AutosaveOptions } from "./save/autosave-scheduler.js";
|
|
6
|
+
import { RetryOptions } from "./save/retry.js";
|
|
7
|
+
import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, DocumentIssue, createConflictError, isDocumentEngineError } from "./engine/errors.js";
|
|
8
|
+
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.js";
|
|
9
|
+
import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.js";
|
|
5
10
|
import { Unsubscribe } from "./engine/event-emitter.js";
|
|
6
|
-
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions,
|
|
11
|
+
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
|
|
7
12
|
import { validateDocument } from "./document/validate-document.js";
|
|
8
|
-
|
|
13
|
+
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
14
|
+
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
|
|
15
|
+
export { type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type FabricDocument, type HistoryOptions, type HistoryState, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CURRENT_SCHEMA_VERSION } from "./document/document-format.js";
|
|
2
2
|
import { validateDocument } from "./document/validate-document.js";
|
|
3
|
-
import { DocumentEngineError, isDocumentEngineError } from "./engine/errors.js";
|
|
3
|
+
import { DocumentEngineError, createConflictError, 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
|
+
import { bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
|
|
7
|
+
export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, isDocumentEngineError, validateDocument };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/save/autosave-scheduler.ts
|
|
2
|
+
function createAutosaveScheduler(run, options = {}) {
|
|
3
|
+
const delay = options.delay ?? 1e3;
|
|
4
|
+
const maxWait = Math.max(delay, options.maxWait ?? 1e4);
|
|
5
|
+
let timer;
|
|
6
|
+
let firstRequestAt;
|
|
7
|
+
function cancel() {
|
|
8
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
9
|
+
timer = void 0;
|
|
10
|
+
firstRequestAt = void 0;
|
|
11
|
+
}
|
|
12
|
+
function fire() {
|
|
13
|
+
cancel();
|
|
14
|
+
run();
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
schedule() {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
firstRequestAt ?? (firstRequestAt = now);
|
|
20
|
+
const timeLeftBeforeMaxWait = maxWait - (now - firstRequestAt);
|
|
21
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
22
|
+
timer = setTimeout(fire, Math.max(0, Math.min(delay, timeLeftBeforeMaxWait)));
|
|
23
|
+
},
|
|
24
|
+
cancel
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
exports.createAutosaveScheduler = createAutosaveScheduler;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/save/autosave-scheduler.ts
|
|
2
|
+
function createAutosaveScheduler(run, options = {}) {
|
|
3
|
+
const delay = options.delay ?? 1e3;
|
|
4
|
+
const maxWait = Math.max(delay, options.maxWait ?? 1e4);
|
|
5
|
+
let timer;
|
|
6
|
+
let firstRequestAt;
|
|
7
|
+
function cancel() {
|
|
8
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
9
|
+
timer = void 0;
|
|
10
|
+
firstRequestAt = void 0;
|
|
11
|
+
}
|
|
12
|
+
function fire() {
|
|
13
|
+
cancel();
|
|
14
|
+
run();
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
schedule() {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
firstRequestAt ?? (firstRequestAt = now);
|
|
20
|
+
const timeLeftBeforeMaxWait = maxWait - (now - firstRequestAt);
|
|
21
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
22
|
+
timer = setTimeout(fire, Math.max(0, Math.min(delay, timeLeftBeforeMaxWait)));
|
|
23
|
+
},
|
|
24
|
+
cancel
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { createAutosaveScheduler };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/save/retry.ts
|
|
2
|
+
function isRetryable(error) {
|
|
3
|
+
const hints = error ?? {};
|
|
4
|
+
if (hints.code === "SAVE_CONFLICT" || hints.code === "SAVE_CANCELLED") return false;
|
|
5
|
+
return hints.retryable !== false;
|
|
6
|
+
}
|
|
7
|
+
function retryDelay(attempt, options = {}) {
|
|
8
|
+
const baseDelay = options.baseDelay ?? 500;
|
|
9
|
+
const maxDelay = options.maxDelay ?? 8e3;
|
|
10
|
+
const ceiling = Math.min(maxDelay, baseDelay * 2 ** attempt);
|
|
11
|
+
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
12
|
+
}
|
|
13
|
+
function wait(milliseconds, signal) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (signal.aborted) {
|
|
16
|
+
reject(/* @__PURE__ */ new Error("aborted"));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const stop = () => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
reject(/* @__PURE__ */ new Error("aborted"));
|
|
22
|
+
};
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
signal.removeEventListener("abort", stop);
|
|
25
|
+
resolve();
|
|
26
|
+
}, milliseconds);
|
|
27
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
exports.isRetryable = isRetryable;
|
|
32
|
+
exports.retryDelay = retryDelay;
|
|
33
|
+
exports.wait = wait;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/save/retry.ts
|
|
2
|
+
function isRetryable(error) {
|
|
3
|
+
const hints = error ?? {};
|
|
4
|
+
if (hints.code === "SAVE_CONFLICT" || hints.code === "SAVE_CANCELLED") return false;
|
|
5
|
+
return hints.retryable !== false;
|
|
6
|
+
}
|
|
7
|
+
function retryDelay(attempt, options = {}) {
|
|
8
|
+
const baseDelay = options.baseDelay ?? 500;
|
|
9
|
+
const maxDelay = options.maxDelay ?? 8e3;
|
|
10
|
+
const ceiling = Math.min(maxDelay, baseDelay * 2 ** attempt);
|
|
11
|
+
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
12
|
+
}
|
|
13
|
+
function wait(milliseconds, signal) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (signal.aborted) {
|
|
16
|
+
reject(/* @__PURE__ */ new Error("aborted"));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const stop = () => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
reject(/* @__PURE__ */ new Error("aborted"));
|
|
22
|
+
};
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
signal.removeEventListener("abort", stop);
|
|
25
|
+
resolve();
|
|
26
|
+
}, milliseconds);
|
|
27
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { isRetryable, retryDelay, wait };
|