[refactor] restore createTLSchema (#1444)

This PR restores `createTLSchema`. 

It also:
- removes `TldrawEditorConfig.default`
- makes `config` a required property of `<TldrawEditor>`, though it's
created automatically in `<Tldraw>`.
- makes `config` a required property of `App`
- removes `TLShapeType` and replaces the rare usage with
`TLShape["type"]`
- adds `TLDefaultShape` for a union of our default shapes
- makes `TLShape` a union of `TLDefaultShape` and `TLUnknownShape`

### Change Type

- [x] `major` — Breaking Change

### Release Notes

- [editor] Simplifies custom shape definition
- [tldraw] Updates props for <TldrawEditor> component to require a
`TldrawEditorConfig`.
This commit is contained in:
Steve Ruiz 2023-05-24 11:48:31 +01:00 committed by GitHub
parent f3182c9874
commit eb26964130
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 377 additions and 285 deletions

View file

@ -1,13 +1,15 @@
import { Canvas, TldrawEditor, useApp } from '@tldraw/tldraw'
import { Canvas, TldrawEditor, TldrawEditorConfig, useApp } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import { useEffect } from 'react'
import { track } from 'signia-react'
import './custom-ui.css'
const config = new TldrawEditorConfig()
export default function Example() {
return (
<div className="tldraw__editor">
<TldrawEditor autoFocus>
<TldrawEditor config={config} autoFocus>
<Canvas />
<CustomUi />
</TldrawEditor>

View file

@ -3,6 +3,7 @@ import {
ContextMenu,
getUserData,
TldrawEditor,
TldrawEditorConfig,
TldrawUi,
TLInstance,
useLocalSyncClient,
@ -12,10 +13,13 @@ import '@tldraw/tldraw/ui.css'
const instanceId = TLInstance.createCustomId('example')
const config = new TldrawEditorConfig()
export default function Example() {
const userData = getUserData()
const syncedStore = useLocalSyncClient({
config,
instanceId,
userId: userData.id,
universalPersistenceKey: 'exploded-example',
@ -24,7 +28,13 @@ export default function Example() {
return (
<div className="tldraw__editor">
<TldrawEditor instanceId={instanceId} userId={userData.id} store={syncedStore} autoFocus>
<TldrawEditor
instanceId={instanceId}
userId={userData.id}
store={syncedStore}
config={config}
autoFocus
>
<TldrawUi>
<ContextMenu>
<Canvas />

View file

@ -4,6 +4,7 @@ import {
ErrorBoundary,
setRuntimeOverrides,
TldrawEditor,
TldrawEditorConfig,
TLUserId,
} from '@tldraw/editor'
import { linksUiOverrides } from './utils/links'
@ -24,6 +25,8 @@ import { FullPageMessage } from './FullPageMessage'
import { onCreateBookmarkFromUrl } from './utils/bookmarks'
import { vscode } from './utils/vscode'
const config = new TldrawEditorConfig()
// @ts-ignore
setRuntimeOverrides({
@ -96,6 +99,7 @@ export const TldrawWrapper = () => {
uri: message.data.uri,
userId: message.data.userId as TLUserId,
isDarkMode: message.data.isDarkMode,
config,
})
// We only want to listen for this message once
window.removeEventListener('message', handleMessage)
@ -126,20 +130,30 @@ export type TLDrawInnerProps = {
uri: string
userId: TLUserId
isDarkMode: boolean
config: TldrawEditorConfig
}
function TldrawInner({ uri, assetSrc, userId, isDarkMode, fileContents }: TLDrawInnerProps) {
function TldrawInner({
uri,
config,
assetSrc,
userId,
isDarkMode,
fileContents,
}: TLDrawInnerProps) {
const instanceId = TAB_ID
const syncedStore = useLocalSyncClient({
universalPersistenceKey: uri,
instanceId,
userId,
config,
})
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
return (
<TldrawEditor
config={config}
assetUrls={assetUrls}
instanceId={TAB_ID}
userId={userId}

View file

@ -4,13 +4,13 @@ import * as vscode from 'vscode'
export const defaultFileContents: TldrawFile = {
tldrawFileFormatVersion: 1,
schema: TldrawEditorConfig.default.storeSchema.serialize(),
schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [],
}
export const fileContentWithErrors: TldrawFile = {
tldrawFileFormatVersion: 1,
schema: TldrawEditorConfig.default.storeSchema.serialize(),
schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [{ typeName: 'shape', id: null } as any],
}

View file

@ -12,11 +12,16 @@ export async function pointWithinActiveArea(x: number, y: number) {
}
export async function waitForReady() {
await browser.waitUntil(() => {
return browser.execute(() => {
return window.tldrawReady
})
})
await Promise.any([
new Promise<boolean>((r) => {
browser.waitUntil(() => browser.execute(() => window.tldrawReady)).then(() => r(true))
}),
new Promise<boolean>((r) => {
// eslint-disable-next-line no-console
console.log('waitFor failed, using timeout')
setTimeout(() => r(true), 2000)
}),
])
// Make sure the window is focused... maybe
await ui.canvas.click(100, 100)

View file

@ -44,7 +44,6 @@ import { sortByIndex } from '@tldraw/indices';
import { StoreSchema } from '@tldraw/tlstore';
import { StoreSnapshot } from '@tldraw/tlstore';
import { StrokePoint } from '@tldraw/primitives';
import { T } from '@tldraw/tlvalidate';
import { TLAlignType } from '@tldraw/tlschema';
import { TLArrowheadType } from '@tldraw/tlschema';
import { TLArrowShape } from '@tldraw/tlschema';
@ -87,7 +86,6 @@ import { TLShapeId } from '@tldraw/tlschema';
import { TLShapePartial } from '@tldraw/tlschema';
import { TLShapeProp } from '@tldraw/tlschema';
import { TLShapeProps } from '@tldraw/tlschema';
import { TLShapeType } from '@tldraw/tlschema';
import { TLSizeStyle } from '@tldraw/tlschema';
import { TLSizeType } from '@tldraw/tlschema';
import { TLStore } from '@tldraw/tlschema';
@ -274,7 +272,7 @@ export class App extends EventEmitter<TLEventMap> {
getPageTransform(shape: TLShape): Matrix2d | undefined;
getPageTransformById(id: TLShapeId): Matrix2d | undefined;
// (undocumented)
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShapeType): TLPageId | TLShapeId;
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']): TLPageId | TLShapeId;
getParentPageId(shape?: TLShape): TLPageId | undefined;
getParentShape(shape?: TLShape): TLShape | undefined;
getParentsMappedToChildren(ids: TLShapeId[]): Map<TLParentId, Set<TLShape>>;
@ -556,7 +554,7 @@ export function applyRotationToSnapshotShapes({ delta, app, snapshot, stage, }:
// @public (undocumented)
export interface AppOptions {
config?: TldrawEditorConfig;
config: TldrawEditorConfig;
getContainer: () => HTMLElement;
store: TLStore;
}
@ -1793,7 +1791,7 @@ export function TldrawEditor(props: TldrawEditorProps): JSX.Element;
// @public (undocumented)
export class TldrawEditorConfig {
constructor(opts: TldrawEditorConfigOptions);
constructor(opts?: TldrawEditorConfigOptions);
// (undocumented)
createStore(config: {
initialData?: StoreSnapshot<TLRecord>;
@ -1801,13 +1799,7 @@ export class TldrawEditorConfig {
instanceId: TLInstanceId;
}): TLStore;
// (undocumented)
static readonly default: TldrawEditorConfig;
// (undocumented)
readonly shapeMigrations: MigrationsForShapes<TLShape>;
// (undocumented)
readonly shapeUtils: UtilsForShapes<TLShape>;
// (undocumented)
readonly shapeValidators: Record<TLShape['type'], T.Validator<any>>;
readonly shapeUtils: Record<TLShape['type'], TLShapeUtilConstructor<any>>;
// (undocumented)
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>;
// (undocumented)
@ -1823,7 +1815,7 @@ export interface TldrawEditorProps {
// (undocumented)
children?: any;
components?: Partial<TLEditorComponents>;
config?: TldrawEditorConfig;
config: TldrawEditorConfig;
instanceId?: TLInstanceId;
isDarkMode?: boolean;
onCreateAssetFromFile?: (file: File) => Promise<TLAsset>;
@ -2044,7 +2036,7 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
// (undocumented)
canEdit: () => boolean;
// (undocumented)
canReceiveNewChildrenOfType: (_type: TLShapeType) => boolean;
canReceiveNewChildrenOfType: (_type: TLShape['type']) => boolean;
// (undocumented)
defaultProps(): TLFrameShape['props'];
// (undocumented)
@ -2456,7 +2448,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
canCrop: TLShapeUtilFlag<T>;
canDropShapes(shape: T, shapes: TLShape[]): boolean;
canEdit: TLShapeUtilFlag<T>;
canReceiveNewChildrenOfType(type: TLShapeType): boolean;
canReceiveNewChildrenOfType(type: TLShape['type']): boolean;
canResize: TLShapeUtilFlag<T>;
canScroll: TLShapeUtilFlag<T>;
canUnmount: TLShapeUtilFlag<T>;

View file

@ -1,7 +1,7 @@
import { TLAsset, TLInstance, TLInstanceId, TLStore, TLUser, TLUserId } from '@tldraw/tlschema'
import { Store } from '@tldraw/tlstore'
import { annotateError } from '@tldraw/utils'
import React, { useCallback, useSyncExternalStore } from 'react'
import React, { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
import { App } from './app/App'
import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls'
import { OptionalErrorBoundary } from './components/ErrorBoundary'
@ -28,12 +28,12 @@ import { useZoomCss } from './hooks/useZoomCss'
/** @public */
export interface TldrawEditorProps {
children?: any
/** A configuration defining major customizations to the app, such as custom shapes and new tools */
config: TldrawEditorConfig
/** Overrides for the tldraw components */
components?: Partial<TLEditorComponents>
/** Whether to display the dark mode. */
isDarkMode?: boolean
/** A configuration defining major customizations to the app, such as custom shapes and new tools */
config?: TldrawEditorConfig
/**
* Called when the app has mounted.
*
@ -133,7 +133,7 @@ export function TldrawEditor(props: TldrawEditorProps) {
}
function TldrawEditorBeforeLoading({
config = TldrawEditorConfig.default,
config,
userId,
instanceId,
store,
@ -143,26 +143,43 @@ function TldrawEditorBeforeLoading({
props.assetUrls ?? defaultEditorAssetUrls
)
store ??= config.createStore({
const [_store, _setStore] = useState<TLStore | SyncedStore>(() => {
return (
store ??
config.createStore({
userId: userId ?? TLUser.createId(),
instanceId: instanceId ?? TLInstance.createId(),
})
)
})
let loadedStore
if (!(store instanceof Store)) {
if (store.error) {
useEffect(() => {
_setStore(() => {
return (
store ??
config.createStore({
userId: userId ?? TLUser.createId(),
instanceId: instanceId ?? TLInstance.createId(),
})
)
})
}, [store, config, userId, instanceId])
let loadedStore: TLStore | SyncedStore
if (!(_store instanceof Store)) {
if (_store.error) {
// for error handling, we fall back to the default error boundary.
// if users want to handle this error differently, they can render
// their own error screen before the TldrawEditor component
throw store.error
throw _store.error
}
if (!store.store) {
if (!_store.store) {
return <LoadingScreen>Connecting...</LoadingScreen>
}
loadedStore = store.store
loadedStore = _store.store
} else {
loadedStore = store
loadedStore = _store
}
if (instanceId && loadedStore.props.instanceId !== instanceId) {
@ -209,8 +226,8 @@ function TldrawEditorAfterLoading({
React.useLayoutEffect(() => {
const app = new App({
store,
getContainer: () => container,
config,
getContainer: () => container,
})
setApp(app)

View file

@ -49,7 +49,6 @@ import {
TLShapeId,
TLShapePartial,
TLShapeProp,
TLShapeType,
TLSizeStyle,
TLStore,
TLUnknownShape,
@ -160,7 +159,7 @@ export interface AppOptions {
*/
store: TLStore
/** A configuration defining major customizations to the app, such as custom shapes and new tools */
config?: TldrawEditorConfig
config: TldrawEditorConfig
/**
* Should return a containing html element which has all the styles applied to the app. If not
* given, the body element will be used.
@ -175,14 +174,15 @@ export function isShapeWithHandles(shape: TLShape) {
/** @public */
export class App extends EventEmitter<TLEventMap> {
constructor({ config = TldrawEditorConfig.default, store, getContainer }: AppOptions) {
constructor({ config, store, getContainer }: AppOptions) {
super()
if (store.schema !== config.storeSchema) {
this.config = config
if (store.schema !== this.config.storeSchema) {
throw new Error('Store schema does not match schema given to App')
}
this.config = config
this.store = store
this.getContainer = getContainer ?? (() => document.body)
@ -191,7 +191,7 @@ export class App extends EventEmitter<TLEventMap> {
// Set the shape utils
this.shapeUtils = Object.fromEntries(
Object.entries(config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)])
Object.entries(this.config.shapeUtils).map(([type, Util]) => [type, new Util(this, type)])
)
if (typeof window !== 'undefined' && 'navigator' in window) {
@ -209,7 +209,7 @@ export class App extends EventEmitter<TLEventMap> {
this.root = new RootState(this)
if (this.root.children) {
config.tools.forEach((Ctor) => {
this.config.tools.forEach((Ctor) => {
this.root.children![Ctor.id] = new Ctor(this)
})
}
@ -2864,7 +2864,7 @@ export class App extends EventEmitter<TLEventMap> {
return this
}
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShapeType) {
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']) {
const shapes = this.sortedShapesArray
for (let i = shapes.length - 1; i >= 0; i--) {

View file

@ -1,5 +1,5 @@
import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
import { TLFrameShape, TLShape, TLShapeId, TLShapeType } from '@tldraw/tlschema'
import { TLFrameShape, TLShape, TLShapeId } from '@tldraw/tlschema'
import { last } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer'
import { defaultEmptyAs } from '../../../utils/string'
@ -148,7 +148,7 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
)
}
override canReceiveNewChildrenOfType = (_type: TLShapeType) => {
override canReceiveNewChildrenOfType = (_type: TLShape['type']) => {
return true
}

View file

@ -5,7 +5,6 @@ import {
TLHandle,
TLShape,
TLShapePartial,
TLShapeType,
TLUnknownShape,
Vec2dModel,
} from '@tldraw/tlschema'
@ -308,7 +307,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param type - The shape type.
* @public
*/
canReceiveNewChildrenOfType(type: TLShapeType) {
canReceiveNewChildrenOfType(type: TLShape['type']) {
return false
}

View file

@ -1,4 +1,4 @@
import { TLShapeType, TLStyleType } from '@tldraw/tlschema'
import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../StateNode'
import { Idle } from './children/Idle'
import { Pointing } from './children/Pointing'
@ -8,7 +8,7 @@ export class TLArrowTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Pointing]
shapeType: TLShapeType = 'arrow'
shapeType = 'arrow'
styles = [
'color',

View file

@ -1,4 +1,4 @@
import { createShapeId, TLArrowShape, TLShapeType } from '@tldraw/tlschema'
import { createShapeId, TLArrowShape } from '@tldraw/tlschema'
import { TLArrowUtil } from '../../../shapeutils/TLArrowUtil/TLArrowUtil'
import { TLEventHandlers } from '../../../types/event-types'
import { StateNode } from '../../StateNode'
@ -7,8 +7,6 @@ import { TLArrowTool } from '../TLArrowTool'
export class Pointing extends StateNode {
static override id = 'pointing'
shapeType = '' as TLShapeType
shape?: TLArrowShape
preciseTimeout = -1
@ -33,7 +31,7 @@ export class Pointing extends StateNode {
this.didTimeout = false
this.shapeType = (this.parent as TLArrowTool).shapeType
const shapeType = (this.parent as TLArrowTool).shapeType
this.app.mark('creating')
@ -42,7 +40,7 @@ export class Pointing extends StateNode {
this.app.createShapes([
{
id,
type: this.shapeType,
type: shapeType,
x: currentPagePoint.x,
y: currentPagePoint.y,
},

View file

@ -1,4 +1,4 @@
import { TLShapeType, TLStyleType } from '@tldraw/tlschema'
import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../StateNode'
import { Idle } from './children/Idle'
@ -9,7 +9,7 @@ export class TLLineTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Pointing]
shapeType: TLShapeType = 'line'
shapeType = 'line'
styles = ['color', 'opacity', 'dash', 'size', 'spline'] as TLStyleType[]
}

View file

@ -1,6 +1,6 @@
import { getIndexAbove, sortByIndex } from '@tldraw/indices'
import { Matrix2d, Vec2d } from '@tldraw/primitives'
import { TLHandle, TLLineShape, TLShapeId, TLShapeType, createShapeId } from '@tldraw/tlschema'
import { TLHandle, TLLineShape, TLShapeId, createShapeId } from '@tldraw/tlschema'
import { last, structuredClone } from '@tldraw/utils'
import { TLEventHandlers, TLInterruptEvent } from '../../../types/event-types'
import { StateNode } from '../../StateNode'
@ -9,8 +9,6 @@ import { TLLineTool } from '../TLLineTool'
export class Pointing extends StateNode {
static override id = 'pointing'
shapeType = '' as TLShapeType
shape = {} as TLLineShape
markPointId = ''
@ -19,7 +17,6 @@ export class Pointing extends StateNode {
const { inputs } = this.app
const { currentPagePoint } = inputs
this.shapeType = (this.parent as TLLineTool).shapeType
this.markPointId = this.app.mark('creating')
let shapeExists = false
@ -85,7 +82,7 @@ export class Pointing extends StateNode {
this.app.createShapes([
{
id,
type: this.shapeType,
type: (this.parent as TLLineTool).shapeType,
x: currentPagePoint.x,
y: currentPagePoint.y,
},

View file

@ -1,63 +1,19 @@
import {
CLIENT_FIXUP_SCRIPT,
TLAsset,
TLCamera,
TLDOCUMENT_ID,
TLDocument,
TLDefaultShape,
TLInstance,
TLInstanceId,
TLInstancePageState,
TLInstancePresence,
TLPage,
TLRecord,
TLShape,
TLStore,
TLStoreProps,
TLUnknownShape,
TLUser,
TLUserDocument,
TLUserId,
TLUserPresence,
arrowShapeTypeMigrations,
arrowShapeTypeValidator,
bookmarkShapeTypeMigrations,
bookmarkShapeTypeValidator,
createIntegrityChecker,
defaultDerivePresenceState,
drawShapeTypeMigrations,
drawShapeTypeValidator,
embedShapeTypeMigrations,
embedShapeTypeValidator,
frameShapeTypeMigrations,
frameShapeTypeValidator,
geoShapeTypeMigrations,
geoShapeTypeValidator,
groupShapeTypeMigrations,
groupShapeTypeValidator,
imageShapeTypeMigrations,
imageShapeTypeValidator,
lineShapeTypeMigrations,
lineShapeTypeValidator,
noteShapeTypeMigrations,
noteShapeTypeValidator,
onValidationFailure,
rootShapeTypeMigrations,
storeMigrations,
textShapeTypeMigrations,
textShapeTypeValidator,
videoShapeTypeMigrations,
videoShapeTypeValidator,
createTLSchema,
} from '@tldraw/tlschema'
import {
Migrations,
RecordType,
Store,
StoreSchema,
StoreSnapshot,
createRecordType,
defineMigrations,
} from '@tldraw/tlstore'
import { T } from '@tldraw/tlvalidate'
import { Migrations, RecordType, Store, StoreSchema, StoreSnapshot } from '@tldraw/tlstore'
import { Signal } from 'signia'
import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { TLBookmarkUtil } from '../app/shapeutils/TLBookmarkUtil/TLBookmarkUtil'
@ -74,51 +30,12 @@ import { TLTextUtil } from '../app/shapeutils/TLTextUtil/TLTextUtil'
import { TLVideoUtil } from '../app/shapeutils/TLVideoUtil/TLVideoUtil'
import { StateNodeConstructor } from '../app/statechart/StateNode'
/** @public */
export type ValidatorsForShapes<T extends TLUnknownShape> = Record<
T['type'],
{ validate: (record: T) => T }
>
// Secret shape types that don't have a shape util yet
type ShapeTypesNotImplemented = 'icon'
/** @public */
export type MigrationsForShapes<T extends TLUnknownShape> = Record<T['type'], Migrations>
type CustomShapeInfo<T extends TLUnknownShape> = {
util: TLShapeUtilConstructor<any>
validator?: { validate: (record: T) => T }
migrations?: Migrations
}
type UtilsForShapes<T extends TLUnknownShape> = Record<T['type'], TLShapeUtilConstructor<any>>
type TldrawEditorConfigOptions<T extends TLUnknownShape = TLShape> = {
tools?: readonly StateNodeConstructor[]
shapes?: { [K in T['type']]: CustomShapeInfo<T> }
/** @internal */
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
}
/** @public */
export class TldrawEditorConfig {
static readonly default = new TldrawEditorConfig({})
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>
readonly TLShape: RecordType<TLShape, 'type' | 'props' | 'index' | 'parentId'>
readonly tools: readonly StateNodeConstructor[]
// Custom shape utils
readonly shapeUtils: UtilsForShapes<TLShape>
// Validators for shape subtypes
readonly shapeValidators: Record<TLShape['type'], T.Validator<any>>
// Migrations for shape subtypes
readonly shapeMigrations: MigrationsForShapes<TLShape>
constructor(opts: TldrawEditorConfigOptions) {
const { shapes = [], tools = [], derivePresenceState } = opts
this.tools = tools
this.shapeUtils = {
const DEFAULT_SHAPE_UTILS: {
[K in Exclude<TLDefaultShape['type'], ShapeTypesNotImplemented>]: TLShapeUtilConstructor<any>
} = {
arrow: TLArrowUtil,
bookmark: TLBookmarkUtil,
draw: TLDrawUtil,
@ -131,78 +48,51 @@ export class TldrawEditorConfig {
note: TLNoteUtil,
text: TLTextUtil,
video: TLVideoUtil,
}
}
this.shapeMigrations = {
arrow: arrowShapeTypeMigrations,
bookmark: bookmarkShapeTypeMigrations,
draw: drawShapeTypeMigrations,
embed: embedShapeTypeMigrations,
frame: frameShapeTypeMigrations,
geo: geoShapeTypeMigrations,
group: groupShapeTypeMigrations,
image: imageShapeTypeMigrations,
line: lineShapeTypeMigrations,
note: noteShapeTypeMigrations,
text: textShapeTypeMigrations,
video: videoShapeTypeMigrations,
}
this.shapeValidators = {
arrow: arrowShapeTypeValidator,
bookmark: bookmarkShapeTypeValidator,
draw: drawShapeTypeValidator,
embed: embedShapeTypeValidator,
frame: frameShapeTypeValidator,
geo: geoShapeTypeValidator,
group: groupShapeTypeValidator,
image: imageShapeTypeValidator,
line: lineShapeTypeValidator,
note: noteShapeTypeValidator,
text: textShapeTypeValidator,
video: videoShapeTypeValidator,
}
// Add custom shapes
for (const [type, shape] of Object.entries(shapes)) {
this.shapeUtils[type] = shape.util
this.shapeMigrations[type] = shape.migrations ?? defineMigrations({})
this.shapeValidators[type] = (shape.validator ?? T.any) as T.Validator<any>
}
const shapeRecord = createRecordType<TLShape>('shape', {
migrations: defineMigrations({
currentVersion: rootShapeTypeMigrations.currentVersion,
firstVersion: rootShapeTypeMigrations.firstVersion,
migrators: rootShapeTypeMigrations.migrators,
subTypeKey: 'type',
subTypeMigrations: this.shapeMigrations,
}),
validator: T.model('shape', T.union('type', { ...this.shapeValidators })),
scope: 'document',
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false }))
this.storeSchema = StoreSchema.create<TLRecord, TLStoreProps>(
/** @public */
export type TldrawEditorConfigOptions = {
tools?: readonly StateNodeConstructor[]
shapes?: Record<
string,
{
asset: TLAsset,
camera: TLCamera,
document: TLDocument,
instance: TLInstance,
instance_page_state: TLInstancePageState,
page: TLPage,
shape: shapeRecord,
user: TLUser,
user_document: TLUserDocument,
user_presence: TLUserPresence,
instance_presence: TLInstancePresence,
},
{
snapshotMigrations: storeMigrations,
onValidationFailure,
createIntegrityChecker: createIntegrityChecker,
derivePresenceState: derivePresenceState ?? defaultDerivePresenceState,
util: TLShapeUtilConstructor<any>
validator?: { validate: <T>(record: T) => T }
migrations?: Migrations
}
)
>
/** @internal */
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
}
/** @public */
export class TldrawEditorConfig {
// Custom tools
readonly tools: readonly StateNodeConstructor[]
// Custom shape utils
readonly shapeUtils: Record<TLShape['type'], TLShapeUtilConstructor<any>>
// The record used for TLShape incorporating any custom shapes
readonly TLShape: RecordType<TLShape, 'type' | 'props' | 'index' | 'parentId'>
// The schema used for the store incorporating any custom shapes
readonly storeSchema: StoreSchema<TLRecord, TLStoreProps>
constructor(opts = {} as TldrawEditorConfigOptions) {
const { shapes = {}, tools = [], derivePresenceState } = opts
this.tools = tools
this.shapeUtils = {
...DEFAULT_SHAPE_UTILS,
...Object.fromEntries(Object.entries(shapes).map(([k, v]) => [k, v.util])),
}
this.storeSchema = createTLSchema({
customShapes: shapes,
derivePresenceState: derivePresenceState,
})
this.TLShape = this.storeSchema.types.shape as RecordType<
TLShape,

View file

@ -0,0 +1,7 @@
import { TestApp } from './TestApp'
it('loads the test app', () => {
expect(() => {
new TestApp()
}).not.toThrow()
})

View file

@ -55,11 +55,13 @@ export const TEST_INSTANCE_ID = TLInstance.createCustomId('testInstance1')
export const TEST_USER_ID = TLUser.createCustomId('testUser1')
export class TestApp extends App {
constructor(options = {} as Partial<AppOptions>) {
constructor(options = {} as Partial<Omit<AppOptions, 'store'>>) {
const elm = document.createElement('div')
elm.tabIndex = 0
const config = options.config ?? new TldrawEditorConfig()
super({
store: (options.config ?? TldrawEditorConfig.default).createStore({
config,
store: config.createStore({
userId: TEST_USER_ID,
instanceId: TEST_INSTANCE_ID,
}),

View file

@ -21,14 +21,17 @@ afterEach(() => {
describe('<Tldraw />', () => {
it('Accepts fresh versions of store and calls `onMount` for each one', async () => {
const initialStore = TldrawEditorConfig.default.createStore({
const config = new TldrawEditorConfig()
const initialStore = config.createStore({
instanceId: TLInstance.createCustomId('test'),
userId: TLUser.createCustomId('test'),
})
const onMount = jest.fn()
const rendered = render(
<TldrawEditor store={initialStore} onMount={onMount} autoFocus>
<TldrawEditor config={config} store={initialStore} onMount={onMount} autoFocus>
<div data-testid="canvas-1" />
</TldrawEditor>
)
@ -40,7 +43,7 @@ describe('<Tldraw />', () => {
// re-render with the same store:
rendered.rerender(
<TldrawEditor store={initialStore} onMount={onMount} autoFocus>
<TldrawEditor config={config} store={initialStore} onMount={onMount} autoFocus>
<div data-testid="canvas-2" />
</TldrawEditor>
)
@ -49,12 +52,12 @@ describe('<Tldraw />', () => {
expect(onMount).toHaveBeenCalledTimes(1)
// re-render with a new store:
const newStore = TldrawEditorConfig.default.createStore({
const newStore = config.createStore({
instanceId: TLInstance.createCustomId('test'),
userId: TLUser.createCustomId('test'),
})
rendered.rerender(
<TldrawEditor store={newStore} onMount={onMount} autoFocus>
<TldrawEditor config={config} store={newStore} onMount={onMount} autoFocus>
<div data-testid="canvas-3" />
</TldrawEditor>
)

View file

@ -1076,19 +1076,6 @@ export interface LegacyTldrawDocument {
/* ------------------ Translations ------------------ */
// const v1ShapeTypesToV2ShapeTypes: Record<TDShapeType, TLShapeType> = {
// [TDShapeType.Rectangle]: 'geo',
// [TDShapeType.Ellipse]: 'geo',
// [TDShapeType.Text]: 'text',
// [TDShapeType.Image]: 'image',
// [TDShapeType.Video]: 'video',
// [TDShapeType.Group]: 'group',
// [TDShapeType.Arrow]: 'arrow',
// [TDShapeType.Sticky]: 'note',
// [TDShapeType.Draw]: 'draw',
// [TDShapeType.Triangle]: 'geo',
// }
const v1ColorsToV2Colors: Record<ColorStyle, TLColorType> = {
[ColorStyle.White]: 'black',
[ColorStyle.Black]: 'black',

View file

@ -208,7 +208,7 @@ export async function parseAndLoadDocument(
forceDarkMode?: boolean
) {
const parseFileResult = parseTldrawJsonFile({
config: TldrawEditorConfig.default,
config: new TldrawEditorConfig(),
json: document,
instanceId: app.instanceId,
userId: app.userId,

View file

@ -17,14 +17,14 @@ function serialize(file: TldrawFile): string {
describe('parseTldrawJsonFile', () => {
it('returns an error if the file is not json', () => {
const result = parseTldrawJsonFile(TldrawEditorConfig.default, 'not json')
const result = parseTldrawJsonFile(new TldrawEditorConfig(), 'not json')
assert(!result.ok)
expect(result.error.type).toBe('notATldrawFile')
})
it('returns an error if the file doesnt look like a tldraw file', () => {
const result = parseTldrawJsonFile(
TldrawEditorConfig.default,
new TldrawEditorConfig(),
JSON.stringify({ not: 'a tldraw file' })
)
assert(!result.ok)
@ -33,10 +33,10 @@ describe('parseTldrawJsonFile', () => {
it('returns an error if the file version is too old', () => {
const result = parseTldrawJsonFile(
TldrawEditorConfig.default,
new TldrawEditorConfig(),
serialize({
tldrawFileFormatVersion: 0,
schema: TldrawEditorConfig.default.storeSchema.serialize(),
schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [],
})
)
@ -46,10 +46,10 @@ describe('parseTldrawJsonFile', () => {
it('returns an error if the file version is too new', () => {
const result = parseTldrawJsonFile(
TldrawEditorConfig.default,
new TldrawEditorConfig(),
serialize({
tldrawFileFormatVersion: 100,
schema: TldrawEditorConfig.default.storeSchema.serialize(),
schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [],
})
)
@ -58,10 +58,10 @@ describe('parseTldrawJsonFile', () => {
})
it('returns an error if migrations fail', () => {
const serializedSchema = TldrawEditorConfig.default.storeSchema.serialize()
const serializedSchema = new TldrawEditorConfig().storeSchema.serialize()
serializedSchema.storeVersion = 100
const result = parseTldrawJsonFile(
TldrawEditorConfig.default,
new TldrawEditorConfig(),
serialize({
tldrawFileFormatVersion: 1,
schema: serializedSchema,
@ -72,10 +72,10 @@ describe('parseTldrawJsonFile', () => {
assert(result.error.type === 'migrationFailed')
expect(result.error.reason).toBe(MigrationFailureReason.TargetVersionTooOld)
const serializedSchema2 = TldrawEditorConfig.default.storeSchema.serialize()
const serializedSchema2 = new TldrawEditorConfig().storeSchema.serialize()
serializedSchema2.recordVersions.shape.version = 100
const result2 = parseTldrawJsonFile(
TldrawEditorConfig.default,
new TldrawEditorConfig(),
serialize({
tldrawFileFormatVersion: 1,
schema: serializedSchema2,
@ -90,10 +90,10 @@ describe('parseTldrawJsonFile', () => {
it('returns an error if a record is invalid', () => {
const result = parseTldrawJsonFile(
TldrawEditorConfig.default,
new TldrawEditorConfig(),
serialize({
tldrawFileFormatVersion: 1,
schema: TldrawEditorConfig.default.storeSchema.serialize(),
schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [
{
typeName: 'shape',
@ -113,10 +113,10 @@ describe('parseTldrawJsonFile', () => {
it('returns a store if the file is valid', () => {
const result = parseTldrawJsonFile(
TldrawEditorConfig.default,
new TldrawEditorConfig(),
serialize({
tldrawFileFormatVersion: 1,
schema: TldrawEditorConfig.default.storeSchema.serialize(),
schema: new TldrawEditorConfig().storeSchema.serialize(),
records: [],
})
)

View file

@ -8,9 +8,10 @@ import { TldrawEditorProps } from '@tldraw/editor';
import { TldrawUiContextProviderProps } from '@tldraw/ui';
// @public (undocumented)
export function Tldraw(props: Omit<TldrawEditorProps, 'store'> & TldrawUiContextProviderProps & {
export function Tldraw(props: Omit<TldrawEditorProps, 'config' | 'store'> & TldrawUiContextProviderProps & {
persistenceKey?: string;
hideUi?: boolean;
config?: TldrawEditorProps['config'];
}): JSX.Element;

View file

@ -1,4 +1,4 @@
import { Canvas, TldrawEditor, TldrawEditorProps } from '@tldraw/editor'
import { Canvas, TldrawEditor, TldrawEditorConfig, TldrawEditorProps } from '@tldraw/editor'
import {
DEFAULT_DOCUMENT_NAME,
TAB_ID,
@ -6,18 +6,33 @@ import {
useLocalSyncClient,
} from '@tldraw/tlsync-client'
import { ContextMenu, TldrawUi, TldrawUiContextProviderProps } from '@tldraw/ui'
import { useEffect, useState } from 'react'
/** @public */
export function Tldraw(
props: Omit<TldrawEditorProps, 'store'> &
props: Omit<TldrawEditorProps, 'store' | 'config'> &
TldrawUiContextProviderProps & {
/** The key under which to persist this editor's data to local storage. */
persistenceKey?: string
/** Whether to hide the user interface and only display the canvas. */
hideUi?: boolean
/** A custom configuration for this Tldraw editor */
config?: TldrawEditorProps['config']
}
) {
const { children, persistenceKey = DEFAULT_DOCUMENT_NAME, instanceId = TAB_ID, ...rest } = props
const {
config,
children,
persistenceKey = DEFAULT_DOCUMENT_NAME,
instanceId = TAB_ID,
...rest
} = props
const [_config, _setConfig] = useState(() => config ?? new TldrawEditorConfig())
useEffect(() => {
_setConfig(config ?? new TldrawEditorConfig())
}, [config])
const userData = getUserData()
@ -25,13 +40,19 @@ export function Tldraw(
const syncedStore = useLocalSyncClient({
instanceId,
userId: userId,
userId,
config: _config,
universalPersistenceKey: persistenceKey,
config: props.config,
})
return (
<TldrawEditor {...rest} instanceId={instanceId} userId={userId} store={syncedStore}>
<TldrawEditor
{...rest}
instanceId={instanceId}
userId={userId}
store={syncedStore}
config={_config}
>
<TldrawUi {...rest}>
<ContextMenu>
<Canvas />

View file

@ -103,6 +103,12 @@ export function createShapeValidator<Type extends string, Props extends object>(
props: Props;
}>;
// @public
export function createTLSchema<T extends TLUnknownShape>(opts?: {
customShapes?: { [K in T["type"]]: CustomShapeInfo<T>; } | undefined;
derivePresenceState?: ((store: TLStore) => Signal<null | TLInstancePresence>) | undefined;
}): StoreSchema<TLRecord, TLStoreProps>;
// @public (undocumented)
export const cursorTypeValidator: T.Validator<string>;
@ -722,6 +728,9 @@ export interface TLDashStyle extends TLBaseStyle {
// @public (undocumented)
export type TLDashType = SetValue<typeof TL_DASH_TYPES>;
// @public
export type TLDefaultShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLIconShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLVideoShape;
// @public
export interface TLDocument extends BaseRecord<'document'> {
// (undocumented)
@ -1137,7 +1146,7 @@ export type TLScribble = {
};
// @public
export type TLShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLIconShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLUnknownShape | TLVideoShape;
export type TLShape = TLDefaultShape | TLUnknownShape;
// @public (undocumented)
export type TLShapeId = ID<TLBaseShape<any, any>>;
@ -1155,9 +1164,6 @@ export type TLShapeProp = keyof TLShapeProps;
// @public (undocumented)
export type TLShapeProps = SmooshedUnionObject<TLShape['props']>;
// @public (undocumented)
export type TLShapeType = TLShape['type'];
// @public (undocumented)
export interface TLSizeStyle extends TLBaseStyle {
// (undocumented)
@ -1252,7 +1258,7 @@ export type TLTextShapeProps = {
// @public (undocumented)
export type TLUiColorType = SetValue<typeof TL_UI_COLOR_TYPES>;
// @public (undocumented)
// @public
export type TLUnknownShape = TLBaseShape<string, object>;
// @public

View file

@ -0,0 +1,134 @@
import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/tlstore'
import { T } from '@tldraw/tlvalidate'
import { Signal } from 'signia'
import { TLRecord } from './TLRecord'
import { TLStore, TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore'
import { defaultDerivePresenceState } from './defaultDerivePresenceState'
import { TLAsset } from './records/TLAsset'
import { TLCamera } from './records/TLCamera'
import { TLDocument } from './records/TLDocument'
import { TLInstance } from './records/TLInstance'
import { TLInstancePageState } from './records/TLInstancePageState'
import { TLInstancePresence } from './records/TLInstancePresence'
import { TLPage } from './records/TLPage'
import { TLShape, TLUnknownShape, rootShapeTypeMigrations } from './records/TLShape'
import { TLUser } from './records/TLUser'
import { TLUserDocument } from './records/TLUserDocument'
import { TLUserPresence } from './records/TLUserPresence'
import { storeMigrations } from './schema'
import { arrowShapeTypeMigrations, arrowShapeTypeValidator } from './shapes/TLArrowShape'
import { bookmarkShapeTypeMigrations, bookmarkShapeTypeValidator } from './shapes/TLBookmarkShape'
import { drawShapeTypeMigrations, drawShapeTypeValidator } from './shapes/TLDrawShape'
import { embedShapeTypeMigrations, embedShapeTypeValidator } from './shapes/TLEmbedShape'
import { frameShapeTypeMigrations, frameShapeTypeValidator } from './shapes/TLFrameShape'
import { geoShapeTypeMigrations, geoShapeTypeValidator } from './shapes/TLGeoShape'
import { groupShapeTypeMigrations, groupShapeTypeValidator } from './shapes/TLGroupShape'
import { imageShapeTypeMigrations, imageShapeTypeValidator } from './shapes/TLImageShape'
import { lineShapeTypeMigrations, lineShapeTypeValidator } from './shapes/TLLineShape'
import { noteShapeTypeMigrations, noteShapeTypeValidator } from './shapes/TLNoteShape'
import { textShapeTypeMigrations, textShapeTypeValidator } from './shapes/TLTextShape'
import { videoShapeTypeMigrations, videoShapeTypeValidator } from './shapes/TLVideoShape'
type DefaultShapeInfo<T extends TLShape> = {
validator: T.Validator<T>
migrations: Migrations
}
const DEFAULT_SHAPES: { [K in TLShape['type']]: DefaultShapeInfo<Extract<TLShape, { type: K }>> } =
{
arrow: { migrations: arrowShapeTypeMigrations, validator: arrowShapeTypeValidator },
bookmark: { migrations: bookmarkShapeTypeMigrations, validator: bookmarkShapeTypeValidator },
draw: { migrations: drawShapeTypeMigrations, validator: drawShapeTypeValidator },
embed: { migrations: embedShapeTypeMigrations, validator: embedShapeTypeValidator },
frame: { migrations: frameShapeTypeMigrations, validator: frameShapeTypeValidator },
geo: { migrations: geoShapeTypeMigrations, validator: geoShapeTypeValidator },
group: { migrations: groupShapeTypeMigrations, validator: groupShapeTypeValidator },
image: { migrations: imageShapeTypeMigrations, validator: imageShapeTypeValidator },
line: { migrations: lineShapeTypeMigrations, validator: lineShapeTypeValidator },
note: { migrations: noteShapeTypeMigrations, validator: noteShapeTypeValidator },
text: { migrations: textShapeTypeMigrations, validator: textShapeTypeValidator },
video: { migrations: videoShapeTypeMigrations, validator: videoShapeTypeValidator },
}
type CustomShapeInfo<T extends TLUnknownShape> = {
validator?: { validate: (record: T) => T }
migrations?: Migrations
}
/**
* Create a store schema for a tldraw store that includes all the default shapes together with any custom shapes.
* @public */
export function createTLSchema<T extends TLUnknownShape>(
opts = {} as {
customShapes?: { [K in T['type']]: CustomShapeInfo<T> }
derivePresenceState?: (store: TLStore) => Signal<TLInstancePresence | null>
}
) {
const { customShapes = {}, derivePresenceState } = opts
const defaultShapeSubTypeEntries = Object.entries(DEFAULT_SHAPES) as [
TLShape['type'],
DefaultShapeInfo<TLShape>
][]
const customShapeSubTypeEntries = Object.entries(customShapes) as [
T['type'],
CustomShapeInfo<T>
][]
// Create a shape record that incorporates the defeault shapes and any custom shapes
// into its subtype migrations and validators, so that we can migrate any new custom
// subtypes. Note that migrations AND validators for custom shapes are optional. If
// not provided, we use an empty migrations set and/or an "any" validator.
const shapeSubTypeMigrationsWithCustomSubTypeMigrations = {
...Object.fromEntries(defaultShapeSubTypeEntries.map(([k, v]) => [k, v.migrations])),
...Object.fromEntries(
customShapeSubTypeEntries.map(([k, v]) => [k, v.migrations ?? defineMigrations({})])
),
}
const validatorWithCustomShapeValidators = T.model(
'shape',
T.union('type', {
...Object.fromEntries(defaultShapeSubTypeEntries.map(([k, v]) => [k, v.validator])),
...Object.fromEntries(
customShapeSubTypeEntries.map(([k, v]) => [k, (v.validator as T.Validator<any>) ?? T.any])
),
})
)
const shapeRecord = createRecordType<TLShape>('shape', {
migrations: defineMigrations({
currentVersion: rootShapeTypeMigrations.currentVersion,
firstVersion: rootShapeTypeMigrations.firstVersion,
migrators: rootShapeTypeMigrations.migrators,
subTypeKey: 'type',
subTypeMigrations: shapeSubTypeMigrationsWithCustomSubTypeMigrations,
}),
validator: validatorWithCustomShapeValidators,
scope: 'document',
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false }))
return StoreSchema.create<TLRecord, TLStoreProps>(
{
asset: TLAsset,
camera: TLCamera,
document: TLDocument,
instance: TLInstance,
instance_page_state: TLInstancePageState,
page: TLPage,
shape: shapeRecord,
user: TLUser,
user_document: TLUserDocument,
user_presence: TLUserPresence,
instance_presence: TLInstancePresence,
},
{
snapshotMigrations: storeMigrations,
onValidationFailure,
createIntegrityChecker: createIntegrityChecker,
derivePresenceState: derivePresenceState ?? defaultDerivePresenceState,
}
)
}

View file

@ -24,6 +24,7 @@ export {
type TLVideoAsset,
} from './assets/TLVideoAsset'
export { createAssetValidator, type TLBaseAsset } from './assets/asset-validation'
export { createTLSchema } from './createTLSchema'
export { defaultDerivePresenceState } from './defaultDerivePresenceState'
export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup'
export { type Box2dModel, type Vec2dModel } from './geometry-types'
@ -58,6 +59,7 @@ export {
isShape,
isShapeId,
rootShapeTypeMigrations,
type TLDefaultShape,
type TLNullableShapeProps,
type TLParentId,
type TLShape,
@ -65,7 +67,6 @@ export {
type TLShapePartial,
type TLShapeProp,
type TLShapeProps,
type TLShapeType,
type TLUnknownShape,
} from './records/TLShape'
export { TLUser, userTypeValidator, type TLUserId } from './records/TLUser'

View file

@ -17,15 +17,11 @@ import { TLVideoShape } from '../shapes/TLVideoShape'
import { SmooshedUnionObject } from '../util-types'
import { TLPageId } from './TLPage'
/** @public */
export type TLUnknownShape = TLBaseShape<string, object>
/**
* TLShape
* The default set of shapes that are available in the editor.
*
* @public
*/
export type TLShape =
* @public */
export type TLDefaultShape =
| TLArrowShape
| TLBookmarkShape
| TLDrawShape
@ -38,11 +34,21 @@ export type TLShape =
| TLNoteShape
| TLTextShape
| TLVideoShape
| TLUnknownShape
| TLIconShape
/** @public */
export type TLShapeType = TLShape['type']
/**
* A type for a shape that is available in the editor but whose type is
* unknowneither one of the editor's default shapes or else a custom shape.
*
* @public */
export type TLUnknownShape = TLBaseShape<string, object>
/**
* The set of all shapes that are available in the editor, including unknown shapes.
*
* @public
*/
export type TLShape = TLDefaultShape | TLUnknownShape
/** @public */
export type TLShapePartial<T extends TLShape = TLShape> = T extends T

View file

@ -101,7 +101,7 @@ export function useLocalSyncClient({ universalPersistenceKey, instanceId, userId
universalPersistenceKey: string;
instanceId: TLInstanceId;
userId: TLUserId;
config?: TldrawEditorConfig;
config: TldrawEditorConfig;
}): SyncedStore;
// (No @packageDocumentation comment for this package)

View file

@ -34,7 +34,7 @@ function testClient(
userId: TLUserId = TLUser.createCustomId('test'),
channel = new BroadcastChannelMock('test')
) {
const store = TldrawEditorConfig.default.createStore({
const store = new TldrawEditorConfig().createStore({
userId,
instanceId,
})

View file

@ -14,12 +14,12 @@ export function useLocalSyncClient({
universalPersistenceKey,
instanceId,
userId,
config = TldrawEditorConfig.default,
config,
}: {
universalPersistenceKey: string
instanceId: TLInstanceId
userId: TLUserId
config?: TldrawEditorConfig
config: TldrawEditorConfig
}): SyncedStore {
const [state, setState] = useState<{ id: string; syncedStore: SyncedStore } | null>(null)