@tldraw/editor 5.5.0-next.a2698a88ecfb → 5.5.0-next.b6a9f7cd7be2
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/dist-cjs/index.d.ts +2 -0
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/TldrawEditor.js +16 -12
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +42 -3
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +144 -155
- package/dist-cjs/lib/editor/Editor.js.map +3 -3
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js +2 -0
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +4 -4
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +14 -6
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +3 -3
- package/dist-cjs/lib/exports/ExportDelay.js +10 -2
- package/dist-cjs/lib/exports/ExportDelay.js.map +2 -2
- package/dist-cjs/lib/exports/FontEmbedder.js +25 -11
- package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/StyleEmbedder.js +9 -4
- package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/fetchCache.js +3 -0
- package/dist-cjs/lib/exports/fetchCache.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgAsImage.js +10 -4
- package/dist-cjs/lib/exports/getSvgAsImage.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgJsx.js +18 -9
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/exports/parseCss.js +1 -1
- package/dist-cjs/lib/exports/parseCss.js.map +1 -1
- package/dist-cjs/lib/hooks/useCanvasEvents.js +24 -0
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useIdentity.js +6 -2
- package/dist-cjs/lib/hooks/useIdentity.js.map +3 -3
- package/dist-cjs/lib/license/LicenseManager.js +9 -1
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +12 -4
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/utils/deepLinks.js +2 -1
- package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js +13 -17
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +78 -40
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +2 -0
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/TldrawEditor.mjs +17 -13
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +42 -3
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +144 -155
- package/dist-esm/lib/editor/Editor.mjs.map +3 -3
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +2 -0
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +4 -4
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +14 -6
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +3 -3
- package/dist-esm/lib/exports/ExportDelay.mjs +11 -3
- package/dist-esm/lib/exports/ExportDelay.mjs.map +2 -2
- package/dist-esm/lib/exports/FontEmbedder.mjs +25 -11
- package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/StyleEmbedder.mjs +9 -4
- package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/fetchCache.mjs +3 -0
- package/dist-esm/lib/exports/fetchCache.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgAsImage.mjs +10 -4
- package/dist-esm/lib/exports/getSvgAsImage.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +18 -9
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/exports/parseCss.mjs +1 -1
- package/dist-esm/lib/exports/parseCss.mjs.map +1 -1
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +24 -0
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useIdentity.mjs +7 -3
- package/dist-esm/lib/hooks/useIdentity.mjs.map +3 -3
- package/dist-esm/lib/license/LicenseManager.mjs +9 -1
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +13 -5
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/utils/deepLinks.mjs +2 -1
- package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs +13 -17
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +78 -40
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +9 -1
- package/package.json +8 -11
- package/src/lib/TldrawEditor.tsx +27 -15
- package/src/lib/components/MenuClickCapture.tsx +55 -4
- package/src/lib/editor/Editor.test.ts +20 -0
- package/src/lib/editor/Editor.ts +221 -259
- package/src/lib/editor/derivations/bindingsIndex.ts +3 -0
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +74 -0
- package/src/lib/editor/managers/TextManager/TextManager.ts +6 -5
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +23 -8
- package/src/lib/exports/ExportDelay.tsx +16 -4
- package/src/lib/exports/FontEmbedder.test.ts +99 -0
- package/src/lib/exports/FontEmbedder.ts +32 -11
- package/src/lib/exports/StyleEmbedder.ts +13 -4
- package/src/lib/exports/fetchCache.test.ts +22 -0
- package/src/lib/exports/fetchCache.ts +4 -0
- package/src/lib/exports/getSvgAsImage.ts +13 -5
- package/src/lib/exports/getSvgJsx.test.ts +43 -1
- package/src/lib/exports/getSvgJsx.tsx +28 -16
- package/src/lib/exports/parseCss.test.ts +4 -0
- package/src/lib/exports/parseCss.ts +2 -2
- package/src/lib/hooks/useCanvasEvents.ts +42 -0
- package/src/lib/hooks/useIdentity.tsx +12 -1
- package/src/lib/license/LicenseManager.ts +13 -1
- package/src/lib/license/LicenseProvider.test.tsx +76 -0
- package/src/lib/license/LicenseProvider.tsx +19 -5
- package/src/lib/test/preserveFocus.test.tsx +81 -0
- package/src/lib/utils/deepLinks.test.ts +15 -0
- package/src/lib/utils/deepLinks.ts +8 -3
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +28 -0
- package/src/lib/utils/sync/LocalIndexedDb.ts +18 -20
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +236 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +106 -54
- package/src/version.ts +3 -3
- package/dist-cjs/lib/primitives/geometry/geometry.bench.js +0 -133
- package/dist-cjs/lib/primitives/geometry/geometry.bench.js.map +0 -7
- package/dist-esm/lib/primitives/geometry/geometry.bench.mjs +0 -132
- package/dist-esm/lib/primitives/geometry/geometry.bench.mjs.map +0 -7
- package/src/lib/primitives/geometry/geometry.bench.ts +0 -179
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
extractSessionStateFromLegacySnapshot,
|
|
8
8
|
loadSessionStateSnapshotIntoStore
|
|
9
9
|
} from "../../config/TLSessionStateSnapshot.mjs";
|
|
10
|
+
import { refreshPage } from "../runtime.mjs";
|
|
10
11
|
import { showCantReadFromIndexDbAlert, showCantWriteToIndexDbAlert } from "./alerts.mjs";
|
|
11
12
|
import { LocalIndexedDb } from "./LocalIndexedDb.mjs";
|
|
12
13
|
const PERSIST_THROTTLE_MS = 350;
|
|
@@ -39,7 +40,6 @@ class TLLocalSyncClient {
|
|
|
39
40
|
this.persistenceKey = persistenceKey;
|
|
40
41
|
this.sessionId = sessionId;
|
|
41
42
|
this.db = new LocalIndexedDb(persistenceKey);
|
|
42
|
-
this.disposables.add(() => this.db.close());
|
|
43
43
|
this.serializedSchema = this.store.schema.serialize();
|
|
44
44
|
this.$sessionStateSnapshot = createSessionStateSnapshotSignal(this.store);
|
|
45
45
|
this.disposables.add(
|
|
@@ -117,6 +117,47 @@ class TLLocalSyncClient {
|
|
|
117
117
|
async connect(onLoad, onLoadError) {
|
|
118
118
|
this.debug("connecting");
|
|
119
119
|
let data;
|
|
120
|
+
const handleMessage = (msg2) => {
|
|
121
|
+
const res = this.store.schema.getMigrationsSince(msg2.schema);
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
this.isReloading = true;
|
|
124
|
+
const timeSinceInit = Date.now() - this.initTime;
|
|
125
|
+
if (timeSinceInit < 5e3) {
|
|
126
|
+
onLoadError(new Error("Schema mismatch, please close other tabs and reload the page"));
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
this.debug("reloading");
|
|
130
|
+
if (typeof window !== "undefined") refreshPage();
|
|
131
|
+
return false;
|
|
132
|
+
} else if (res.value.length > 0) {
|
|
133
|
+
this.debug("telling them to reload");
|
|
134
|
+
this.channel.postMessage({ type: "announce", schema: this.serializedSchema });
|
|
135
|
+
this.shouldDoFullDBWrite = true;
|
|
136
|
+
this.schedulePersist();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (msg2.type === "diff") {
|
|
140
|
+
this.debug("applying diff");
|
|
141
|
+
transact(() => {
|
|
142
|
+
this.store.mergeRemoteChanges(() => {
|
|
143
|
+
this.store.applyDiff(msg2.changes);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
};
|
|
149
|
+
let bufferedMessages = [];
|
|
150
|
+
this.channel.onmessage = ({ data: data2 }) => {
|
|
151
|
+
this.debug("got message", data2);
|
|
152
|
+
if (bufferedMessages) {
|
|
153
|
+
bufferedMessages.push(data2);
|
|
154
|
+
} else {
|
|
155
|
+
handleMessage(data2);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
this.disposables.add(() => {
|
|
159
|
+
this.channel.close();
|
|
160
|
+
});
|
|
120
161
|
try {
|
|
121
162
|
data = await this.db.load({ sessionId: this.sessionId });
|
|
122
163
|
} catch (error) {
|
|
@@ -154,41 +195,14 @@ class TLLocalSyncClient {
|
|
|
154
195
|
});
|
|
155
196
|
}
|
|
156
197
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const timeSinceInit = Date.now() - this.initTime;
|
|
163
|
-
if (timeSinceInit < 5e3) {
|
|
164
|
-
onLoadError(new Error("Schema mismatch, please close other tabs and reload the page"));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
this.debug("reloading");
|
|
168
|
-
this.isReloading = true;
|
|
169
|
-
window?.location?.reload?.();
|
|
170
|
-
return;
|
|
171
|
-
} else if (res.value.length > 0) {
|
|
172
|
-
this.debug("telling them to reload");
|
|
173
|
-
this.channel.postMessage({ type: "announce", schema: this.serializedSchema });
|
|
174
|
-
this.shouldDoFullDBWrite = true;
|
|
175
|
-
this.persistIfNeeded();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (msg2.type === "diff") {
|
|
179
|
-
this.debug("applying diff");
|
|
180
|
-
transact(() => {
|
|
181
|
-
this.store.mergeRemoteChanges(() => {
|
|
182
|
-
this.store.applyDiff(msg2.changes);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
this.channel.postMessage({ type: "announce", schema: this.serializedSchema });
|
|
188
|
-
this.disposables.add(() => {
|
|
189
|
-
this.channel.close();
|
|
190
|
-
});
|
|
198
|
+
const messages = bufferedMessages;
|
|
199
|
+
bufferedMessages = null;
|
|
200
|
+
for (const message of messages) {
|
|
201
|
+
if (!handleMessage(message)) return;
|
|
202
|
+
}
|
|
191
203
|
this.didLoad = true;
|
|
204
|
+
if (this.diffQueue.length > 0) this.schedulePersist();
|
|
205
|
+
this.channel.postMessage({ type: "announce", schema: this.serializedSchema });
|
|
192
206
|
onLoad(this);
|
|
193
207
|
} catch (e) {
|
|
194
208
|
this.debug("error loading data from store", e);
|
|
@@ -198,14 +212,36 @@ class TLLocalSyncClient {
|
|
|
198
212
|
}
|
|
199
213
|
}
|
|
200
214
|
close() {
|
|
215
|
+
if (this.didDispose) return;
|
|
201
216
|
this.debug("closing");
|
|
202
217
|
this.didDispose = true;
|
|
218
|
+
this.store._flushHistory();
|
|
203
219
|
this.disposables.forEach((d) => d());
|
|
220
|
+
if (this.scheduledPersistTimeout) {
|
|
221
|
+
clearTimeout(this.scheduledPersistTimeout);
|
|
222
|
+
this.scheduledPersistTimeout = null;
|
|
223
|
+
}
|
|
204
224
|
if (typeof window !== "undefined" && window.tlsync === this) {
|
|
205
225
|
delete window.tlsync;
|
|
206
226
|
}
|
|
227
|
+
this.flushAndCloseDb();
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Flush queued changes so edits made in the last PERSIST_THROTTLE_MS before unmount aren't lost,
|
|
231
|
+
* then close the db. Never flushes before load (a full write then would wipe the db with an empty
|
|
232
|
+
* store), and only when something is queued: shouldDoFullDBWrite stays true until the first
|
|
233
|
+
* persist, so an unconditional flush would rewrite the whole document on every no-op close.
|
|
234
|
+
*/
|
|
235
|
+
flushAndCloseDb() {
|
|
236
|
+
if (this.persistPromise) {
|
|
237
|
+
this.persistPromise.then(() => this.flushAndCloseDb());
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (this.didLoad && this.diffQueue.length > 0) this.persistIfNeeded();
|
|
241
|
+
this.db.close();
|
|
207
242
|
}
|
|
208
243
|
isPersisting = false;
|
|
244
|
+
persistPromise = null;
|
|
209
245
|
didLastWriteError = false;
|
|
210
246
|
// eslint-disable-next-line no-restricted-globals
|
|
211
247
|
scheduledPersistTimeout = null;
|
|
@@ -218,6 +254,7 @@ class TLLocalSyncClient {
|
|
|
218
254
|
schedulePersist() {
|
|
219
255
|
this.debug("schedulePersist", this.scheduledPersistTimeout);
|
|
220
256
|
if (this.scheduledPersistTimeout) return;
|
|
257
|
+
if (this.didDispose) return;
|
|
221
258
|
this.scheduledPersistTimeout = setTimeout(
|
|
222
259
|
() => {
|
|
223
260
|
this.scheduledPersistTimeout = null;
|
|
@@ -247,11 +284,12 @@ class TLLocalSyncClient {
|
|
|
247
284
|
clearTimeout(this.scheduledPersistTimeout);
|
|
248
285
|
this.scheduledPersistTimeout = null;
|
|
249
286
|
}
|
|
287
|
+
if (!this.didLoad) return;
|
|
250
288
|
if (this.isPersisting) return;
|
|
251
289
|
if (this.isReloading) return;
|
|
252
290
|
if (this.store.isPossiblyCorrupted()) return;
|
|
253
291
|
if (this.shouldDoFullDBWrite || this.diffQueue.length > 0) {
|
|
254
|
-
this.doPersist();
|
|
292
|
+
this.persistPromise = this.doPersist();
|
|
255
293
|
}
|
|
256
294
|
}
|
|
257
295
|
/**
|
|
@@ -260,7 +298,6 @@ class TLLocalSyncClient {
|
|
|
260
298
|
*/
|
|
261
299
|
async doPersist() {
|
|
262
300
|
assert(!this.isPersisting, "persist already in progress");
|
|
263
|
-
if (this.didDispose) return;
|
|
264
301
|
this.isPersisting = true;
|
|
265
302
|
this.debug("doPersist start");
|
|
266
303
|
const diffQueue = this.diffQueue;
|
|
@@ -290,12 +327,13 @@ class TLLocalSyncClient {
|
|
|
290
327
|
this.shouldDoFullDBWrite = true;
|
|
291
328
|
this.didLastWriteError = true;
|
|
292
329
|
console.error("failed to store changes in indexed db", e);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
330
|
+
if (!this.didDispose && typeof window !== "undefined") {
|
|
331
|
+
showCantWriteToIndexDbAlert();
|
|
332
|
+
refreshPage();
|
|
296
333
|
}
|
|
297
334
|
}
|
|
298
335
|
this.isPersisting = false;
|
|
336
|
+
this.persistPromise = null;
|
|
299
337
|
this.debug("doPersist end");
|
|
300
338
|
this.schedulePersist();
|
|
301
339
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/lib/utils/sync/TLLocalSyncClient.ts"],
|
|
4
|
-
"sourcesContent": ["import { Signal, transact } from '@tldraw/state'\nimport { RecordsDiff, SerializedSchema, UnknownRecord, squashRecordDiffs } from '@tldraw/store'\nimport { TLStore } from '@tldraw/tlschema'\nimport { assert } from '@tldraw/utils'\nimport {\n\tTAB_ID,\n\tTLSessionStateSnapshot,\n\tcreateSessionStateSnapshotSignal,\n\textractSessionStateFromLegacySnapshot,\n\tloadSessionStateSnapshotIntoStore,\n} from '../../config/TLSessionStateSnapshot'\nimport { showCantReadFromIndexDbAlert, showCantWriteToIndexDbAlert } from './alerts'\nimport { LocalIndexedDb } from './LocalIndexedDb'\n\n/** How should we debounce persists? */\nconst PERSIST_THROTTLE_MS = 350\n/** If we're in an error state, how long should we wait before retrying a write? */\nconst PERSIST_RETRY_THROTTLE_MS = 10_000\n\nconst UPDATE_INSTANCE_STATE = Symbol('UPDATE_INSTANCE_STATE')\n\n/**\n * IMPORTANT!!!\n *\n * This is just a quick-n-dirty temporary solution that will be replaced with the remote sync client\n * once it has the db integrated\n */\n\ninterface SyncMessage {\n\ttype: 'diff'\n\tstoreId: string\n\tchanges: RecordsDiff<UnknownRecord>\n\tschema: SerializedSchema\n}\n\n// Sent by new clients when they connect\n// If another client is on the channel with a newer schema version\n// It will\ninterface AnnounceMessage {\n\ttype: 'announce'\n\tschema: SerializedSchema\n}\n\ntype Message = SyncMessage | AnnounceMessage\n\ntype UnpackPromise<T> = T extends Promise<infer U> ? U : T\n\nconst msg = (msg: Message) => msg\n\n/** @internal */\nexport class BroadcastChannelMock {\n\tonmessage?: (e: MessageEvent) => void\n\tconstructor(_name: string) {\n\t\t// noop\n\t}\n\tpostMessage(_msg: Message) {\n\t\t// noop\n\t}\n\tclose() {\n\t\t// noop\n\t}\n}\n\nconst BC = typeof BroadcastChannel === 'undefined' ? BroadcastChannelMock : BroadcastChannel\n\n/** @internal */\nexport class TLLocalSyncClient {\n\tprivate disposables = new Set<() => void>()\n\tprivate diffQueue: Array<RecordsDiff<UnknownRecord> | typeof UPDATE_INSTANCE_STATE> = []\n\tprivate didDispose = false\n\tprivate didLoad = false\n\tprivate shouldDoFullDBWrite = true\n\tprivate isReloading = false\n\treadonly persistenceKey: string\n\treadonly sessionId: string\n\treadonly serializedSchema: SerializedSchema\n\tprivate isDebugging = false\n\tprivate readonly documentTypes: ReadonlySet<string>\n\tprivate readonly $sessionStateSnapshot: Signal<TLSessionStateSnapshot | null>\n\t/** @internal */\n\treadonly db: LocalIndexedDb\n\n\tinitTime = Date.now()\n\tprivate debug(...args: any[]) {\n\t\tif (this.isDebugging) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.debug(...args)\n\t\t}\n\t}\n\tconstructor(\n\t\tpublic readonly store: TLStore,\n\t\t{\n\t\t\tpersistenceKey,\n\t\t\tsessionId = TAB_ID,\n\t\t\tonLoad,\n\t\t\tonLoadError,\n\t\t}: {\n\t\t\tpersistenceKey: string\n\t\t\tsessionId?: string\n\t\t\tonLoad(self: TLLocalSyncClient): void\n\t\t\tonLoadError(error: Error): void\n\t\t},\n\t\tpublic readonly channel = new BC(`tldraw-tab-sync-${persistenceKey}`)\n\t) {\n\t\tif (typeof window !== 'undefined') {\n\t\t\t;(window as any).tlsync = this\n\t\t}\n\t\tthis.persistenceKey = persistenceKey\n\t\tthis.sessionId = sessionId\n\t\tthis.db = new LocalIndexedDb(persistenceKey)\n\t\tthis.disposables.add(() => this.db.close())\n\n\t\tthis.serializedSchema = this.store.schema.serialize()\n\t\tthis.$sessionStateSnapshot = createSessionStateSnapshotSignal(this.store)\n\n\t\tthis.disposables.add(\n\t\t\t// Set up a subscription to changes from the store: When\n\t\t\t// the store changes (and if the change was made by the user)\n\t\t\t// then immediately send the diff to other tabs via postMessage\n\t\t\t// and schedule a persist.\n\t\t\tstore.listen(\n\t\t\t\t({ changes }) => {\n\t\t\t\t\tthis.diffQueue.push(changes)\n\t\t\t\t\tthis.channel.postMessage(\n\t\t\t\t\t\tmsg({\n\t\t\t\t\t\t\ttype: 'diff',\n\t\t\t\t\t\t\tstoreId: this.store.id,\n\t\t\t\t\t\t\tchanges,\n\t\t\t\t\t\t\tschema: this.serializedSchema,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t\tthis.schedulePersist()\n\t\t\t\t},\n\t\t\t\t{ source: 'user', scope: 'document' }\n\t\t\t)\n\t\t)\n\t\tthis.disposables.add(\n\t\t\tstore.listen(\n\t\t\t\t() => {\n\t\t\t\t\tthis.diffQueue.push(UPDATE_INSTANCE_STATE)\n\t\t\t\t\tthis.schedulePersist()\n\t\t\t\t},\n\t\t\t\t{ scope: 'session' }\n\t\t\t)\n\t\t)\n\n\t\tif (typeof window !== 'undefined') {\n\t\t\t// Persists are throttled, so without this the last PERSIST_THROTTLE_MS of edits are lost\n\t\t\t// whenever the tab is closed or reloaded. `pagehide` is the last reliable event before\n\t\t\t// unload on desktop; on mobile the page can be discarded without it firing, so we also\n\t\t\t// flush when the tab is hidden.\n\t\t\tconst flush = () => {\n\t\t\t\t// Before the initial load completes a full write would overwrite the saved\n\t\t\t\t// document with an empty store.\n\t\t\t\tif (!this.didLoad) return\n\t\t\t\tthis.persistIfNeeded()\n\t\t\t}\n\t\t\tconst onVisibilityChange = () => {\n\t\t\t\tif (document.visibilityState === 'hidden') flush()\n\t\t\t}\n\t\t\twindow.addEventListener('pagehide', flush)\n\t\t\tdocument.addEventListener('visibilitychange', onVisibilityChange)\n\t\t\tthis.disposables.add(() => {\n\t\t\t\twindow.removeEventListener('pagehide', flush)\n\t\t\t\tdocument.removeEventListener('visibilitychange', onVisibilityChange)\n\t\t\t})\n\t\t}\n\n\t\tthis.connect(onLoad, onLoadError)\n\n\t\tthis.documentTypes = new Set(\n\t\t\tObject.values(this.store.schema.types)\n\t\t\t\t.filter((t) => t.scope === 'document')\n\t\t\t\t.map((t) => t.typeName)\n\t\t)\n\t}\n\n\tprivate async connect(onLoad: (client: this) => void, onLoadError: (error: Error) => void) {\n\t\tthis.debug('connecting')\n\t\tlet data: UnpackPromise<ReturnType<LocalIndexedDb['load']>> | undefined\n\n\t\ttry {\n\t\t\tdata = await this.db.load({ sessionId: this.sessionId })\n\t\t} catch (error: any) {\n\t\t\tonLoadError(error)\n\t\t\tshowCantReadFromIndexDbAlert()\n\t\t\treturn\n\t\t}\n\n\t\tthis.debug('loaded data from store', data, 'didDispose', this.didDispose)\n\t\tif (this.didDispose) return\n\n\t\ttry {\n\t\t\tif (data) {\n\t\t\t\tconst documentSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r]))\n\t\t\t\tconst sessionStateSnapshot =\n\t\t\t\t\tdata.sessionStateSnapshot ?? extractSessionStateFromLegacySnapshot(documentSnapshot)\n\t\t\t\tconst migrationResult = this.store.schema.migrateStoreSnapshot({\n\t\t\t\t\tstore: documentSnapshot,\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\tschema: data.schema ?? this.store.schema.serializeEarliestVersion(),\n\t\t\t\t})\n\n\t\t\t\tif (migrationResult.type === 'error') {\n\t\t\t\t\tconsole.error('failed to migrate store', migrationResult)\n\t\t\t\t\tonLoadError(new Error(`Failed to migrate store: ${migrationResult.reason}`))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst records = Object.values(migrationResult.value).filter((r) =>\n\t\t\t\t\tthis.documentTypes.has(r.typeName)\n\t\t\t\t)\n\t\t\t\tif (records.length > 0) {\n\t\t\t\t\t// 3. Merge the changes into the REAL STORE\n\t\t\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t\t\t// Calling put will validate the records!\n\t\t\t\t\t\tthis.store.put(records, 'initialize')\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tif (sessionStateSnapshot) {\n\t\t\t\t\tloadSessionStateSnapshotIntoStore(this.store, sessionStateSnapshot, {\n\t\t\t\t\t\tforceOverwrite: true,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.channel.onmessage = ({ data }) => {\n\t\t\t\tthis.debug('got message', data)\n\t\t\t\tconst msg = data as Message\n\t\t\t\t// if their schema is earlier than ours, we need to tell them so they can refresh\n\t\t\t\t// if their schema is later than ours, we need to refresh\n\t\t\t\tconst res = this.store.schema.getMigrationsSince(msg.schema)\n\n\t\t\t\tif (!res.ok) {\n\t\t\t\t\t// we are older, refresh\n\t\t\t\t\t// but add a safety check to make sure we don't get in an infinite loop\n\t\t\t\t\tconst timeSinceInit = Date.now() - this.initTime\n\t\t\t\t\tif (timeSinceInit < 5000) {\n\t\t\t\t\t\t// This tab was just reloaded, but is out of date compared to other tabs.\n\t\t\t\t\t\t// Not expecting this to ever happen. It should only happen if we roll back a release that incremented\n\t\t\t\t\t\t// the schema version (which we should never do)\n\t\t\t\t\t\t// Or maybe during development if you have multiple local tabs open running the app on prod mode and you\n\t\t\t\t\t\t// check out an older commit. Dev server should be fine.\n\t\t\t\t\t\tonLoadError(new Error('Schema mismatch, please close other tabs and reload the page'))\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tthis.debug('reloading')\n\t\t\t\t\tthis.isReloading = true\n\t\t\t\t\twindow?.location?.reload?.()\n\t\t\t\t\treturn\n\t\t\t\t} else if (res.value.length > 0) {\n\t\t\t\t\t// they are older, tell them to refresh and not write any more data\n\t\t\t\t\tthis.debug('telling them to reload')\n\t\t\t\t\tthis.channel.postMessage({ type: 'announce', schema: this.serializedSchema })\n\t\t\t\t\t// schedule a full db write in case they wrote data anyway\n\t\t\t\t\tthis.shouldDoFullDBWrite = true\n\t\t\t\t\tthis.persistIfNeeded()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// otherwise, all good, same version :)\n\t\t\t\tif (msg.type === 'diff') {\n\t\t\t\t\tthis.debug('applying diff')\n\t\t\t\t\ttransact(() => {\n\t\t\t\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t\t\t\tthis.store.applyDiff(msg.changes as any)\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.channel.postMessage({ type: 'announce', schema: this.serializedSchema })\n\t\t\tthis.disposables.add(() => {\n\t\t\t\tthis.channel.close()\n\t\t\t})\n\t\t\tthis.didLoad = true\n\t\t\tonLoad(this)\n\t\t} catch (e: any) {\n\t\t\tthis.debug('error loading data from store', e)\n\t\t\tif (this.didDispose) return\n\t\t\tonLoadError(e)\n\t\t\treturn\n\t\t}\n\t}\n\n\tclose() {\n\t\tthis.debug('closing')\n\t\tthis.didDispose = true\n\t\tthis.disposables.forEach((d) => d())\n\t\tif (typeof window !== 'undefined' && (window as any).tlsync === this) {\n\t\t\tdelete (window as any).tlsync\n\t\t}\n\t}\n\n\tprivate isPersisting = false\n\tprivate didLastWriteError = false\n\t// eslint-disable-next-line no-restricted-globals\n\tprivate scheduledPersistTimeout: ReturnType<typeof setTimeout> | null = null\n\n\t/**\n\t * Schedule a persist. Persists don't happen immediately: they are throttled to avoid writing too\n\t * often, and will retry if failed.\n\t *\n\t * @internal\n\t */\n\tprivate schedulePersist() {\n\t\tthis.debug('schedulePersist', this.scheduledPersistTimeout)\n\t\tif (this.scheduledPersistTimeout) return\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tthis.scheduledPersistTimeout = setTimeout(\n\t\t\t() => {\n\t\t\t\tthis.scheduledPersistTimeout = null\n\t\t\t\tthis.persistIfNeeded()\n\t\t\t},\n\t\t\tthis.didLastWriteError ? PERSIST_RETRY_THROTTLE_MS : PERSIST_THROTTLE_MS\n\t\t)\n\t}\n\n\t/**\n\t * Persist to IndexedDB only under certain circumstances:\n\t *\n\t * - If we're not already persisting\n\t * - If we're not reloading the page\n\t * - And we have something to persist (a full db write scheduled or changes in the diff queue)\n\t *\n\t * @internal\n\t */\n\tprivate persistIfNeeded() {\n\t\tthis.debug('persistIfNeeded', {\n\t\t\tisPersisting: this.isPersisting,\n\t\t\tisReloading: this.isReloading,\n\t\t\tshouldDoFullDBWrite: this.shouldDoFullDBWrite,\n\t\t\tdiffQueueLength: this.diffQueue.length,\n\t\t\tstoreIsPossiblyCorrupt: this.store.isPossiblyCorrupted(),\n\t\t})\n\n\t\t// if we've scheduled a persist for the future, that's no longer needed\n\t\tif (this.scheduledPersistTimeout) {\n\t\t\tclearTimeout(this.scheduledPersistTimeout)\n\t\t\tthis.scheduledPersistTimeout = null\n\t\t}\n\n\t\t// if a persist is already in progress, we don't need to do anything -\n\t\t// if there are still outstanding changes once it's finished, it'll\n\t\t// schedule another persist\n\t\tif (this.isPersisting) return\n\n\t\t// if we're reloading the page, it's because there's a newer client\n\t\t// present so lets not overwrite their changes\n\t\tif (this.isReloading) return\n\n\t\t// if the store is possibly corrupted, we don't want to persist\n\t\tif (this.store.isPossiblyCorrupted()) return\n\n\t\t// if we're scheduled for a full write or if we have changes outstanding, let's persist them!\n\t\tif (this.shouldDoFullDBWrite || this.diffQueue.length > 0) {\n\t\t\tthis.doPersist()\n\t\t}\n\t}\n\n\t/**\n\t * Actually persist to IndexedDB. If the write fails, then we'll retry with a full db write after\n\t * a short delay.\n\t */\n\tprivate async doPersist() {\n\t\tassert(!this.isPersisting, 'persist already in progress')\n\t\tif (this.didDispose) return\n\t\tthis.isPersisting = true\n\n\t\tthis.debug('doPersist start')\n\n\t\t// instantly empty the diff queue, but keep our own copy of it. this way\n\t\t// diffs that come in during the persist will still get tracked\n\t\tconst diffQueue = this.diffQueue\n\t\tthis.diffQueue = []\n\n\t\ttry {\n\t\t\tif (this.shouldDoFullDBWrite) {\n\t\t\t\tthis.shouldDoFullDBWrite = false\n\t\t\t\tawait this.db.storeSnapshot({\n\t\t\t\t\tschema: this.store.schema,\n\t\t\t\t\tsnapshot: this.store.serialize(),\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\tsessionStateSnapshot: this.$sessionStateSnapshot.get(),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tconst diffs = squashRecordDiffs(\n\t\t\t\t\tdiffQueue.filter((d): d is RecordsDiff<UnknownRecord> => d !== UPDATE_INSTANCE_STATE)\n\t\t\t\t)\n\t\t\t\tawait this.db.storeChanges({\n\t\t\t\t\tchanges: diffs,\n\t\t\t\t\tschema: this.store.schema,\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\tsessionStateSnapshot: this.$sessionStateSnapshot.get(),\n\t\t\t\t})\n\t\t\t}\n\t\t\tthis.didLastWriteError = false\n\t\t} catch (e) {\n\t\t\t// set this.shouldDoFullDBWrite because we clear the diffQueue no matter what,\n\t\t\t// so if this is just a temporary error, we will still persist all changes\n\t\t\tthis.shouldDoFullDBWrite = true\n\t\t\tthis.didLastWriteError = true\n\t\t\tconsole.error('failed to store changes in indexed db', e)\n\n\t\t\tshowCantWriteToIndexDbAlert()\n\t\t\tif (typeof window !== 'undefined') {\n\t\t\t\t// adios\n\t\t\t\twindow.location.reload()\n\t\t\t}\n\t\t}\n\n\t\tthis.isPersisting = false\n\t\tthis.debug('doPersist end')\n\n\t\t// changes might have come in between when we started the persist and\n\t\t// now. we request another persist so any new changes can get written\n\t\tthis.schedulePersist()\n\t}\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAiB,gBAAgB;AACjC,SAAuD,yBAAyB;AAEhF,SAAS,cAAc;AACvB;AAAA,EACC;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,8BAA8B,mCAAmC;AAC1E,SAAS,sBAAsB;AAG/B,MAAM,sBAAsB;AAE5B,MAAM,4BAA4B;AAElC,MAAM,wBAAwB,uBAAO,uBAAuB;AA4B5D,MAAM,MAAM,CAACA,SAAiBA;AAGvB,MAAM,qBAAqB;AAAA,EACjC;AAAA,EACA,YAAY,OAAe;AAAA,EAE3B;AAAA,EACA,YAAY,MAAe;AAAA,EAE3B;AAAA,EACA,QAAQ;AAAA,EAER;AACD;AAEA,MAAM,KAAK,OAAO,qBAAqB,cAAc,uBAAuB;AAGrE,MAAM,kBAAkB;AAAA,EAuB9B,YACiB,OAChB;AAAA,IACC;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,EACD,GAMgB,UAAU,IAAI,GAAG,mBAAmB,cAAc,EAAE,GACnE;AAbe;AAYA;AAEhB,QAAI,OAAO,WAAW,aAAa;AAClC;AAAC,MAAC,OAAe,SAAS;AAAA,IAC3B;AACA,SAAK,iBAAiB;AACtB,SAAK,YAAY;AACjB,SAAK,KAAK,IAAI,eAAe,cAAc;
|
|
4
|
+
"sourcesContent": ["import { Signal, transact } from '@tldraw/state'\nimport { RecordsDiff, SerializedSchema, UnknownRecord, squashRecordDiffs } from '@tldraw/store'\nimport { TLStore } from '@tldraw/tlschema'\nimport { assert } from '@tldraw/utils'\nimport {\n\tTAB_ID,\n\tTLSessionStateSnapshot,\n\tcreateSessionStateSnapshotSignal,\n\textractSessionStateFromLegacySnapshot,\n\tloadSessionStateSnapshotIntoStore,\n} from '../../config/TLSessionStateSnapshot'\nimport { refreshPage } from '../runtime'\nimport { showCantReadFromIndexDbAlert, showCantWriteToIndexDbAlert } from './alerts'\nimport { LocalIndexedDb } from './LocalIndexedDb'\n\n/** How should we debounce persists? */\nconst PERSIST_THROTTLE_MS = 350\n/** If we're in an error state, how long should we wait before retrying a write? */\nconst PERSIST_RETRY_THROTTLE_MS = 10_000\n\nconst UPDATE_INSTANCE_STATE = Symbol('UPDATE_INSTANCE_STATE')\n\n/**\n * IMPORTANT!!!\n *\n * This is just a quick-n-dirty temporary solution that will be replaced with the remote sync client\n * once it has the db integrated\n */\n\ninterface SyncMessage {\n\ttype: 'diff'\n\tstoreId: string\n\tchanges: RecordsDiff<UnknownRecord>\n\tschema: SerializedSchema\n}\n\n// Sent by new clients when they connect\n// If another client is on the channel with a newer schema version\n// It will\ninterface AnnounceMessage {\n\ttype: 'announce'\n\tschema: SerializedSchema\n}\n\ntype Message = SyncMessage | AnnounceMessage\n\ntype UnpackPromise<T> = T extends Promise<infer U> ? U : T\n\nconst msg = (msg: Message) => msg\n\n/** @internal */\nexport class BroadcastChannelMock {\n\tonmessage?: (e: MessageEvent) => void\n\tconstructor(_name: string) {\n\t\t// noop\n\t}\n\tpostMessage(_msg: Message) {\n\t\t// noop\n\t}\n\tclose() {\n\t\t// noop\n\t}\n}\n\nconst BC = typeof BroadcastChannel === 'undefined' ? BroadcastChannelMock : BroadcastChannel\n\n/** @internal */\nexport class TLLocalSyncClient {\n\tprivate disposables = new Set<() => void>()\n\tprivate diffQueue: Array<RecordsDiff<UnknownRecord> | typeof UPDATE_INSTANCE_STATE> = []\n\tprivate didDispose = false\n\tprivate didLoad = false\n\tprivate shouldDoFullDBWrite = true\n\tprivate isReloading = false\n\treadonly persistenceKey: string\n\treadonly sessionId: string\n\treadonly serializedSchema: SerializedSchema\n\tprivate isDebugging = false\n\tprivate readonly documentTypes: ReadonlySet<string>\n\tprivate readonly $sessionStateSnapshot: Signal<TLSessionStateSnapshot | null>\n\t/** @internal */\n\treadonly db: LocalIndexedDb\n\n\tinitTime = Date.now()\n\tprivate debug(...args: any[]) {\n\t\tif (this.isDebugging) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.debug(...args)\n\t\t}\n\t}\n\tconstructor(\n\t\tpublic readonly store: TLStore,\n\t\t{\n\t\t\tpersistenceKey,\n\t\t\tsessionId = TAB_ID,\n\t\t\tonLoad,\n\t\t\tonLoadError,\n\t\t}: {\n\t\t\tpersistenceKey: string\n\t\t\tsessionId?: string\n\t\t\tonLoad(self: TLLocalSyncClient): void\n\t\t\tonLoadError(error: Error): void\n\t\t},\n\t\tpublic readonly channel = new BC(`tldraw-tab-sync-${persistenceKey}`)\n\t) {\n\t\tif (typeof window !== 'undefined') {\n\t\t\t;(window as any).tlsync = this\n\t\t}\n\t\tthis.persistenceKey = persistenceKey\n\t\tthis.sessionId = sessionId\n\t\tthis.db = new LocalIndexedDb(persistenceKey)\n\n\t\tthis.serializedSchema = this.store.schema.serialize()\n\t\tthis.$sessionStateSnapshot = createSessionStateSnapshotSignal(this.store)\n\n\t\tthis.disposables.add(\n\t\t\t// Set up a subscription to changes from the store: When\n\t\t\t// the store changes (and if the change was made by the user)\n\t\t\t// then immediately send the diff to other tabs via postMessage\n\t\t\t// and schedule a persist.\n\t\t\tstore.listen(\n\t\t\t\t({ changes }) => {\n\t\t\t\t\tthis.diffQueue.push(changes)\n\t\t\t\t\tthis.channel.postMessage(\n\t\t\t\t\t\tmsg({\n\t\t\t\t\t\t\ttype: 'diff',\n\t\t\t\t\t\t\tstoreId: this.store.id,\n\t\t\t\t\t\t\tchanges,\n\t\t\t\t\t\t\tschema: this.serializedSchema,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t\tthis.schedulePersist()\n\t\t\t\t},\n\t\t\t\t{ source: 'user', scope: 'document' }\n\t\t\t)\n\t\t)\n\t\tthis.disposables.add(\n\t\t\tstore.listen(\n\t\t\t\t() => {\n\t\t\t\t\tthis.diffQueue.push(UPDATE_INSTANCE_STATE)\n\t\t\t\t\tthis.schedulePersist()\n\t\t\t\t},\n\t\t\t\t{ scope: 'session' }\n\t\t\t)\n\t\t)\n\n\t\tif (typeof window !== 'undefined') {\n\t\t\t// Persists are throttled, so without this the last PERSIST_THROTTLE_MS of edits are lost\n\t\t\t// whenever the tab is closed or reloaded. `pagehide` is the last reliable event before\n\t\t\t// unload on desktop; on mobile the page can be discarded without it firing, so we also\n\t\t\t// flush when the tab is hidden.\n\t\t\tconst flush = () => {\n\t\t\t\t// Before the initial load completes a full write would overwrite the saved\n\t\t\t\t// document with an empty store.\n\t\t\t\tif (!this.didLoad) return\n\t\t\t\tthis.persistIfNeeded()\n\t\t\t}\n\t\t\tconst onVisibilityChange = () => {\n\t\t\t\tif (document.visibilityState === 'hidden') flush()\n\t\t\t}\n\t\t\twindow.addEventListener('pagehide', flush)\n\t\t\tdocument.addEventListener('visibilitychange', onVisibilityChange)\n\t\t\tthis.disposables.add(() => {\n\t\t\t\twindow.removeEventListener('pagehide', flush)\n\t\t\t\tdocument.removeEventListener('visibilitychange', onVisibilityChange)\n\t\t\t})\n\t\t}\n\n\t\tthis.connect(onLoad, onLoadError)\n\n\t\tthis.documentTypes = new Set(\n\t\t\tObject.values(this.store.schema.types)\n\t\t\t\t.filter((t) => t.scope === 'document')\n\t\t\t\t.map((t) => t.typeName)\n\t\t)\n\t}\n\n\tprivate async connect(onLoad: (client: this) => void, onLoadError: (error: Error) => void) {\n\t\tthis.debug('connecting')\n\t\tlet data: UnpackPromise<ReturnType<LocalIndexedDb['load']>> | undefined\n\n\t\tconst handleMessage = (msg: Message) => {\n\t\t\t// if their schema is earlier than ours, we need to tell them so they can refresh\n\t\t\t// if their schema is later than ours, we need to refresh\n\t\t\tconst res = this.store.schema.getMigrationsSince(msg.schema)\n\n\t\t\tif (!res.ok) {\n\t\t\t\tthis.isReloading = true\n\t\t\t\t// we are older, refresh\n\t\t\t\t// but add a safety check to make sure we don't get in an infinite loop\n\t\t\t\tconst timeSinceInit = Date.now() - this.initTime\n\t\t\t\tif (timeSinceInit < 5000) {\n\t\t\t\t\t// This tab was just reloaded, but is out of date compared to other tabs.\n\t\t\t\t\t// Not expecting this to ever happen. It should only happen if we roll back a release that incremented\n\t\t\t\t\t// the schema version (which we should never do)\n\t\t\t\t\t// Or maybe during development if you have multiple local tabs open running the app on prod mode and you\n\t\t\t\t\t// check out an older commit. Dev server should be fine.\n\t\t\t\t\tonLoadError(new Error('Schema mismatch, please close other tabs and reload the page'))\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tthis.debug('reloading')\n\t\t\t\tif (typeof window !== 'undefined') refreshPage()\n\t\t\t\treturn false\n\t\t\t} else if (res.value.length > 0) {\n\t\t\t\t// they are older, tell them to refresh and not write any more data\n\t\t\t\tthis.debug('telling them to reload')\n\t\t\t\tthis.channel.postMessage({ type: 'announce', schema: this.serializedSchema })\n\t\t\t\t// schedule a full db write in case they wrote data anyway\n\t\t\t\tthis.shouldDoFullDBWrite = true\n\t\t\t\tthis.schedulePersist()\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// otherwise, all good, same version :)\n\t\t\tif (msg.type === 'diff') {\n\t\t\t\tthis.debug('applying diff')\n\t\t\t\ttransact(() => {\n\t\t\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t\t\tthis.store.applyDiff(msg.changes as any)\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\n\t\t// Listen from the start and buffer until we've loaded: diffs other tabs broadcast while\n\t\t// we're still reading from IndexedDB would otherwise be dropped, then erased by our first\n\t\t// (full) db write.\n\t\tlet bufferedMessages: Message[] | null = []\n\t\tthis.channel.onmessage = ({ data }) => {\n\t\t\tthis.debug('got message', data)\n\t\t\tif (bufferedMessages) {\n\t\t\t\tbufferedMessages.push(data as Message)\n\t\t\t} else {\n\t\t\t\thandleMessage(data as Message)\n\t\t\t}\n\t\t}\n\t\tthis.disposables.add(() => {\n\t\t\tthis.channel.close()\n\t\t})\n\n\t\ttry {\n\t\t\tdata = await this.db.load({ sessionId: this.sessionId })\n\t\t} catch (error: any) {\n\t\t\tonLoadError(error)\n\t\t\tshowCantReadFromIndexDbAlert()\n\t\t\treturn\n\t\t}\n\n\t\tthis.debug('loaded data from store', data, 'didDispose', this.didDispose)\n\t\tif (this.didDispose) return\n\n\t\ttry {\n\t\t\tif (data) {\n\t\t\t\tconst documentSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r]))\n\t\t\t\tconst sessionStateSnapshot =\n\t\t\t\t\tdata.sessionStateSnapshot ?? extractSessionStateFromLegacySnapshot(documentSnapshot)\n\t\t\t\tconst migrationResult = this.store.schema.migrateStoreSnapshot({\n\t\t\t\t\tstore: documentSnapshot,\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\tschema: data.schema ?? this.store.schema.serializeEarliestVersion(),\n\t\t\t\t})\n\n\t\t\t\tif (migrationResult.type === 'error') {\n\t\t\t\t\tconsole.error('failed to migrate store', migrationResult)\n\t\t\t\t\tonLoadError(new Error(`Failed to migrate store: ${migrationResult.reason}`))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconst records = Object.values(migrationResult.value).filter((r) =>\n\t\t\t\t\tthis.documentTypes.has(r.typeName)\n\t\t\t\t)\n\t\t\t\tif (records.length > 0) {\n\t\t\t\t\t// 3. Merge the changes into the REAL STORE\n\t\t\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t\t\t// Calling put will validate the records!\n\t\t\t\t\t\tthis.store.put(records, 'initialize')\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tif (sessionStateSnapshot) {\n\t\t\t\t\tloadSessionStateSnapshotIntoStore(this.store, sessionStateSnapshot, {\n\t\t\t\t\t\tforceOverwrite: true,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Older-schema announcements schedule writes, so drain every diff before those writes\n\t\t\t// or an onLoad callback that immediately closes the client can capture a snapshot.\n\t\t\tconst messages = bufferedMessages\n\t\t\tbufferedMessages = null\n\t\t\tfor (const message of messages) {\n\t\t\t\tif (!handleMessage(message)) return\n\t\t\t}\n\t\t\tthis.didLoad = true\n\t\t\tif (this.diffQueue.length > 0) this.schedulePersist()\n\n\t\t\tthis.channel.postMessage({ type: 'announce', schema: this.serializedSchema })\n\t\t\tonLoad(this)\n\t\t} catch (e: any) {\n\t\t\tthis.debug('error loading data from store', e)\n\t\t\tif (this.didDispose) return\n\t\t\tonLoadError(e)\n\t\t\treturn\n\t\t}\n\t}\n\n\tclose() {\n\t\tif (this.didDispose) return\n\t\tthis.debug('closing')\n\t\tthis.didDispose = true\n\t\t// Store listeners normally run on the next frame; collect the last edits before removal.\n\t\tthis.store._flushHistory()\n\t\tthis.disposables.forEach((d) => d())\n\t\tif (this.scheduledPersistTimeout) {\n\t\t\tclearTimeout(this.scheduledPersistTimeout)\n\t\t\tthis.scheduledPersistTimeout = null\n\t\t}\n\t\tif (typeof window !== 'undefined' && (window as any).tlsync === this) {\n\t\t\tdelete (window as any).tlsync\n\t\t}\n\t\tthis.flushAndCloseDb()\n\t}\n\n\t/**\n\t * Flush queued changes so edits made in the last PERSIST_THROTTLE_MS before unmount aren't lost,\n\t * then close the db. Never flushes before load (a full write then would wipe the db with an empty\n\t * store), and only when something is queued: shouldDoFullDBWrite stays true until the first\n\t * persist, so an unconditional flush would rewrite the whole document on every no-op close.\n\t */\n\tprivate flushAndCloseDb() {\n\t\t// A persist in flight can't be joined (persistIfNeeded bails while one is running) and,\n\t\t// now that we're disposed, won't schedule a follow-up. Edits queued during that write\n\t\t// would be lost if the db closed now, so wait for it and flush again.\n\t\tif (this.persistPromise) {\n\t\t\tthis.persistPromise.then(() => this.flushAndCloseDb())\n\t\t\treturn\n\t\t}\n\t\tif (this.didLoad && this.diffQueue.length > 0) this.persistIfNeeded()\n\t\t// the db waits for the transaction the flush just opened before actually closing\n\t\tthis.db.close()\n\t}\n\n\tprivate isPersisting = false\n\tprivate persistPromise: Promise<void> | null = null\n\tprivate didLastWriteError = false\n\t// eslint-disable-next-line no-restricted-globals\n\tprivate scheduledPersistTimeout: ReturnType<typeof setTimeout> | null = null\n\n\t/**\n\t * Schedule a persist. Persists don't happen immediately: they are throttled to avoid writing too\n\t * often, and will retry if failed.\n\t *\n\t * @internal\n\t */\n\tprivate schedulePersist() {\n\t\tthis.debug('schedulePersist', this.scheduledPersistTimeout)\n\t\tif (this.scheduledPersistTimeout) return\n\t\t// A deferred persist would try to write to a database that is closing or already closed.\n\t\tif (this.didDispose) return\n\t\t// eslint-disable-next-line no-restricted-globals\n\t\tthis.scheduledPersistTimeout = setTimeout(\n\t\t\t() => {\n\t\t\t\tthis.scheduledPersistTimeout = null\n\t\t\t\tthis.persistIfNeeded()\n\t\t\t},\n\t\t\tthis.didLastWriteError ? PERSIST_RETRY_THROTTLE_MS : PERSIST_THROTTLE_MS\n\t\t)\n\t}\n\n\t/**\n\t * Persist to IndexedDB only under certain circumstances:\n\t *\n\t * - If we're not already persisting\n\t * - If we're not reloading the page\n\t * - And we have something to persist (a full db write scheduled or changes in the diff queue)\n\t *\n\t * @internal\n\t */\n\tprivate persistIfNeeded() {\n\t\tthis.debug('persistIfNeeded', {\n\t\t\tisPersisting: this.isPersisting,\n\t\t\tisReloading: this.isReloading,\n\t\t\tshouldDoFullDBWrite: this.shouldDoFullDBWrite,\n\t\t\tdiffQueueLength: this.diffQueue.length,\n\t\t\tstoreIsPossiblyCorrupt: this.store.isPossiblyCorrupted(),\n\t\t})\n\n\t\t// if we've scheduled a persist for the future, that's no longer needed\n\t\tif (this.scheduledPersistTimeout) {\n\t\t\tclearTimeout(this.scheduledPersistTimeout)\n\t\t\tthis.scheduledPersistTimeout = null\n\t\t}\n\n\t\tif (!this.didLoad) return\n\n\t\t// if a persist is already in progress, we don't need to do anything -\n\t\t// if there are still outstanding changes once it's finished, it'll\n\t\t// schedule another persist\n\t\tif (this.isPersisting) return\n\n\t\t// if we're reloading the page, it's because there's a newer client\n\t\t// present so lets not overwrite their changes\n\t\tif (this.isReloading) return\n\n\t\t// if the store is possibly corrupted, we don't want to persist\n\t\tif (this.store.isPossiblyCorrupted()) return\n\n\t\t// if we're scheduled for a full write or if we have changes outstanding, let's persist them!\n\t\tif (this.shouldDoFullDBWrite || this.diffQueue.length > 0) {\n\t\t\tthis.persistPromise = this.doPersist()\n\t\t}\n\t}\n\n\t/**\n\t * Actually persist to IndexedDB. If the write fails, then we'll retry with a full db write after\n\t * a short delay.\n\t */\n\tprivate async doPersist() {\n\t\tassert(!this.isPersisting, 'persist already in progress')\n\t\tthis.isPersisting = true\n\n\t\tthis.debug('doPersist start')\n\n\t\t// instantly empty the diff queue, but keep our own copy of it. this way\n\t\t// diffs that come in during the persist will still get tracked\n\t\tconst diffQueue = this.diffQueue\n\t\tthis.diffQueue = []\n\n\t\ttry {\n\t\t\tif (this.shouldDoFullDBWrite) {\n\t\t\t\tthis.shouldDoFullDBWrite = false\n\t\t\t\tawait this.db.storeSnapshot({\n\t\t\t\t\tschema: this.store.schema,\n\t\t\t\t\tsnapshot: this.store.serialize(),\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\tsessionStateSnapshot: this.$sessionStateSnapshot.get(),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tconst diffs = squashRecordDiffs(\n\t\t\t\t\tdiffQueue.filter((d): d is RecordsDiff<UnknownRecord> => d !== UPDATE_INSTANCE_STATE)\n\t\t\t\t)\n\t\t\t\tawait this.db.storeChanges({\n\t\t\t\t\tchanges: diffs,\n\t\t\t\t\tschema: this.store.schema,\n\t\t\t\t\tsessionId: this.sessionId,\n\t\t\t\t\tsessionStateSnapshot: this.$sessionStateSnapshot.get(),\n\t\t\t\t})\n\t\t\t}\n\t\t\tthis.didLastWriteError = false\n\t\t} catch (e) {\n\t\t\t// set this.shouldDoFullDBWrite because we clear the diffQueue no matter what,\n\t\t\t// so if this is just a temporary error, we will still persist all changes\n\t\t\tthis.shouldDoFullDBWrite = true\n\t\t\tthis.didLastWriteError = true\n\t\t\tconsole.error('failed to store changes in indexed db', e)\n\n\t\t\tif (!this.didDispose && typeof window !== 'undefined') {\n\t\t\t\tshowCantWriteToIndexDbAlert()\n\t\t\t\trefreshPage()\n\t\t\t}\n\t\t}\n\n\t\tthis.isPersisting = false\n\t\tthis.persistPromise = null\n\t\tthis.debug('doPersist end')\n\n\t\t// changes might have come in between when we started the persist and\n\t\t// now. we request another persist so any new changes can get written\n\t\tthis.schedulePersist()\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["msg", "data"]
|
|
7
7
|
}
|
package/dist-esm/version.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const version = "5.5.0-next.
|
|
1
|
+
const version = "5.5.0-next.b6a9f7cd7be2";
|
|
2
2
|
const publishDates = {
|
|
3
3
|
major: "2026-05-06T16:28:18.473Z",
|
|
4
|
-
minor: "2026-09-
|
|
5
|
-
patch: "2026-09-
|
|
4
|
+
minor: "2026-09-16T11:13:34.604Z",
|
|
5
|
+
patch: "2026-09-16T11:13:34.604Z"
|
|
6
6
|
};
|
|
7
7
|
export {
|
|
8
8
|
publishDates,
|
package/dist-esm/version.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.5.0-next.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.5.0-next.b6a9f7cd7be2'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-09-16T11:13:34.604Z',\n\tpatch: '2026-09-16T11:13:34.604Z',\n}\n"],
|
|
5
5
|
"mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/editor.css
CHANGED
|
@@ -650,7 +650,15 @@ input,
|
|
|
650
650
|
overflow: hidden;
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
-
|
|
653
|
+
/**
|
|
654
|
+
* Only when there's actually a highlight to style. ProseMirror hides the native selection whenever
|
|
655
|
+
* it holds a NodeSelection — triple-clicking a list's bullet selects the whole list, for one — by
|
|
656
|
+
* zeroing the ::selection background and nothing else. Painting the selected-contrast color with
|
|
657
|
+
* no highlight behind it leaves the text invisible against the shape, and `inherit` can't undo it:
|
|
658
|
+
* highlight pseudo-elements inherit from the ancestor's ::selection, so it just picks the same
|
|
659
|
+
* color back up.
|
|
660
|
+
*/
|
|
661
|
+
.tl-text-input:not(:has(.ProseMirror-hideselection))::selection {
|
|
654
662
|
background: var(--tl-color-selected);
|
|
655
663
|
color: var(--tl-color-selected-contrast);
|
|
656
664
|
text-shadow: none;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (editor).",
|
|
4
|
-
"version": "5.5.0-next.
|
|
4
|
+
"version": "5.5.0-next.b6a9f7cd7be2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"test-ci": "yarn run -T vitest run --passWithNoTests",
|
|
44
44
|
"test": "yarn run -T vitest --passWithNoTests",
|
|
45
|
-
"benchmark": "yarn run -T tsx ./internal/scripts/benchmark.ts",
|
|
46
45
|
"test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
|
|
47
46
|
"build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
|
|
48
47
|
"build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
|
|
@@ -55,12 +54,12 @@
|
|
|
55
54
|
"@tiptap/core": "^3.12.1",
|
|
56
55
|
"@tiptap/pm": "^3.12.1",
|
|
57
56
|
"@tiptap/react": "^3.12.1",
|
|
58
|
-
"@tldraw/state": "5.5.0-next.
|
|
59
|
-
"@tldraw/state-react": "5.5.0-next.
|
|
60
|
-
"@tldraw/store": "5.5.0-next.
|
|
61
|
-
"@tldraw/tlschema": "5.5.0-next.
|
|
62
|
-
"@tldraw/utils": "5.5.0-next.
|
|
63
|
-
"@tldraw/validate": "5.5.0-next.
|
|
57
|
+
"@tldraw/state": "5.5.0-next.b6a9f7cd7be2",
|
|
58
|
+
"@tldraw/state-react": "5.5.0-next.b6a9f7cd7be2",
|
|
59
|
+
"@tldraw/store": "5.5.0-next.b6a9f7cd7be2",
|
|
60
|
+
"@tldraw/tlschema": "5.5.0-next.b6a9f7cd7be2",
|
|
61
|
+
"@tldraw/utils": "5.5.0-next.b6a9f7cd7be2",
|
|
62
|
+
"@tldraw/validate": "5.5.0-next.b6a9f7cd7be2",
|
|
64
63
|
"classnames": "^2.5.1",
|
|
65
64
|
"eventemitter3": "^4.0.7",
|
|
66
65
|
"idb": "^7.1.1",
|
|
@@ -75,18 +74,16 @@
|
|
|
75
74
|
"@peculiar/webcrypto": "^1.5.0",
|
|
76
75
|
"@testing-library/dom": "^10.0.0",
|
|
77
76
|
"@testing-library/react": "^16.0.0",
|
|
78
|
-
"@types/benchmark": "^2.1.5",
|
|
79
77
|
"@types/rbush": "^3.0.0",
|
|
80
78
|
"@types/react": "^19.2.7",
|
|
81
79
|
"@types/react-dom": "^19.2.3",
|
|
82
80
|
"@types/wicg-file-system-access": "^2020.9.8",
|
|
83
|
-
"benchmark": "^2.1.4",
|
|
84
81
|
"fake-indexeddb": "^4.0.2",
|
|
85
82
|
"lazyrepo": "0.0.0-alpha.27",
|
|
86
83
|
"react": "^19.2.1",
|
|
87
84
|
"react-dom": "^19.2.1",
|
|
88
85
|
"resize-observer-polyfill": "^1.5.1",
|
|
89
|
-
"vitest": "^
|
|
86
|
+
"vitest": "^5.0.0"
|
|
90
87
|
},
|
|
91
88
|
"engines": {
|
|
92
89
|
"node": ">=22.12.0"
|
package/src/lib/TldrawEditor.tsx
CHANGED
|
@@ -46,7 +46,7 @@ import { EditorProvider, useEditor } from './hooks/useEditor'
|
|
|
46
46
|
import { EditorComponentsProvider } from './hooks/useEditorComponents'
|
|
47
47
|
import { useEvent } from './hooks/useEvent'
|
|
48
48
|
import { useForceUpdate } from './hooks/useForceUpdate'
|
|
49
|
-
import { useShallowObjectIdentity } from './hooks/useIdentity'
|
|
49
|
+
import { useDeepObjectIdentity, useShallowObjectIdentity } from './hooks/useIdentity'
|
|
50
50
|
import { useLocalStore } from './hooks/useLocalStore'
|
|
51
51
|
import { useRefState } from './hooks/useRefState'
|
|
52
52
|
import { useStateAttribute } from './hooks/useStateAttribute'
|
|
@@ -311,18 +311,24 @@ export const TldrawEditor = memo(function TldrawEditor({
|
|
|
311
311
|
const ErrorFallback =
|
|
312
312
|
components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback
|
|
313
313
|
|
|
314
|
-
// Merge deprecated props with options
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
314
|
+
// Merge deprecated props with options (options win). `options` is a dependency of the
|
|
315
|
+
// editor-creating effect, so it's shallow-stabilised below; the nested objects are
|
|
316
|
+
// deep-stabilised here first, or an inline `options={{ camera: { ... } }}` would still
|
|
317
|
+
// recreate the editor on every render. `text` needs the deep comparison as well: `<Tldraw>`
|
|
318
|
+
// builds a fresh `tipTapConfig` inside it whenever its own `options.text` is a new identity.
|
|
319
|
+
const camera = useDeepObjectIdentity(_options?.camera)
|
|
320
|
+
const gridSteps = useDeepObjectIdentity(_options?.gridSteps)
|
|
321
|
+
const text = useDeepObjectIdentity(_options?.text ?? _textOptions)
|
|
322
|
+
const mergedDeepLinks = _options?.deepLinks ?? _deepLinks
|
|
323
|
+
const deepLinkOptions = useDeepObjectIdentity(
|
|
324
|
+
mergedDeepLinks === true ? undefined : mergedDeepLinks
|
|
325
|
+
)
|
|
326
|
+
const deepLinks = mergedDeepLinks === true ? true : deepLinkOptions
|
|
327
|
+
let mergedOptions = _options
|
|
328
|
+
if (camera !== undefined) mergedOptions = { ...mergedOptions, camera }
|
|
329
|
+
if (gridSteps !== undefined) mergedOptions = { ...mergedOptions, gridSteps }
|
|
330
|
+
if (text !== undefined) mergedOptions = { ...mergedOptions, text }
|
|
331
|
+
if (deepLinks !== undefined) mergedOptions = { ...mergedOptions, deepLinks }
|
|
326
332
|
|
|
327
333
|
// apply defaults. if you're using the bare @tldraw/editor package, we
|
|
328
334
|
// default these to the "tldraw zero" configuration. We have different
|
|
@@ -669,19 +675,25 @@ function TldrawEditorWithReadyStore({
|
|
|
669
675
|
useEffect(
|
|
670
676
|
function handleFocusOnPointerDownForPreserveFocusMode() {
|
|
671
677
|
if (!editor) return
|
|
678
|
+
const container = editor.getContainer()
|
|
672
679
|
|
|
673
680
|
function handleFocusOnPointerDown() {
|
|
674
681
|
if (!editor) return
|
|
675
682
|
editor.focus()
|
|
676
683
|
}
|
|
677
684
|
|
|
678
|
-
function handleBlurOnPointerDown() {
|
|
685
|
+
function handleBlurOnPointerDown(e: PointerEvent) {
|
|
679
686
|
if (!editor) return
|
|
687
|
+
// The same pointerdown bubbles from the container to the body; blurring here would
|
|
688
|
+
// cancel the interaction the container listener just focused for. Check the composed
|
|
689
|
+
// path rather than `e.target`: when the editor lives in an open shadow root the target
|
|
690
|
+
// is retargeted to the shadow host by the time the event reaches the body. (A closed
|
|
691
|
+
// shadow root truncates the path at the host, so those still blur.)
|
|
692
|
+
if (e.composedPath().includes(container)) return
|
|
680
693
|
editor.blur()
|
|
681
694
|
}
|
|
682
695
|
|
|
683
696
|
if (autoFocus && noAutoFocus()) {
|
|
684
|
-
const container = editor.getContainer()
|
|
685
697
|
container.addEventListener('pointerdown', handleFocusOnPointerDown)
|
|
686
698
|
container.ownerDocument.body.addEventListener('pointerdown', handleBlurOnPointerDown)
|
|
687
699
|
|
|
@@ -29,6 +29,7 @@ export function MenuClickCapture() {
|
|
|
29
29
|
isDown: false,
|
|
30
30
|
isDragging: false,
|
|
31
31
|
button: 0,
|
|
32
|
+
pointerId: -1,
|
|
32
33
|
start: new Vec(),
|
|
33
34
|
})
|
|
34
35
|
|
|
@@ -69,6 +70,11 @@ export function MenuClickCapture() {
|
|
|
69
70
|
|
|
70
71
|
const handlePointerDown = useCallback(
|
|
71
72
|
(e: PointerEvent) => {
|
|
73
|
+
// This overlay tracks a single press. A second pointer landing while one is active —
|
|
74
|
+
// a palm resting on the glass beside a pen — must not take over the gesture, or its
|
|
75
|
+
// own cancel would end the press it stole.
|
|
76
|
+
if (rPointerState.current.isDown) return
|
|
77
|
+
|
|
72
78
|
const button = getPointerEventButton(e)
|
|
73
79
|
if (button !== 0 && button !== 2) return
|
|
74
80
|
|
|
@@ -94,6 +100,7 @@ export function MenuClickCapture() {
|
|
|
94
100
|
isDown: true,
|
|
95
101
|
isDragging: false,
|
|
96
102
|
button,
|
|
103
|
+
pointerId: e.pointerId,
|
|
97
104
|
start: new Vec(e.clientX, e.clientY),
|
|
98
105
|
}
|
|
99
106
|
|
|
@@ -111,7 +118,7 @@ export function MenuClickCapture() {
|
|
|
111
118
|
const handlePointerMove = useCallback(
|
|
112
119
|
(e: PointerEvent) => {
|
|
113
120
|
const state = rPointerState.current
|
|
114
|
-
if (!state.isDown) return
|
|
121
|
+
if (!state.isDown || state.pointerId !== e.pointerId) return
|
|
115
122
|
|
|
116
123
|
// Left-click: wait for the drag threshold before forwarding anything, then
|
|
117
124
|
// replay pointerdown at the original start so the editor records the
|
|
@@ -144,15 +151,17 @@ export function MenuClickCapture() {
|
|
|
144
151
|
|
|
145
152
|
const handlePointerUp = useCallback(
|
|
146
153
|
(e: PointerEvent) => {
|
|
147
|
-
const
|
|
148
|
-
|
|
154
|
+
const state = rPointerState.current
|
|
155
|
+
if (!state.isDown || state.pointerId !== e.pointerId) return
|
|
156
|
+
|
|
157
|
+
const isStaticRightClick = state.button === 2 && !state.isDragging
|
|
149
158
|
|
|
150
159
|
editor.dispatch({
|
|
151
160
|
type: 'pointer',
|
|
152
161
|
target: 'canvas',
|
|
153
162
|
name: 'pointer_up',
|
|
154
163
|
...getPointerInfo(editor, e),
|
|
155
|
-
button:
|
|
164
|
+
button: state.button === 2 ? 2 : getPointerEventButton(e),
|
|
156
165
|
})
|
|
157
166
|
|
|
158
167
|
if (isStaticRightClick && editor.options.rightClickPanning) {
|
|
@@ -183,12 +192,53 @@ export function MenuClickCapture() {
|
|
|
183
192
|
isDown: false,
|
|
184
193
|
isDragging: false,
|
|
185
194
|
button: 0,
|
|
195
|
+
pointerId: -1,
|
|
186
196
|
start: new Vec(e.clientX, e.clientY),
|
|
187
197
|
}
|
|
188
198
|
},
|
|
189
199
|
[editor]
|
|
190
200
|
)
|
|
191
201
|
|
|
202
|
+
const handlePointerCancel = useCallback(
|
|
203
|
+
(e: PointerEvent) => {
|
|
204
|
+
const state = rPointerState.current
|
|
205
|
+
if (!state.isDown || state.pointerId !== e.pointerId) {
|
|
206
|
+
// Either a right-click press that was forwarded to the canvas, or a pointer this
|
|
207
|
+
// overlay never tracked. Neither is ours to end, so let the canvas handler decide.
|
|
208
|
+
canvasEvents.onPointerCancel?.(e)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// The canvas's own cancel handler doesn't know about this element's pointer
|
|
213
|
+
// state, so without this the overlay stays mounted over the canvas and
|
|
214
|
+
// swallows the next interaction after the browser cancels a press.
|
|
215
|
+
if (editor.inputs.getIsPointing()) {
|
|
216
|
+
editor.interrupt()
|
|
217
|
+
editor.dispatch({
|
|
218
|
+
type: 'pointer',
|
|
219
|
+
target: 'canvas',
|
|
220
|
+
name: 'pointer_up',
|
|
221
|
+
...getPointerInfo(editor, e),
|
|
222
|
+
// pointercancel reports button -1
|
|
223
|
+
button: state.button,
|
|
224
|
+
})
|
|
225
|
+
} else {
|
|
226
|
+
editor.markEventAsHandled(e)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
releasePointerCapture(e.currentTarget, e)
|
|
230
|
+
setIsPointing(false)
|
|
231
|
+
rPointerState.current = {
|
|
232
|
+
isDown: false,
|
|
233
|
+
isDragging: false,
|
|
234
|
+
button: 0,
|
|
235
|
+
pointerId: -1,
|
|
236
|
+
start: new Vec(e.clientX, e.clientY),
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
[canvasEvents, editor]
|
|
240
|
+
)
|
|
241
|
+
|
|
192
242
|
return (
|
|
193
243
|
showElement && (
|
|
194
244
|
<div
|
|
@@ -198,6 +248,7 @@ export function MenuClickCapture() {
|
|
|
198
248
|
onPointerDown={handlePointerDown}
|
|
199
249
|
onPointerMove={handlePointerMove}
|
|
200
250
|
onPointerUp={handlePointerUp}
|
|
251
|
+
onPointerCancel={handlePointerCancel}
|
|
201
252
|
onContextMenu={(e) => {
|
|
202
253
|
e.preventDefault()
|
|
203
254
|
e.stopPropagation()
|
|
@@ -96,6 +96,26 @@ describe('updateShape', () => {
|
|
|
96
96
|
})
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
+
describe('setErasingShapes', () => {
|
|
100
|
+
it('sorts a copy of the ids rather than the array the caller passed in', () => {
|
|
101
|
+
const a = createShapeId('a')
|
|
102
|
+
const b = createShapeId('b')
|
|
103
|
+
editor.createShapes([
|
|
104
|
+
{ id: a, type: 'my-custom-shape', props: { w: 100, h: 100, text: undefined } },
|
|
105
|
+
{ id: b, type: 'my-custom-shape', props: { w: 100, h: 100, text: undefined } },
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
// the store freezes the arrays it owns, so sorting one in place throws
|
|
109
|
+
const ids = [b, a]
|
|
110
|
+
Object.freeze(ids)
|
|
111
|
+
|
|
112
|
+
editor.setErasingShapes(ids)
|
|
113
|
+
|
|
114
|
+
expect(ids).toEqual([b, a])
|
|
115
|
+
expect(editor.getErasingShapeIds()).toEqual([a, b])
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
99
119
|
describe('updateViewportScreenBounds', () => {
|
|
100
120
|
it('bails without crashing or updating bounds when the container document has no body', () => {
|
|
101
121
|
const documentSpy = vi
|