@tldraw/editor 3.14.2 → 3.15.0-canary.039c346f1d6f
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 +23 -4
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +7 -1
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +20 -2
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +7 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/hooks/useEditor.js +1 -4
- package/dist-cjs/lib/hooks/useEditor.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +3 -1
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
- package/dist-cjs/lib/primitives/intersect.js +4 -4
- package/dist-cjs/lib/primitives/intersect.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +4 -0
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +0 -1
- 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 +23 -4
- package/dist-esm/index.mjs +10 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +7 -1
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +20 -2
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +7 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEditor.mjs +1 -4
- package/dist-esm/lib/hooks/useEditor.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +3 -1
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
- package/dist-esm/lib/primitives/intersect.mjs +5 -5
- package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +4 -0
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +0 -1
- 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/package.json +7 -7
- package/src/index.ts +8 -1
- package/src/lib/config/TLUserPreferences.ts +7 -0
- package/src/lib/editor/Editor.test.ts +407 -0
- package/src/lib/editor/Editor.ts +30 -5
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -0
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
- package/src/lib/hooks/useEditor.tsx +6 -5
- package/src/lib/primitives/geometry/Arc2d.ts +2 -2
- package/src/lib/primitives/geometry/Circle2d.ts +2 -2
- package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -1
- package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
- package/src/lib/primitives/geometry/geometry-constants.ts +2 -1
- package/src/lib/primitives/intersect.test.ts +946 -0
- package/src/lib/primitives/intersect.ts +12 -5
- package/src/lib/primitives/utils.ts +11 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +0 -1
- package/src/version.ts +3 -3
|
@@ -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 { LocalIndexedDb } from './LocalIndexedDb'\nimport { showCantReadFromIndexDbAlert, showCantWriteToIndexDbAlert } from './alerts'\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 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\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\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}\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": "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
|
|
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 { LocalIndexedDb } from './LocalIndexedDb'\nimport { showCantReadFromIndexDbAlert, showCantWriteToIndexDbAlert } from './alerts'\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 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\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\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}\n\n\tprivate isPersisting = false\n\tprivate didLastWriteError = false\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": "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",
|
|
6
6
|
"names": ["msg", "data"]
|
|
7
7
|
}
|
package/dist-esm/version.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const version = "3.
|
|
1
|
+
const version = "3.15.0-canary.039c346f1d6f";
|
|
2
2
|
const publishDates = {
|
|
3
3
|
major: "2024-09-13T14:36:29.063Z",
|
|
4
|
-
minor: "2025-07-
|
|
5
|
-
patch: "2025-07-
|
|
4
|
+
minor: "2025-07-11T16:17:04.725Z",
|
|
5
|
+
patch: "2025-07-11T16:17:04.725Z"
|
|
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 = '3.
|
|
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 = '3.15.0-canary.039c346f1d6f'\nexport const publishDates = {\n\tmajor: '2024-09-13T14:36:29.063Z',\n\tminor: '2025-07-11T16:17:04.725Z',\n\tpatch: '2025-07-11T16:17:04.725Z',\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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "A tiny little drawing app (editor).",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.15.0-canary.039c346f1d6f",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"@tiptap/core": "^2.9.1",
|
|
49
49
|
"@tiptap/pm": "^2.9.1",
|
|
50
50
|
"@tiptap/react": "^2.9.1",
|
|
51
|
-
"@tldraw/state": "3.
|
|
52
|
-
"@tldraw/state-react": "3.
|
|
53
|
-
"@tldraw/store": "3.
|
|
54
|
-
"@tldraw/tlschema": "3.
|
|
55
|
-
"@tldraw/utils": "3.
|
|
56
|
-
"@tldraw/validate": "3.
|
|
51
|
+
"@tldraw/state": "3.15.0-canary.039c346f1d6f",
|
|
52
|
+
"@tldraw/state-react": "3.15.0-canary.039c346f1d6f",
|
|
53
|
+
"@tldraw/store": "3.15.0-canary.039c346f1d6f",
|
|
54
|
+
"@tldraw/tlschema": "3.15.0-canary.039c346f1d6f",
|
|
55
|
+
"@tldraw/utils": "3.15.0-canary.039c346f1d6f",
|
|
56
|
+
"@tldraw/validate": "3.15.0-canary.039c346f1d6f",
|
|
57
57
|
"@types/core-js": "^2.5.8",
|
|
58
58
|
"@use-gesture/react": "^10.3.1",
|
|
59
59
|
"classnames": "^2.5.1",
|
package/src/index.ts
CHANGED
|
@@ -287,7 +287,13 @@ export {
|
|
|
287
287
|
type ContainerProviderProps,
|
|
288
288
|
} from './lib/hooks/useContainer'
|
|
289
289
|
export { getCursor } from './lib/hooks/useCursor'
|
|
290
|
-
export {
|
|
290
|
+
export {
|
|
291
|
+
EditorContext,
|
|
292
|
+
EditorProvider,
|
|
293
|
+
useEditor,
|
|
294
|
+
useMaybeEditor,
|
|
295
|
+
type EditorProviderProps,
|
|
296
|
+
} from './lib/hooks/useEditor'
|
|
291
297
|
export { useEditorComponents } from './lib/hooks/useEditorComponents'
|
|
292
298
|
export type { TLEditorComponents } from './lib/hooks/useEditorComponents'
|
|
293
299
|
export { useEvent, useReactiveEvent } from './lib/hooks/useEvent'
|
|
@@ -340,6 +346,7 @@ export { CubicBezier2d } from './lib/primitives/geometry/CubicBezier2d'
|
|
|
340
346
|
export { CubicSpline2d } from './lib/primitives/geometry/CubicSpline2d'
|
|
341
347
|
export { Edge2d } from './lib/primitives/geometry/Edge2d'
|
|
342
348
|
export { Ellipse2d } from './lib/primitives/geometry/Ellipse2d'
|
|
349
|
+
export { getVerticesCountForArcLength } from './lib/primitives/geometry/geometry-constants'
|
|
343
350
|
export {
|
|
344
351
|
Geometry2d,
|
|
345
352
|
Geometry2dFilters,
|
|
@@ -17,6 +17,7 @@ export interface TLUserPreferences {
|
|
|
17
17
|
// N.B. These are duplicated in TLdrawAppUser.
|
|
18
18
|
locale?: string | null
|
|
19
19
|
animationSpeed?: number | null
|
|
20
|
+
areKeyboardShortcutsEnabled?: boolean | null
|
|
20
21
|
edgeScrollSpeed?: number | null
|
|
21
22
|
colorScheme?: 'light' | 'dark' | 'system'
|
|
22
23
|
isSnapMode?: boolean | null
|
|
@@ -44,6 +45,7 @@ export const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUser
|
|
|
44
45
|
// N.B. These are duplicated in TLdrawAppUser.
|
|
45
46
|
locale: T.string.nullable().optional(),
|
|
46
47
|
animationSpeed: T.number.nullable().optional(),
|
|
48
|
+
areKeyboardShortcutsEnabled: T.boolean.nullable().optional(),
|
|
47
49
|
edgeScrollSpeed: T.number.nullable().optional(),
|
|
48
50
|
colorScheme: T.literalEnum('light', 'dark', 'system').optional(),
|
|
49
51
|
isSnapMode: T.boolean.nullable().optional(),
|
|
@@ -61,6 +63,7 @@ const Versions = {
|
|
|
61
63
|
AddDynamicSizeMode: 6,
|
|
62
64
|
AllowSystemColorScheme: 7,
|
|
63
65
|
AddPasteAtCursor: 8,
|
|
66
|
+
AddKeyboardShortcuts: 9,
|
|
64
67
|
} as const
|
|
65
68
|
|
|
66
69
|
const CURRENT_VERSION = Math.max(...Object.values(Versions))
|
|
@@ -96,6 +99,9 @@ function migrateSnapshot(data: { version: number; user: any }) {
|
|
|
96
99
|
if (data.version < Versions.AddPasteAtCursor) {
|
|
97
100
|
data.user.isPasteAtCursorMode = false
|
|
98
101
|
}
|
|
102
|
+
if (data.version < Versions.AddKeyboardShortcuts) {
|
|
103
|
+
data.user.areKeyboardShortcutsEnabled = true
|
|
104
|
+
}
|
|
99
105
|
|
|
100
106
|
// finally
|
|
101
107
|
data.version = CURRENT_VERSION
|
|
@@ -139,6 +145,7 @@ export const defaultUserPreferences = Object.freeze({
|
|
|
139
145
|
// N.B. These are duplicated in TLdrawAppUser.
|
|
140
146
|
edgeScrollSpeed: 1,
|
|
141
147
|
animationSpeed: userPrefersReducedMotion() ? 0 : 1,
|
|
148
|
+
areKeyboardShortcutsEnabled: true,
|
|
142
149
|
isSnapMode: false,
|
|
143
150
|
isWrapMode: false,
|
|
144
151
|
isDynamicSizeMode: false,
|
|
@@ -425,3 +425,410 @@ describe('getShapesAtPoint', () => {
|
|
|
425
425
|
expect(hollowShapesWithHitInside[0].id).toBe(ids.hollowShape)
|
|
426
426
|
})
|
|
427
427
|
})
|
|
428
|
+
|
|
429
|
+
describe('selectAll', () => {
|
|
430
|
+
const selectAllIds = {
|
|
431
|
+
pageShape1: createShapeId('pageShape1'),
|
|
432
|
+
pageShape2: createShapeId('pageShape2'),
|
|
433
|
+
pageShape3: createShapeId('pageShape3'),
|
|
434
|
+
container1: createShapeId('container1'),
|
|
435
|
+
containerChild1: createShapeId('containerChild1'),
|
|
436
|
+
containerChild2: createShapeId('containerChild2'),
|
|
437
|
+
containerChild3: createShapeId('containerChild3'),
|
|
438
|
+
containerGrandchild1: createShapeId('containerGrandchild1'),
|
|
439
|
+
container2: createShapeId('container2'),
|
|
440
|
+
container2Child1: createShapeId('container2Child1'),
|
|
441
|
+
container2Child2: createShapeId('container2Child2'),
|
|
442
|
+
container2Grandchild1: createShapeId('container2Grandchild1'),
|
|
443
|
+
lockedShape: createShapeId('lockedShape'),
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
beforeEach(() => {
|
|
447
|
+
// Clear any existing shapes
|
|
448
|
+
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
|
|
449
|
+
|
|
450
|
+
// Create shapes directly on the page (no parentId means they're children of the page)
|
|
451
|
+
editor.createShapes([
|
|
452
|
+
{
|
|
453
|
+
id: selectAllIds.pageShape1,
|
|
454
|
+
type: 'my-custom-shape',
|
|
455
|
+
x: 100,
|
|
456
|
+
y: 100,
|
|
457
|
+
props: { w: 100, h: 100 },
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: selectAllIds.pageShape2,
|
|
461
|
+
type: 'my-custom-shape',
|
|
462
|
+
x: 300,
|
|
463
|
+
y: 100,
|
|
464
|
+
props: { w: 100, h: 100 },
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: selectAllIds.pageShape3,
|
|
468
|
+
type: 'my-custom-shape',
|
|
469
|
+
x: 500,
|
|
470
|
+
y: 100,
|
|
471
|
+
props: { w: 100, h: 100 },
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
id: selectAllIds.lockedShape,
|
|
475
|
+
type: 'my-custom-shape',
|
|
476
|
+
x: 700,
|
|
477
|
+
y: 100,
|
|
478
|
+
props: { w: 100, h: 100 },
|
|
479
|
+
isLocked: true,
|
|
480
|
+
},
|
|
481
|
+
])
|
|
482
|
+
|
|
483
|
+
// Create a container shape (simulating a frame or group)
|
|
484
|
+
editor.createShape({
|
|
485
|
+
id: selectAllIds.container1,
|
|
486
|
+
type: 'my-custom-shape',
|
|
487
|
+
x: 100,
|
|
488
|
+
y: 300,
|
|
489
|
+
props: { w: 400, h: 200 },
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
// Create children inside the container (parentId set to container1)
|
|
493
|
+
editor.createShapes([
|
|
494
|
+
{
|
|
495
|
+
id: selectAllIds.containerChild1,
|
|
496
|
+
type: 'my-custom-shape',
|
|
497
|
+
parentId: selectAllIds.container1,
|
|
498
|
+
x: 120,
|
|
499
|
+
y: 320,
|
|
500
|
+
props: { w: 50, h: 50 },
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
id: selectAllIds.containerChild2,
|
|
504
|
+
type: 'my-custom-shape',
|
|
505
|
+
parentId: selectAllIds.container1,
|
|
506
|
+
x: 200,
|
|
507
|
+
y: 320,
|
|
508
|
+
props: { w: 50, h: 50 },
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
id: selectAllIds.containerChild3,
|
|
512
|
+
type: 'my-custom-shape',
|
|
513
|
+
parentId: selectAllIds.container1,
|
|
514
|
+
x: 280,
|
|
515
|
+
y: 320,
|
|
516
|
+
props: { w: 50, h: 50 },
|
|
517
|
+
},
|
|
518
|
+
])
|
|
519
|
+
|
|
520
|
+
// Create a grandchild inside one of the container children
|
|
521
|
+
editor.createShape({
|
|
522
|
+
id: selectAllIds.containerGrandchild1,
|
|
523
|
+
type: 'my-custom-shape',
|
|
524
|
+
parentId: selectAllIds.containerChild3,
|
|
525
|
+
x: 290,
|
|
526
|
+
y: 330,
|
|
527
|
+
props: { w: 30, h: 30 },
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
// Create a second container (simulating a group)
|
|
531
|
+
editor.createShape({
|
|
532
|
+
id: selectAllIds.container2,
|
|
533
|
+
type: 'my-custom-shape',
|
|
534
|
+
x: 600,
|
|
535
|
+
y: 300,
|
|
536
|
+
props: { w: 200, h: 200 },
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
// Create children inside the second container
|
|
540
|
+
editor.createShapes([
|
|
541
|
+
{
|
|
542
|
+
id: selectAllIds.container2Child1,
|
|
543
|
+
type: 'my-custom-shape',
|
|
544
|
+
parentId: selectAllIds.container2,
|
|
545
|
+
x: 620,
|
|
546
|
+
y: 320,
|
|
547
|
+
props: { w: 50, h: 50 },
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
id: selectAllIds.container2Child2,
|
|
551
|
+
type: 'my-custom-shape',
|
|
552
|
+
parentId: selectAllIds.container2,
|
|
553
|
+
x: 680,
|
|
554
|
+
y: 320,
|
|
555
|
+
props: { w: 50, h: 50 },
|
|
556
|
+
},
|
|
557
|
+
])
|
|
558
|
+
|
|
559
|
+
// Create a grandchild in the second container
|
|
560
|
+
editor.createShape({
|
|
561
|
+
id: selectAllIds.container2Grandchild1,
|
|
562
|
+
type: 'my-custom-shape',
|
|
563
|
+
parentId: selectAllIds.container2Child1,
|
|
564
|
+
x: 630,
|
|
565
|
+
y: 330,
|
|
566
|
+
props: { w: 30, h: 30 },
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
// Clear selection
|
|
570
|
+
editor.selectNone()
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it('when no shapes are selected, selects all page-level shapes (excluding locked ones)', () => {
|
|
574
|
+
// Initially no shapes selected
|
|
575
|
+
expect(editor.getSelectedShapeIds()).toEqual([])
|
|
576
|
+
|
|
577
|
+
// Call selectAll
|
|
578
|
+
editor.selectAll()
|
|
579
|
+
|
|
580
|
+
// Should select all page-level shapes (excluding locked ones)
|
|
581
|
+
const selectedIds = editor.getSelectedShapeIds()
|
|
582
|
+
expect(Array.from(selectedIds).sort()).toEqual(
|
|
583
|
+
[
|
|
584
|
+
selectAllIds.pageShape1,
|
|
585
|
+
selectAllIds.pageShape2,
|
|
586
|
+
selectAllIds.pageShape3,
|
|
587
|
+
selectAllIds.container1,
|
|
588
|
+
selectAllIds.container2,
|
|
589
|
+
].sort()
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
// Should NOT include locked shape or children/grandchildren
|
|
593
|
+
expect(selectedIds).not.toContain(selectAllIds.lockedShape)
|
|
594
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild1)
|
|
595
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild2)
|
|
596
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild3)
|
|
597
|
+
expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
|
|
598
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Child1)
|
|
599
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Child2)
|
|
600
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
it('when shapes are selected only on the page, all children of the page should be selected (but not their descendants)', () => {
|
|
604
|
+
// Select some page-level shapes
|
|
605
|
+
editor.select(selectAllIds.pageShape1, selectAllIds.pageShape2)
|
|
606
|
+
|
|
607
|
+
// Call selectAll
|
|
608
|
+
editor.selectAll()
|
|
609
|
+
|
|
610
|
+
// Should select all page-level shapes (excluding locked ones), but not descendants
|
|
611
|
+
const selectedIds = editor.getSelectedShapeIds()
|
|
612
|
+
expect(Array.from(selectedIds).sort()).toEqual(
|
|
613
|
+
[
|
|
614
|
+
selectAllIds.pageShape1,
|
|
615
|
+
selectAllIds.pageShape2,
|
|
616
|
+
selectAllIds.pageShape3,
|
|
617
|
+
selectAllIds.container1,
|
|
618
|
+
selectAllIds.container2,
|
|
619
|
+
].sort()
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
// Should NOT include children or grandchildren or locked shapes
|
|
623
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild1)
|
|
624
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild2)
|
|
625
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild3)
|
|
626
|
+
expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
|
|
627
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Child1)
|
|
628
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Child2)
|
|
629
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
|
|
630
|
+
expect(selectedIds).not.toContain(selectAllIds.lockedShape)
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it('when shapes are selected within a container, only children of the container should be selected (not their descendants)', () => {
|
|
634
|
+
// Select some container children
|
|
635
|
+
editor.select(selectAllIds.containerChild1, selectAllIds.containerChild2)
|
|
636
|
+
|
|
637
|
+
// Call selectAll
|
|
638
|
+
editor.selectAll()
|
|
639
|
+
|
|
640
|
+
// Should select all container children (but not their descendants)
|
|
641
|
+
const selectedIds = editor.getSelectedShapeIds()
|
|
642
|
+
expect(Array.from(selectedIds).sort()).toEqual(
|
|
643
|
+
[
|
|
644
|
+
selectAllIds.containerChild1,
|
|
645
|
+
selectAllIds.containerChild2,
|
|
646
|
+
selectAllIds.containerChild3,
|
|
647
|
+
].sort()
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
// Should NOT include page-level shapes or grandchildren
|
|
651
|
+
expect(selectedIds).not.toContain(selectAllIds.pageShape1)
|
|
652
|
+
expect(selectedIds).not.toContain(selectAllIds.pageShape2)
|
|
653
|
+
expect(selectedIds).not.toContain(selectAllIds.pageShape3)
|
|
654
|
+
expect(selectedIds).not.toContain(selectAllIds.container1)
|
|
655
|
+
expect(selectedIds).not.toContain(selectAllIds.container2)
|
|
656
|
+
expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
|
|
657
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Child1)
|
|
658
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Child2)
|
|
659
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('when shapes are selected within a second container, only children of that container should be selected', () => {
|
|
663
|
+
// Select some second container children
|
|
664
|
+
editor.select(selectAllIds.container2Child1)
|
|
665
|
+
|
|
666
|
+
// Call selectAll
|
|
667
|
+
editor.selectAll()
|
|
668
|
+
|
|
669
|
+
// Should select all second container children (but not their descendants)
|
|
670
|
+
const selectedIds = editor.getSelectedShapeIds()
|
|
671
|
+
expect(Array.from(selectedIds).sort()).toEqual(
|
|
672
|
+
[selectAllIds.container2Child1, selectAllIds.container2Child2].sort()
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
// Should NOT include page-level shapes or other container's children or grandchildren
|
|
676
|
+
expect(selectedIds).not.toContain(selectAllIds.pageShape1)
|
|
677
|
+
expect(selectedIds).not.toContain(selectAllIds.pageShape2)
|
|
678
|
+
expect(selectedIds).not.toContain(selectAllIds.pageShape3)
|
|
679
|
+
expect(selectedIds).not.toContain(selectAllIds.container1)
|
|
680
|
+
expect(selectedIds).not.toContain(selectAllIds.container2)
|
|
681
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild1)
|
|
682
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild2)
|
|
683
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild3)
|
|
684
|
+
expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
|
|
685
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
it('when shapes are selected that belong to different parents, no change/history entry should be made', () => {
|
|
689
|
+
// Select shapes from different parents (page and container)
|
|
690
|
+
editor.select(selectAllIds.pageShape1, selectAllIds.containerChild1)
|
|
691
|
+
|
|
692
|
+
const initialSelectedIds = editor.getSelectedShapeIds()
|
|
693
|
+
|
|
694
|
+
// Spy on setSelectedShapes to verify it's not called
|
|
695
|
+
const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
|
|
696
|
+
|
|
697
|
+
// Call selectAll
|
|
698
|
+
editor.selectAll()
|
|
699
|
+
|
|
700
|
+
// Selection should remain unchanged
|
|
701
|
+
expect(editor.getSelectedShapeIds()).toEqual(initialSelectedIds)
|
|
702
|
+
|
|
703
|
+
// setSelectedShapes should not have been called (the method returns early)
|
|
704
|
+
expect(setSelectedShapesSpy).not.toHaveBeenCalled()
|
|
705
|
+
|
|
706
|
+
setSelectedShapesSpy.mockRestore()
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('when shapes are selected that belong to different containers, no change/history entry should be made', () => {
|
|
710
|
+
// Select shapes from different containers
|
|
711
|
+
editor.select(selectAllIds.containerChild1, selectAllIds.container2Child1)
|
|
712
|
+
|
|
713
|
+
const initialSelectedIds = editor.getSelectedShapeIds()
|
|
714
|
+
|
|
715
|
+
// Spy on setSelectedShapes to verify it's not called
|
|
716
|
+
const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
|
|
717
|
+
|
|
718
|
+
// Call selectAll
|
|
719
|
+
editor.selectAll()
|
|
720
|
+
|
|
721
|
+
// Selection should remain unchanged
|
|
722
|
+
expect(editor.getSelectedShapeIds()).toEqual(initialSelectedIds)
|
|
723
|
+
|
|
724
|
+
// setSelectedShapes should not have been called
|
|
725
|
+
expect(setSelectedShapesSpy).not.toHaveBeenCalled()
|
|
726
|
+
|
|
727
|
+
setSelectedShapesSpy.mockRestore()
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
it('should not select locked shapes', () => {
|
|
731
|
+
// Select a page-level shape
|
|
732
|
+
editor.select(selectAllIds.pageShape1)
|
|
733
|
+
|
|
734
|
+
// Call selectAll
|
|
735
|
+
editor.selectAll()
|
|
736
|
+
|
|
737
|
+
// Should select all page-level shapes except locked ones
|
|
738
|
+
const selectedIds = editor.getSelectedShapeIds()
|
|
739
|
+
expect(selectedIds).not.toContain(selectAllIds.lockedShape)
|
|
740
|
+
expect(selectedIds).toContain(selectAllIds.pageShape1)
|
|
741
|
+
expect(selectedIds).toContain(selectAllIds.pageShape2)
|
|
742
|
+
expect(selectedIds).toContain(selectAllIds.pageShape3)
|
|
743
|
+
expect(selectedIds).toContain(selectAllIds.container1)
|
|
744
|
+
expect(selectedIds).toContain(selectAllIds.container2)
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
it('should handle empty container by selecting all siblings at the same level', () => {
|
|
748
|
+
// Create an empty container
|
|
749
|
+
const emptyContainerId = createShapeId('emptyContainer')
|
|
750
|
+
editor.createShape({
|
|
751
|
+
id: emptyContainerId,
|
|
752
|
+
type: 'my-custom-shape',
|
|
753
|
+
x: 800,
|
|
754
|
+
y: 400,
|
|
755
|
+
props: { w: 100, h: 100 },
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
// Clear selection first
|
|
759
|
+
editor.selectNone()
|
|
760
|
+
|
|
761
|
+
// Select the empty container
|
|
762
|
+
editor.select(emptyContainerId)
|
|
763
|
+
|
|
764
|
+
// Call selectAll - since the empty container has no children, it should select all siblings (page-level shapes)
|
|
765
|
+
editor.selectAll()
|
|
766
|
+
|
|
767
|
+
// Should select all page-level shapes (including the empty container itself)
|
|
768
|
+
const selectedIds = editor.getSelectedShapeIds()
|
|
769
|
+
expect(Array.from(selectedIds).sort()).toEqual(
|
|
770
|
+
[
|
|
771
|
+
selectAllIds.pageShape1,
|
|
772
|
+
selectAllIds.pageShape2,
|
|
773
|
+
selectAllIds.pageShape3,
|
|
774
|
+
selectAllIds.container1,
|
|
775
|
+
selectAllIds.container2,
|
|
776
|
+
emptyContainerId,
|
|
777
|
+
].sort()
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
// Should NOT include locked shapes or children/grandchildren
|
|
781
|
+
expect(selectedIds).not.toContain(selectAllIds.lockedShape)
|
|
782
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild1)
|
|
783
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild2)
|
|
784
|
+
expect(selectedIds).not.toContain(selectAllIds.containerChild3)
|
|
785
|
+
expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
|
|
786
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Child1)
|
|
787
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Child2)
|
|
788
|
+
expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
it('should work correctly when selecting all shapes of same parent type', () => {
|
|
792
|
+
// Select all container children
|
|
793
|
+
editor.select(
|
|
794
|
+
selectAllIds.containerChild1,
|
|
795
|
+
selectAllIds.containerChild2,
|
|
796
|
+
selectAllIds.containerChild3
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
// Call selectAll - should maintain the same selection since all children are already selected
|
|
800
|
+
editor.selectAll()
|
|
801
|
+
|
|
802
|
+
// Should still have all container children selected
|
|
803
|
+
const selectedIds = editor.getSelectedShapeIds()
|
|
804
|
+
expect(Array.from(selectedIds).sort()).toEqual(
|
|
805
|
+
[
|
|
806
|
+
selectAllIds.containerChild1,
|
|
807
|
+
selectAllIds.containerChild2,
|
|
808
|
+
selectAllIds.containerChild3,
|
|
809
|
+
].sort()
|
|
810
|
+
)
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
it('should handle mixed selection levels gracefully by doing nothing', () => {
|
|
814
|
+
// Select a mix: page shape (parent=page), container (parent=page), and container child (parent=container1)
|
|
815
|
+
// These all have different parent IDs so selectAll should do nothing
|
|
816
|
+
editor.select(selectAllIds.pageShape1, selectAllIds.containerChild1)
|
|
817
|
+
|
|
818
|
+
const initialSelectedIds = Array.from(editor.getSelectedShapeIds())
|
|
819
|
+
|
|
820
|
+
// Spy on setSelectedShapes to verify it's not called
|
|
821
|
+
const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
|
|
822
|
+
|
|
823
|
+
// Call selectAll
|
|
824
|
+
editor.selectAll()
|
|
825
|
+
|
|
826
|
+
// Selection should remain unchanged since shapes have different parents
|
|
827
|
+
expect(Array.from(editor.getSelectedShapeIds())).toEqual(initialSelectedIds)
|
|
828
|
+
|
|
829
|
+
// setSelectedShapes should not have been called
|
|
830
|
+
expect(setSelectedShapesSpy).not.toHaveBeenCalled()
|
|
831
|
+
|
|
832
|
+
setSelectedShapesSpy.mockRestore()
|
|
833
|
+
})
|
|
834
|
+
})
|