mini defineShape API (#1563)

Based on #1549, but with a lot of code-structure related changes backed
out. Shape schemas are still defined in tlschemas with this diff.

Couple differences between this and #1549:
- This tightens up the relationship between store schemas and editor
schemas a bit
- Reduces the number of places we need to remember to include core
shapes
- Only `<TLdrawEditor />` sets default shapes by default. If you're
doing something funky with lower-level APIs, you need to specify
`defaultShapes` manually
- Replaces `validator` with `props` for shapes

### Change Type

- [x] `major` — Breaking Change

### Test Plan

1. Add a step-by-step description of how to test your PR here.
2.

- [x] Unit Tests
- [ ] Webdriver tests

### Release Notes

[dev-facing, notes to come]
This commit is contained in:
alex 2023-06-12 15:04:14 +01:00 committed by GitHub
parent 4b680d9451
commit 1927f88041
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 1081 additions and 673 deletions

View file

@ -1,4 +1,4 @@
import { Canvas, ContextMenu, TldrawEditor, TldrawUi, createTLStore } from '@tldraw/tldraw'
import { Tldraw, createTLStore, defaultShapes } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
import { throttle } from '@tldraw/utils'
@ -7,7 +7,7 @@ import { useLayoutEffect, useState } from 'react'
const PERSISTENCE_KEY = 'example-3'
export default function PersistenceExample() {
const [store] = useState(() => createTLStore())
const [store] = useState(() => createTLStore({ shapes: defaultShapes }))
const [loadingState, setLoadingState] = useState<
{ status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string }
>({
@ -64,13 +64,7 @@ export default function PersistenceExample() {
return (
<div className="tldraw__editor">
<TldrawEditor store={store} autoFocus>
<TldrawUi>
<ContextMenu>
<Canvas />
</ContextMenu>
</TldrawUi>
</TldrawEditor>
<Tldraw store={store} autoFocus />
</div>
)
}

View file

@ -1,9 +0,0 @@
import { TLBaseShape } from '@tldraw/tldraw'
export type CardShape = TLBaseShape<
'card',
{
w: number
h: number
}
>

View file

@ -1,5 +1,12 @@
import { BaseBoxShapeUtil, HTMLContainer } from '@tldraw/tldraw'
import { CardShape } from './CardShape'
import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape, defineShape } from '@tldraw/tldraw'
export type CardShape = TLBaseShape<
'card',
{
w: number
h: number
}
>
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
// Id — the shape util's id
@ -43,3 +50,7 @@ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
return <rect width={shape.props.w} height={shape.props.h} />
}
}
export const CardShape = defineShape('card', {
util: CardShapeUtil,
})

View file

@ -1,10 +1,10 @@
import { TLUiMenuGroup, Tldraw, menuItem, toolbarItem } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
import { CardShape } from './CardShape'
import { CardShapeTool } from './CardShapeTool'
import { CardShapeUtil } from './CardShapeUtil'
const shapes = { card: { util: CardShapeUtil } }
const shapes = [CardShape]
const tools = [CardShapeTool]
export default function CustomConfigExample() {

View file

@ -1,4 +1,4 @@
import { Canvas, TldrawEditor, useEditor } from '@tldraw/tldraw'
import { Canvas, TldrawEditor, defaultShapes, defaultTools, useEditor } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import { useEffect } from 'react'
import { track } from 'signia-react'
@ -7,7 +7,7 @@ import './custom-ui.css'
export default function CustomUiExample() {
return (
<div className="tldraw__editor">
<TldrawEditor autoFocus>
<TldrawEditor shapes={defaultShapes} tools={defaultTools} autoFocus>
<Canvas />
<CustomUi />
</TldrawEditor>

View file

@ -1,11 +1,23 @@
import { Canvas, ContextMenu, TldrawEditor, TldrawUi } from '@tldraw/tldraw'
import {
Canvas,
ContextMenu,
TldrawEditor,
TldrawUi,
defaultShapes,
defaultTools,
} from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
export default function ExplodedExample() {
return (
<div className="tldraw__editor">
<TldrawEditor autoFocus persistenceKey="exploded-example">
<TldrawEditor
shapes={defaultShapes}
tools={defaultTools}
autoFocus
persistenceKey="exploded-example"
>
<TldrawUi>
<ContextMenu>
<Canvas />

View file

@ -1,19 +1,16 @@
import { createShapeId, Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
import { ErrorShapeUtil } from './ErrorShapeUtil'
import { ErrorShape } from './ErrorShape'
const shapes = {
error: {
util: ErrorShapeUtil, // a custom shape that will always error
},
}
const shapes = [ErrorShape]
export default function ErrorBoundaryExample() {
return (
<div className="tldraw__editor">
<Tldraw
shapes={shapes}
tools={[]}
components={{
ErrorFallback: null, // disable app-level error boundaries
ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>, // use a custom error fallback for shapes

View file

@ -1,3 +1,20 @@
import { TLBaseShape } from '@tldraw/tldraw'
import { BaseBoxShapeUtil, TLBaseShape, defineShape } from '@tldraw/tldraw'
export type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }>
export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
static override type = 'error' as const
override type = 'error' as const
defaultProps() {
return { message: 'Error!', w: 100, h: 100 }
}
render(shape: ErrorShape) {
throw new Error(shape.props.message)
}
indicator() {
throw new Error(`Error shape indicator!`)
}
}
export const ErrorShape = defineShape('error', { util: ErrorShapeUtil })

View file

@ -1,17 +0,0 @@
import { BaseBoxShapeUtil } from '@tldraw/tldraw'
import { ErrorShape } from './ErrorShape'
export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
static override type = 'error'
override type = 'error' as const
defaultProps() {
return { message: 'Error!', w: 100, h: 100 }
}
render(shape: ErrorShape) {
throw new Error(shape.props.message)
}
indicator() {
throw new Error(`Error shape indicator!`)
}
}

View file

@ -1,4 +1,4 @@
import { TLStoreWithStatus, createTLStore } from '@tldraw/tldraw'
import { TLStoreWithStatus, createTLStore, defaultShapes } from '@tldraw/tldraw'
import { useEffect, useState } from 'react'
import {
initializeStoreFromYjsDoc,
@ -14,7 +14,7 @@ export function useYjsStore() {
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({ status: 'loading' })
useEffect(() => {
const store = createTLStore()
const store = createTLStore({ shapes: defaultShapes })
initializeStoreFromYjsDoc(store)
syncYjsDocChangesToStore(store)
syncStoreChangesToYjsDoc(store)

View file

@ -1,4 +1,12 @@
import { Canvas, Editor, ErrorBoundary, TldrawEditor, setRuntimeOverrides } from '@tldraw/editor'
import {
Canvas,
Editor,
ErrorBoundary,
TldrawEditor,
defaultShapes,
defaultTools,
setRuntimeOverrides,
} from '@tldraw/editor'
import { linksUiOverrides } from './utils/links'
// eslint-disable-next-line import/no-internal-modules
import '@tldraw/editor/editor.css'
@ -124,7 +132,14 @@ function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerPro
}, [])
return (
<TldrawEditor assetUrls={assetUrls} persistenceKey={uri} onMount={handleMount} autoFocus>
<TldrawEditor
shapes={defaultShapes}
tools={defaultTools}
assetUrls={assetUrls}
persistenceKey={uri}
onMount={handleMount}
autoFocus
>
{/* <DarkModeHandler themeKind={themeKind} /> */}
<TldrawUi assetUrls={assetUrls} overrides={[menuOverrides, linksUiOverrides]}>
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />

View file

@ -1,17 +1,17 @@
import { createTLSchema } from '@tldraw/editor'
import { createTLStore, defaultShapes } from '@tldraw/editor'
import { TldrawFile } from '@tldraw/file-format'
import * as vscode from 'vscode'
import { nicelog } from './utils'
export const defaultFileContents: TldrawFile = {
tldrawFileFormatVersion: 1,
schema: createTLSchema().serialize(),
schema: createTLStore({ shapes: defaultShapes }).schema.serialize(),
records: [],
}
export const fileContentWithErrors: TldrawFile = {
tldrawFileFormatVersion: 1,
schema: createTLSchema().serialize(),
schema: createTLStore({ shapes: defaultShapes }).schema.serialize(),
records: [{ typeName: 'shape', id: null } as any],
}

View file

@ -30,7 +30,9 @@ import { SelectionCorner } from '@tldraw/primitives';
import { SelectionEdge } from '@tldraw/primitives';
import { SelectionHandle } from '@tldraw/primitives';
import { SerializedSchema } from '@tldraw/store';
import { ShapeProps } from '@tldraw/tlschema';
import { Signal } from 'signia';
import { StoreSchema } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { StrokePoint } from '@tldraw/primitives';
import { TLAlignType } from '@tldraw/tlschema';
@ -77,6 +79,7 @@ import { TLShapeProps } from '@tldraw/tlschema';
import { TLSizeStyle } from '@tldraw/tlschema';
import { TLSizeType } from '@tldraw/tlschema';
import { TLStore } from '@tldraw/tlschema';
import { TLStoreProps } from '@tldraw/tlschema';
import { TLStyleCollections } from '@tldraw/tlschema';
import { TLStyleType } from '@tldraw/tlschema';
import { TLTextShape } from '@tldraw/tlschema';
@ -166,7 +169,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// (undocumented)
toSvg(shape: TLArrowShape, font: string, colors: TLExportColors): SVGGElement;
// (undocumented)
static type: string;
static type: "arrow";
}
// @public (undocumented)
@ -221,7 +224,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
// (undocumented)
render(shape: TLBookmarkShape): JSX.Element;
// (undocumented)
static type: string;
static type: "bookmark";
// (undocumented)
protected updateBookmarkAsset: {
(shape: TLBookmarkShape): Promise<void>;
@ -243,6 +246,9 @@ export const checkFlag: (flag: (() => boolean) | boolean | undefined) => boolean
// @public
export function containBoxSize(originalSize: BoxWidthHeight, containBoxSize: BoxWidthHeight): BoxWidthHeight;
// @public (undocumented)
export const coreShapes: readonly [TLShapeInfo<TLGroupShape>, TLShapeInfo<TLEmbedShape>, TLShapeInfo<TLBookmarkShape>, TLShapeInfo<TLImageShape>, TLShapeInfo<TLTextShape>];
// @public (undocumented)
export function correctSpacesToNbsp(input: string): string;
@ -250,7 +256,7 @@ export function correctSpacesToNbsp(input: string): string;
export function createSessionStateSnapshotSignal(store: TLStore): Signal<null | TLSessionStateSnapshot>;
// @public
export function createTLStore(opts?: TLStoreOptions): TLStore;
export function createTLStore({ initialData, defaultName, ...rest }: TLStoreOptions): TLStore;
// @public (undocumented)
export function dataTransferItemAsString(item: DataTransferItem): Promise<string>;
@ -298,11 +304,14 @@ export function defaultEmptyAs(str: string, dflt: string): string;
export const DefaultErrorFallback: TLErrorFallbackComponent;
// @public (undocumented)
export const defaultShapes: Record<string, TLShapeInfo>;
export const defaultShapes: readonly [TLShapeInfo<TLDrawShape>, TLShapeInfo<TLGeoShape>, TLShapeInfo<TLLineShape>, TLShapeInfo<TLNoteShape>, TLShapeInfo<TLFrameShape>, TLShapeInfo<TLArrowShape>, TLShapeInfo<TLHighlightShape>, TLShapeInfo<TLVideoShape>];
// @public (undocumented)
export const defaultTools: TLStateNodeConstructor[];
// @public (undocumented)
export function defineShape<T extends TLUnknownShape>(type: T['type'], opts: Omit<TLShapeInfo<T>, 'type'>): TLShapeInfo<T>;
// @internal (undocumented)
export const DOUBLE_CLICK_DURATION = 450;
@ -347,12 +356,12 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
// (undocumented)
toSvg(shape: TLDrawShape, _font: string | undefined, colors: TLExportColors): SVGGElement;
// (undocumented)
static type: string;
static type: "draw";
}
// @public (undocumented)
export class Editor extends EventEmitter<TLEventMap> {
constructor({ store, user, tools, shapes, getContainer, }: TLEditorOptions);
constructor({ store, user, shapes, tools, getContainer }: TLEditorOptions);
addOpenMenu(id: string): this;
alignShapes(operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top', ids?: TLShapeId[]): this;
get allShapesCommonBounds(): Box2d | null;
@ -792,7 +801,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
// (undocumented)
render(shape: TLEmbedShape): JSX.Element;
// (undocumented)
static type: string;
static type: "embed";
}
// @public (undocumented)
@ -867,7 +876,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
// (undocumented)
toSvg(shape: TLFrameShape, font: string, colors: TLExportColors): Promise<SVGElement> | SVGElement;
// (undocumented)
static type: string;
static type: "frame";
}
// @public (undocumented)
@ -985,7 +994,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
// (undocumented)
toSvg(shape: TLGeoShape, font: string, colors: TLExportColors): SVGElement;
// (undocumented)
static type: string;
static type: "geo";
}
// @public
@ -1104,7 +1113,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
// (undocumented)
render(shape: TLGroupShape): JSX.Element | null;
// (undocumented)
static type: string;
static type: "group";
// (undocumented)
type: "group";
}
@ -1160,7 +1169,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
// (undocumented)
toSvg(shape: TLHighlightShape, _font: string | undefined, colors: TLExportColors): SVGPathElement;
// (undocumented)
static type: string;
static type: "highlight";
}
// @public (undocumented)
@ -1191,7 +1200,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
// (undocumented)
toSvg(shape: TLImageShape): Promise<SVGGElement>;
// (undocumented)
static type: string;
static type: "image";
}
// @public (undocumented)
@ -1261,7 +1270,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
// (undocumented)
toSvg(shape: TLLineShape, _font: string, colors: TLExportColors): SVGGElement;
// (undocumented)
static type: string;
static type: "line";
}
// @public (undocumented)
@ -1281,6 +1290,17 @@ export const MAJOR_NUDGE_FACTOR = 10;
// @public (undocumented)
export function matchEmbedUrl(url: string): {
definition: {
readonly type: "tldraw";
readonly title: "tldraw";
readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"];
readonly minWidth: 300;
readonly minHeight: 300;
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "codepen";
readonly title: "Codepen";
readonly hostnames: readonly ["codepen.io"];
@ -1289,7 +1309,7 @@ export function matchEmbedUrl(url: string): {
readonly width: 520;
readonly height: 400;
readonly doesResize: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly toEmbedUrl: (url: string) => string | undefined; /** @public */
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "codesandbox";
@ -1411,17 +1431,6 @@ export function matchEmbedUrl(url: string): {
readonly doesResize: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "tldraw";
readonly title: "tldraw";
readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"];
readonly minWidth: 300;
readonly minHeight: 300;
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "vimeo";
readonly title: "Vimeo";
@ -1453,6 +1462,17 @@ export function matchEmbedUrl(url: string): {
// @public (undocumented)
export function matchUrl(url: string): {
definition: {
readonly type: "tldraw";
readonly title: "tldraw";
readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"];
readonly minWidth: 300;
readonly minHeight: 300;
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "codepen";
readonly title: "Codepen";
readonly hostnames: readonly ["codepen.io"];
@ -1461,7 +1481,7 @@ export function matchUrl(url: string): {
readonly width: 520;
readonly height: 400;
readonly doesResize: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly toEmbedUrl: (url: string) => string | undefined; /** @public */
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "codesandbox";
@ -1583,17 +1603,6 @@ export function matchUrl(url: string): {
readonly doesResize: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "tldraw";
readonly title: "tldraw";
readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"];
readonly minWidth: 300;
readonly minHeight: 300;
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "vimeo";
readonly title: "Vimeo";
@ -1731,7 +1740,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
// (undocumented)
toSvg(shape: TLNoteShape, font: string, colors: TLExportColors): SVGGElement;
// (undocumented)
static type: string;
static type: "note";
}
// @public (undocumented)
@ -2092,7 +2101,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
// (undocumented)
toSvg(shape: TLTextShape, font: string | undefined, colors: TLExportColors): SVGGElement;
// (undocumented)
static type: string;
static type: "text";
}
// @public (undocumented)
@ -2191,8 +2200,8 @@ export const TldrawEditor: React_2.NamedExoticComponent<TldrawEditorProps>;
// @public (undocumented)
export type TldrawEditorProps = {
children?: any;
shapes?: Record<string, TLShapeInfo>;
tools?: TLStateNodeConstructor[];
shapes?: readonly AnyTLShapeInfo[];
tools?: readonly TLStateNodeConstructor[];
assetUrls?: RecursivePartial<TLEditorAssetUrls>;
autoFocus?: boolean;
components?: Partial<TLEditorComponents>;
@ -2260,9 +2269,9 @@ export interface TLEditorComponents {
// @public (undocumented)
export interface TLEditorOptions {
getContainer: () => HTMLElement;
shapes?: Record<string, TLShapeInfo>;
shapes: readonly AnyTLShapeInfo[];
store: TLStore;
tools?: TLStateNodeConstructor[];
tools: readonly TLStateNodeConstructor[];
user?: TLUser;
}
@ -2596,12 +2605,11 @@ export interface TLSessionStateSnapshot {
}
// @public (undocumented)
export type TLShapeInfo = {
util: TLShapeUtilConstructor<any>;
export type TLShapeInfo<T extends TLUnknownShape = TLUnknownShape> = {
type: T['type'];
util: TLShapeUtilConstructor<T>;
props?: ShapeProps<T>;
migrations?: Migrations;
validator?: {
validate: (record: any) => any;
};
};
// @public (undocumented)
@ -2634,10 +2642,13 @@ export type TLStoreEventInfo = HistoryEntry<TLRecord>;
// @public (undocumented)
export type TLStoreOptions = {
customShapes?: Record<string, TLShapeInfo>;
initialData?: StoreSnapshot<TLRecord>;
defaultName?: string;
};
} & ({
schema: StoreSchema<TLRecord, TLStoreProps>;
} | {
shapes: readonly AnyTLShapeInfo[];
});
// @public (undocumented)
export type TLStoreWithStatus = {
@ -2713,9 +2724,9 @@ export function useContainer(): HTMLDivElement;
export const useEditor: () => Editor;
// @internal (undocumented)
export function useLocalStore(opts?: {
persistenceKey?: string | undefined;
sessionId?: string | undefined;
export function useLocalStore({ persistenceKey, sessionId, ...rest }: {
persistenceKey?: string;
sessionId?: string;
} & TLStoreOptions): TLStoreWithStatus;
// @internal (undocumented)
@ -2754,7 +2765,7 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
// (undocumented)
toSvg(shape: TLVideoShape): SVGGElement;
// (undocumented)
static type: string;
static type: "video";
}
// @internal (undocumented)

View file

@ -41,12 +41,12 @@ export {
} from './lib/config/TLUserPreferences'
export {
createTLStore,
type TLShapeInfo,
type TLStoreEventInfo,
type TLStoreOptions,
} from './lib/config/createTLStore'
export { defaultShapes } from './lib/config/defaultShapes'
export { coreShapes, defaultShapes } from './lib/config/defaultShapes'
export { defaultTools } from './lib/config/defaultTools'
export { defineShape, type TLShapeInfo } from './lib/config/defineShape'
export {
ANIMATION_MEDIUM_MS,
ANIMATION_SHORT_MS,

View file

@ -1,11 +1,11 @@
import { Store, StoreSnapshot } from '@tldraw/store'
import { TLRecord, TLStore } from '@tldraw/tlschema'
import { RecursivePartial, annotateError } from '@tldraw/utils'
import { RecursivePartial, Required, annotateError } from '@tldraw/utils'
import React, { memo, useCallback, useLayoutEffect, useState, useSyncExternalStore } from 'react'
import { TLEditorAssetUrls, useDefaultEditorAssetsWithOverrides } from './assetUrls'
import { DefaultErrorFallback } from './components/DefaultErrorFallback'
import { OptionalErrorBoundary } from './components/ErrorBoundary'
import { TLShapeInfo } from './config/createTLStore'
import { AnyTLShapeInfo } from './config/defineShape'
import { Editor } from './editor/Editor'
import { TLStateNodeConstructor } from './editor/tools/StateNode'
import { ContainerProvider, useContainer } from './hooks/useContainer'
@ -31,11 +31,11 @@ export type TldrawEditorProps = {
/**
* An array of shape utils to use in the editor.
*/
shapes?: Record<string, TLShapeInfo>
shapes?: readonly AnyTLShapeInfo[]
/**
* An array of tools to use in the editor.
*/
tools?: TLStateNodeConstructor[]
tools?: readonly TLStateNodeConstructor[]
/**
* Urls for where to find fonts and other assets.
*/
@ -105,16 +105,28 @@ declare global {
}
}
const EMPTY_SHAPES_ARRAY = [] as const
const EMPTY_TOOLS_ARRAY = [] as const
/** @public */
export const TldrawEditor = memo(function TldrawEditor(props: TldrawEditorProps) {
export const TldrawEditor = memo(function TldrawEditor({
store,
components,
...rest
}: TldrawEditorProps) {
const [container, setContainer] = React.useState<HTMLDivElement | null>(null)
const ErrorFallback =
props.components?.ErrorFallback === undefined
? DefaultErrorFallback
: props.components?.ErrorFallback
components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback
const { store, ...rest } = props
// apply defaults. if you're using the bare @tldraw/editor package, we
// default these to the "tldraw zero" configuration. We have different
// defaults applied in @tldraw/tldraw.
const withDefaults = {
...rest,
shapes: rest.shapes ?? EMPTY_SHAPES_ARRAY,
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
}
return (
<div ref={setContainer} draggable={false} className="tl-container tl-theme__light" tabIndex={0}>
@ -124,18 +136,18 @@ export const TldrawEditor = memo(function TldrawEditor(props: TldrawEditorProps)
>
{container && (
<ContainerProvider container={container}>
<EditorComponentsProvider overrides={props.components}>
<EditorComponentsProvider overrides={components}>
{store ? (
store instanceof Store ? (
// Store is ready to go, whether externally synced or not
<TldrawEditorWithReadyStore {...rest} store={store} />
<TldrawEditorWithReadyStore {...withDefaults} store={store} />
) : (
// Store is a synced store, so handle syncing stages internally
<TldrawEditorWithLoadingStore {...rest} store={store} />
<TldrawEditorWithLoadingStore {...withDefaults} store={store} />
)
) : (
// We have no store (it's undefined) so create one and possibly sync it
<TldrawEditorWithOwnStore {...rest} store={store} />
<TldrawEditorWithOwnStore {...withDefaults} store={store} />
)}
</EditorComponentsProvider>
</ContainerProvider>
@ -145,11 +157,13 @@ export const TldrawEditor = memo(function TldrawEditor(props: TldrawEditorProps)
)
})
function TldrawEditorWithOwnStore(props: TldrawEditorProps & { store: undefined }) {
function TldrawEditorWithOwnStore(
props: Required<TldrawEditorProps & { store: undefined }, 'shapes' | 'tools'>
) {
const { defaultName, initialData, shapes, persistenceKey, sessionId } = props
const syncedStore = useLocalStore({
customShapes: shapes,
shapes,
initialData,
persistenceKey,
sessionId,
@ -163,7 +177,7 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
store,
assetUrls,
...rest
}: TldrawEditorProps & { store: TLStoreWithStatus }) {
}: Required<TldrawEditorProps & { store: TLStoreWithStatus }, 'shapes' | 'tools'>) {
const assets = useDefaultEditorAssetsWithOverrides(assetUrls)
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets)
@ -206,9 +220,12 @@ function TldrawEditorWithReadyStore({
tools,
shapes,
autoFocus,
}: TldrawEditorProps & {
store: TLStore
}) {
}: Required<
TldrawEditorProps & {
store: TLStore
},
'shapes' | 'tools'
>) {
const { ErrorFallback } = useEditorComponents()
const container = useContainer()
const [editor, setEditor] = useState<Editor | null>(null)

View file

@ -1,20 +1,13 @@
import { HistoryEntry, Migrations, Store, StoreSnapshot } from '@tldraw/store'
import { TLRecord, TLStore, createTLSchema } from '@tldraw/tlschema'
import { TLShapeUtilConstructor } from '../editor/shapeutils/ShapeUtil'
/** @public */
export type TLShapeInfo = {
util: TLShapeUtilConstructor<any>
migrations?: Migrations
validator?: { validate: (record: any) => any }
}
import { HistoryEntry, Store, StoreSchema, StoreSnapshot } from '@tldraw/store'
import { TLRecord, TLStore, TLStoreProps, createTLSchema } from '@tldraw/tlschema'
import { checkShapesAndAddCore } from './defaultShapes'
import { AnyTLShapeInfo, TLShapeInfo } from './defineShape'
/** @public */
export type TLStoreOptions = {
customShapes?: Record<string, TLShapeInfo>
initialData?: StoreSnapshot<TLRecord>
defaultName?: string
}
} & ({ shapes: readonly AnyTLShapeInfo[] } | { schema: StoreSchema<TLRecord, TLStoreProps> })
/** @public */
export type TLStoreEventInfo = HistoryEntry<TLRecord>
@ -25,14 +18,20 @@ export type TLStoreEventInfo = HistoryEntry<TLRecord>
* @param opts - Options for creating the store.
*
* @public */
export function createTLStore(opts = {} as TLStoreOptions): TLStore {
const { customShapes = {}, initialData, defaultName = '' } = opts
export function createTLStore({ initialData, defaultName = '', ...rest }: TLStoreOptions): TLStore {
const schema =
'schema' in rest
? rest.schema
: createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapes)) })
return new Store({
schema: createTLSchema({ customShapes }),
schema,
initialData,
props: {
defaultName,
},
})
}
function shapesArrayToShapeMap(shapes: TLShapeInfo[]) {
return Object.fromEntries(shapes.map((s) => [s.type, s]))
}

View file

@ -1,3 +1,31 @@
import {
arrowShapeMigrations,
arrowShapeProps,
bookmarkShapeMigrations,
bookmarkShapeProps,
drawShapeMigrations,
drawShapeProps,
embedShapeMigrations,
embedShapeProps,
frameShapeMigrations,
frameShapeProps,
geoShapeMigrations,
geoShapeProps,
groupShapeMigrations,
groupShapeProps,
highlightShapeMigrations,
highlightShapeProps,
imageShapeMigrations,
imageShapeProps,
lineShapeMigrations,
lineShapeProps,
noteShapeMigrations,
noteShapeProps,
textShapeMigrations,
textShapeProps,
videoShapeMigrations,
videoShapeProps,
} from '@tldraw/tlschema'
import { ArrowShapeUtil } from '../editor/shapeutils/ArrowShapeUtil/ArrowShapeUtil'
import { BookmarkShapeUtil } from '../editor/shapeutils/BookmarkShapeUtil/BookmarkShapeUtil'
import { DrawShapeUtil } from '../editor/shapeutils/DrawShapeUtil/DrawShapeUtil'
@ -11,57 +39,103 @@ import { LineShapeUtil } from '../editor/shapeutils/LineShapeUtil/LineShapeUtil'
import { NoteShapeUtil } from '../editor/shapeutils/NoteShapeUtil/NoteShapeUtil'
import { TextShapeUtil } from '../editor/shapeutils/TextShapeUtil/TextShapeUtil'
import { VideoShapeUtil } from '../editor/shapeutils/VideoShapeUtil/VideoShapeUtil'
import { TLShapeInfo } from './createTLStore'
import { AnyTLShapeInfo, TLShapeInfo, defineShape } from './defineShape'
/** @public */
export const coreShapes: Record<string, TLShapeInfo> = {
export const coreShapes = [
// created by grouping interactions, probably the corest core shape that we have
group: {
defineShape('group', {
util: GroupShapeUtil,
},
props: groupShapeProps,
migrations: groupShapeMigrations,
}),
// created by embed menu / url drop
embed: {
defineShape('embed', {
util: EmbedShapeUtil,
},
props: embedShapeProps,
migrations: embedShapeMigrations,
}),
// created by copy and paste / url drop
bookmark: {
defineShape('bookmark', {
util: BookmarkShapeUtil,
},
props: bookmarkShapeProps,
migrations: bookmarkShapeMigrations,
}),
// created by copy and paste / file drop
image: {
defineShape('image', {
util: ImageShapeUtil,
},
// created by copy and paste / file drop
video: {
util: VideoShapeUtil,
},
props: imageShapeProps,
migrations: imageShapeMigrations,
}),
// created by copy and paste
text: {
defineShape('text', {
util: TextShapeUtil,
},
}
props: textShapeProps,
migrations: textShapeMigrations,
}),
] as const
/** @public */
export const defaultShapes: Record<string, TLShapeInfo> = {
draw: {
export const defaultShapes = [
defineShape('draw', {
util: DrawShapeUtil,
},
geo: {
props: drawShapeProps,
migrations: drawShapeMigrations,
}),
defineShape('geo', {
util: GeoShapeUtil,
},
line: {
props: geoShapeProps,
migrations: geoShapeMigrations,
}),
defineShape('line', {
util: LineShapeUtil,
},
note: {
props: lineShapeProps,
migrations: lineShapeMigrations,
}),
defineShape('note', {
util: NoteShapeUtil,
},
frame: {
props: noteShapeProps,
migrations: noteShapeMigrations,
}),
defineShape('frame', {
util: FrameShapeUtil,
},
arrow: {
props: frameShapeProps,
migrations: frameShapeMigrations,
}),
defineShape('arrow', {
util: ArrowShapeUtil,
},
highlight: {
props: arrowShapeProps,
migrations: arrowShapeMigrations,
}),
defineShape('highlight', {
util: HighlightShapeUtil,
},
props: highlightShapeProps,
migrations: highlightShapeMigrations,
}),
defineShape('video', {
util: VideoShapeUtil,
props: videoShapeProps,
migrations: videoShapeMigrations,
}),
] as const
const coreShapeTypes = new Set<string>(coreShapes.map((s) => s.type))
export function checkShapesAndAddCore(customShapes: readonly TLShapeInfo[]) {
const shapes: AnyTLShapeInfo[] = [...coreShapes]
const addedCustomShapeTypes = new Set<string>()
for (const customShape of customShapes) {
if (coreShapeTypes.has(customShape.type)) {
throw new Error(
`Shape type "${customShape.type}" is a core shapes type and cannot be overridden`
)
}
if (addedCustomShapeTypes.has(customShape.type)) {
throw new Error(`Shape type "${customShape.type}" is defined more than once`)
}
shapes.push(customShape)
addedCustomShapeTypes.add(customShape.type)
}
return shapes
}

View file

@ -0,0 +1,26 @@
import { Migrations } from '@tldraw/store'
import { ShapeProps, TLBaseShape, TLUnknownShape } from '@tldraw/tlschema'
import { assert } from '@tldraw/utils'
import { TLShapeUtilConstructor } from '../editor/shapeutils/ShapeUtil'
/** @public */
export type TLShapeInfo<T extends TLUnknownShape = TLUnknownShape> = {
type: T['type']
util: TLShapeUtilConstructor<T>
props?: ShapeProps<T>
migrations?: Migrations
}
export type AnyTLShapeInfo = TLShapeInfo<TLBaseShape<any, any>>
/** @public */
export function defineShape<T extends TLUnknownShape>(
type: T['type'],
opts: Omit<TLShapeInfo<T>, 'type'>
): TLShapeInfo<T> {
assert(
type === opts.util.type,
`Shape type "${type}" does not match util type "${opts.util.type}"`
)
return { type, ...opts }
}

View file

@ -66,9 +66,11 @@ import {
} from '@tldraw/tlschema'
import {
annotateError,
assert,
compact,
dedupe,
deepCopy,
getOwnProperty,
partition,
sortById,
structuredClone,
@ -76,10 +78,9 @@ import {
import { EventEmitter } from 'eventemitter3'
import { nanoid } from 'nanoid'
import { EMPTY_ARRAY, atom, computed, transact } from 'signia'
import { TLShapeInfo } from '../config/createTLStore'
import { TLUser, createTLUser } from '../config/createTLUser'
import { coreShapes, defaultShapes } from '../config/defaultShapes'
import { defaultTools } from '../config/defaultTools'
import { checkShapesAndAddCore } from '../config/defaultShapes'
import { AnyTLShapeInfo } from '../config/defineShape'
import {
ANIMATION_MEDIUM_MS,
BLACKLISTED_PROPS,
@ -164,11 +165,11 @@ export interface TLEditorOptions {
/**
* An array of shapes to use in the editor. These will be used to create and manage shapes in the editor.
*/
shapes?: Record<string, TLShapeInfo>
shapes: readonly AnyTLShapeInfo[]
/**
* An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor.
*/
tools?: TLStateNodeConstructor[]
tools: readonly TLStateNodeConstructor[]
/**
* A user defined externally to replace the default user.
*/
@ -182,13 +183,7 @@ export interface TLEditorOptions {
/** @public */
export class Editor extends EventEmitter<TLEventMap> {
constructor({
store,
user,
tools = defaultTools,
shapes = defaultShapes,
getContainer,
}: TLEditorOptions) {
constructor({ store, user, shapes, tools, getContainer }: TLEditorOptions) {
super()
this.store = store
@ -201,22 +196,29 @@ export class Editor extends EventEmitter<TLEventMap> {
this.root = new RootState(this)
// Shapes.
// Accept shapes from constructor parameters which may not conflict with the root note's core tools.
const shapeUtils = Object.fromEntries(
Object.values(coreShapes).map(({ util: Util }) => [Util.type, new Util(this, Util.type)])
)
const allShapes = checkShapesAndAddCore(shapes)
for (const [type, { util: Util }] of Object.entries(shapes)) {
if (shapeUtils[type]) {
throw Error(`May not overwrite core shape of type "${type}".`)
const shapeTypesInSchema = new Set(
Object.keys(store.schema.types.shape.migrations.subTypeMigrations!)
)
for (const shape of allShapes) {
if (!shapeTypesInSchema.has(shape.type)) {
throw Error(
`Editor and store have different shapes: "${shape.type}" was passed into the editor but not the schema`
)
}
if (type !== Util.type) {
throw Error(`Shape util's type "${Util.type}" does not match provided type "${type}".`)
}
shapeUtils[type] = new Util(this, Util.type)
shapeTypesInSchema.delete(shape.type)
}
this.shapeUtils = shapeUtils
if (shapeTypesInSchema.size > 0) {
throw Error(
`Editor and store have different shapes: "${
[...shapeTypesInSchema][0]
}" is present in the store schema but not provided to the editor`
)
}
this.shapeUtils = Object.fromEntries(
allShapes.map(({ util: Util }) => [Util.type, new Util(this, Util.type)])
)
// Tools.
// Accept tools from constructor parameters which may not conflict with the root note's default or
@ -976,12 +978,24 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
getShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): ShapeUtil<S>
getShapeUtil<T extends ShapeUtil>({
type,
}: {
getShapeUtil<T extends ShapeUtil>(shapeUtilConstructor: {
type: T extends ShapeUtil<infer R> ? R['type'] : string
}): T {
return this.shapeUtils[type] as T
const shapeUtil = getOwnProperty(this.shapeUtils, shapeUtilConstructor.type) as T | undefined
assert(shapeUtil, `No shape util found for type "${shapeUtilConstructor.type}"`)
// does shapeUtilConstructor extends ShapeUtil?
if (
'prototype' in shapeUtilConstructor &&
shapeUtilConstructor.prototype instanceof ShapeUtil
) {
assert(
shapeUtil instanceof (shapeUtilConstructor as any),
`Shape util found for type "${shapeUtilConstructor.type}" is not an instance of the provided constructor`
)
}
return shapeUtil as T
}
/**

View file

@ -57,7 +57,7 @@ let globalRenderIndex = 0
/** @public */
export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
static override type = 'arrow'
static override type = 'arrow' as const
override canEdit = () => true
override canBind = () => false

View file

@ -18,7 +18,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
/** @public */
export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
static override type = 'bookmark'
static override type = 'bookmark' as const
override canResize = () => false

View file

@ -22,7 +22,7 @@ import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments
/** @public */
export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
static override type = 'draw'
static override type = 'draw' as const
hideResizeHandles = (shape: TLDrawShape) => getIsDot(shape)
hideRotateHandle = (shape: TLDrawShape) => getIsDot(shape)

View file

@ -27,7 +27,7 @@ const getSandboxPermissions = (permissions: TLEmbedShapePermissions) => {
/** @public */
export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
static override type = 'embed'
static override type = 'embed' as const
override canUnmount: TLShapeUtilFlag<TLEmbedShape> = () => false
override canResize = (shape: TLEmbedShape) => {

View file

@ -11,7 +11,7 @@ import { FrameHeading } from './components/FrameHeading'
/** @public */
export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
static override type = 'frame'
static override type = 'frame' as const
override canBind = () => true

View file

@ -41,7 +41,7 @@ const MIN_SIZE_WITH_LABEL = 17 * 3
/** @public */
export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
static override type = 'geo'
static override type = 'geo' as const
canEdit = () => true

View file

@ -6,7 +6,7 @@ import { DashedOutlineBox } from '../shared/DashedOutlineBox'
/** @public */
export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
static override type = 'group'
static override type = 'group' as const
type = 'group' as const

View file

@ -15,7 +15,7 @@ const UNDERLAY_OPACITY = 0.82
/** @public */
export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
static type = 'highlight'
static type = 'highlight' as const
hideResizeHandles = (shape: TLHighlightShape) => getIsDot(shape)
hideRotateHandle = (shape: TLHighlightShape) => getIsDot(shape)

View file

@ -49,7 +49,7 @@ async function getDataURIFromURL(url: string): Promise<string> {
/** @public */
export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
static override type = 'image'
static override type = 'image' as const
override isAspectRatioLocked = () => true
override canCrop = () => true

View file

@ -26,7 +26,7 @@ const handlesCache = new WeakMapCache<TLLineShape['props'], TLHandle[]>()
/** @public */
export class LineShapeUtil extends ShapeUtil<TLLineShape> {
static override type = 'line'
static override type = 'line' as const
override hideResizeHandles = () => true
override hideRotateHandle = () => true

View file

@ -12,7 +12,7 @@ const NOTE_SIZE = 200
/** @public */
export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
static override type = 'note'
static override type = 'note' as const
canEdit = () => true
hideResizeHandles = () => true

View file

@ -18,7 +18,7 @@ const sizeCache = new WeakMapCache<TLTextShape['props'], { height: number; width
/** @public */
export class TextShapeUtil extends ShapeUtil<TLTextShape> {
static override type = 'text'
static override type = 'text' as const
canEdit = () => true

View file

@ -11,7 +11,7 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
/** @public */
export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
static override type = 'video'
static override type = 'video' as const
override canEdit = () => true
override isAspectRatioLocked = () => true

View file

@ -6,11 +6,11 @@ import { TLLocalSyncClient } from '../utils/sync/TLLocalSyncClient'
import { useTLStore } from './useTLStore'
/** @internal */
export function useLocalStore(
opts = {} as { persistenceKey?: string; sessionId?: string } & TLStoreOptions
): TLStoreWithStatus {
const { persistenceKey, sessionId, ...rest } = opts
export function useLocalStore({
persistenceKey,
sessionId,
...rest
}: { persistenceKey?: string; sessionId?: string } & TLStoreOptions): TLStoreWithStatus {
const [state, setState] = useState<{ id: string; storeWithStatus: TLStoreWithStatus } | null>(
null
)

View file

@ -1,5 +1,7 @@
import { PageRecordType, createShapeId } from '@tldraw/tlschema'
import { PageRecordType, TLShape, createShapeId } from '@tldraw/tlschema'
import { structuredClone } from '@tldraw/utils'
import { BaseBoxShapeUtil } from '../editor/shapeutils/BaseBoxShapeUtil'
import { GeoShapeUtil } from '../editor/shapeutils/GeoShapeUtil/GeoShapeUtil'
import { TestEditor } from './TestEditor'
import { TL } from './jsx'
@ -441,3 +443,60 @@ describe('isFocused', () => {
expect(blurMock).toHaveBeenCalled()
})
})
describe('getShapeUtil', () => {
it('accepts shapes', () => {
const geoShape = editor.getShapeById(ids.box1)!
const geoUtil = editor.getShapeUtil(geoShape)
expect(geoUtil).toBeInstanceOf(GeoShapeUtil)
})
it('accepts shape utils', () => {
const geoUtil = editor.getShapeUtil(GeoShapeUtil)
expect(geoUtil).toBeInstanceOf(GeoShapeUtil)
})
it('throws if that shape type isnt registered', () => {
const myFakeShape = { type: 'fake' } as TLShape
expect(() => editor.getShapeUtil(myFakeShape)).toThrowErrorMatchingInlineSnapshot(
`"No shape util found for type \\"fake\\""`
)
class MyFakeShapeUtil extends BaseBoxShapeUtil<any> {
static type = 'fake'
defaultProps() {
throw new Error('Method not implemented.')
}
render() {
throw new Error('Method not implemented.')
}
indicator() {
throw new Error('Method not implemented.')
}
}
expect(() => editor.getShapeUtil(MyFakeShapeUtil)).toThrowErrorMatchingInlineSnapshot(
`"No shape util found for type \\"fake\\""`
)
})
it("throws if a shape util that isn't the one registered is passed in", () => {
class MyFakeGeoShapeUtil extends BaseBoxShapeUtil<any> {
static type = 'geo'
defaultProps() {
throw new Error('Method not implemented.')
}
render() {
throw new Error('Method not implemented.')
}
indicator() {
throw new Error('Method not implemented.')
}
}
expect(() => editor.getShapeUtil(MyFakeGeoShapeUtil)).toThrowErrorMatchingInlineSnapshot(
`"Shape util found for type \\"geo\\" is not an instance of the provided constructor"`
)
})
})

View file

@ -55,16 +55,14 @@ declare global {
export const TEST_INSTANCE_ID = InstanceRecordType.createId('testInstance1')
export class TestEditor extends Editor {
constructor(options = {} as Partial<Omit<TLEditorOptions, 'store'>>) {
constructor(options: Partial<Omit<TLEditorOptions, 'store'>> = {}) {
const elm = document.createElement('div')
const { shapes = {}, tools = [] } = options
const { shapes = defaultShapes, tools = [] } = options
elm.tabIndex = 0
super({
shapes: { ...defaultShapes, ...shapes },
shapes,
tools: [...defaultTools, ...tools],
store: createTLStore({
customShapes: shapes,
}),
store: createTLStore({ shapes }),
getContainer: () => elm,
...options,
})

View file

@ -1,9 +1,13 @@
import { act, render, screen } from '@testing-library/react'
import { TLBaseShape, createShapeId } from '@tldraw/tlschema'
import { noop } from '@tldraw/utils'
import { TldrawEditor } from '../TldrawEditor'
import { Canvas } from '../components/Canvas'
import { HTMLContainer } from '../components/HTMLContainer'
import { createTLStore } from '../config/createTLStore'
import { defaultShapes } from '../config/defaultShapes'
import { defaultTools } from '../config/defaultTools'
import { defineShape } from '../config/defineShape'
import { Editor } from '../editor/Editor'
import { BaseBoxShapeUtil } from '../editor/shapeutils/BaseBoxShapeUtil'
import { BaseBoxShapeTool } from '../editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
@ -23,56 +27,135 @@ afterEach(() => {
window.fetch = originalFetch
})
function checkAllShapes(editor: Editor, shapes: string[]) {
expect(Object.keys(editor!.store.schema.types.shape.migrations.subTypeMigrations!)).toStrictEqual(
shapes
)
expect(Object.keys(editor!.shapeUtils)).toStrictEqual(shapes)
}
describe('<TldrawEditor />', () => {
it('Renders without crashing', async () => {
await act(async () => (
<TldrawEditor autoFocus>
render(
<TldrawEditor autoFocus components={{ ErrorFallback: null }}>
<div data-testid="canvas-1" />
</TldrawEditor>
))
})
it('Creates its own store', async () => {
let store: any
render(
await act(async () => (
<TldrawEditor
onMount={(editor) => {
store = editor.store
}}
autoFocus
>
<div data-testid="canvas-1" />
</TldrawEditor>
))
)
await screen.findByTestId('canvas-1')
expect(store).toBeTruthy()
})
it('Creates its own store with core shapes', async () => {
let editor: Editor
render(
<TldrawEditor
components={{ ErrorFallback: null }}
onMount={(e) => {
editor = e
}}
autoFocus
>
<div data-testid="canvas-1" />
</TldrawEditor>
)
await screen.findByTestId('canvas-1')
checkAllShapes(editor!, ['group', 'embed', 'bookmark', 'image', 'text'])
})
it('Can be created with default shapes', async () => {
let editor: Editor
render(
<TldrawEditor
components={{ ErrorFallback: null }}
shapes={defaultShapes}
onMount={(e) => {
editor = e
}}
autoFocus
>
<div data-testid="canvas-1" />
</TldrawEditor>
)
await screen.findByTestId('canvas-1')
expect(editor!).toBeTruthy()
checkAllShapes(editor!, [
'group',
'embed',
'bookmark',
'image',
'text',
'draw',
'geo',
'line',
'note',
'frame',
'arrow',
'highlight',
'video',
])
})
it('Renders with an external store', async () => {
const store = createTLStore()
const store = createTLStore({ shapes: [] })
render(
await act(async () => (
<TldrawEditor
store={store}
onMount={(editor) => {
expect(editor.store).toBe(store)
}}
autoFocus
>
<div data-testid="canvas-1" />
</TldrawEditor>
))
<TldrawEditor
components={{ ErrorFallback: null }}
store={store}
onMount={(editor) => {
expect(editor.store).toBe(store)
}}
autoFocus
>
<div data-testid="canvas-1" />
</TldrawEditor>
)
await screen.findByTestId('canvas-1')
})
it('throws if the store has different shapes to the ones passed in', async () => {
const spy = jest.spyOn(console, 'error').mockImplementation(noop)
expect(() =>
render(
<TldrawEditor
shapes={defaultShapes}
components={{ ErrorFallback: null }}
store={createTLStore({ shapes: [] })}
autoFocus
>
<div data-testid="canvas-1" />
</TldrawEditor>
)
).toThrowErrorMatchingInlineSnapshot(
`"Editor and store have different shapes: \\"draw\\" was passed into the editor but not the schema"`
)
expect(() =>
render(
<TldrawEditor
components={{ ErrorFallback: null }}
store={createTLStore({ shapes: defaultShapes })}
autoFocus
>
<div data-testid="canvas-1" />
</TldrawEditor>
)
).toThrowErrorMatchingInlineSnapshot(
`"Editor and store have different shapes: \\"draw\\" is present in the store schema but not provided to the editor"`
)
spy.mockRestore()
})
it('Accepts fresh versions of store and calls `onMount` for each one', async () => {
const initialStore = createTLStore({})
const initialStore = createTLStore({ shapes: [] })
const onMount = jest.fn()
const rendered = render(
<TldrawEditor store={initialStore} onMount={onMount} autoFocus>
<TldrawEditor
components={{ ErrorFallback: null }}
store={initialStore}
onMount={onMount}
autoFocus
>
<div data-testid="canvas-1" />
</TldrawEditor>
)
@ -82,7 +165,12 @@ describe('<TldrawEditor />', () => {
expect(initialEditor.store).toBe(initialStore)
// re-render with the same store:
rendered.rerender(
<TldrawEditor store={initialStore} onMount={onMount} autoFocus>
<TldrawEditor
components={{ ErrorFallback: null }}
store={initialStore}
onMount={onMount}
autoFocus
>
<div data-testid="canvas-2" />
</TldrawEditor>
)
@ -90,9 +178,14 @@ describe('<TldrawEditor />', () => {
// not called again:
expect(onMount).toHaveBeenCalledTimes(1)
// re-render with a new store:
const newStore = createTLStore({})
const newStore = createTLStore({ shapes: [] })
rendered.rerender(
<TldrawEditor store={newStore} onMount={onMount} autoFocus>
<TldrawEditor
components={{ ErrorFallback: null }}
store={newStore}
onMount={onMount}
autoFocus
>
<div data-testid="canvas-3" />
</TldrawEditor>
)
@ -105,17 +198,18 @@ describe('<TldrawEditor />', () => {
it('Renders the canvas and shapes', async () => {
let editor = {} as Editor
render(
await act(async () => (
<TldrawEditor
autoFocus
onMount={(editorApp) => {
editor = editorApp
}}
>
<Canvas />
<div data-testid="canvas-1" />
</TldrawEditor>
))
<TldrawEditor
components={{ ErrorFallback: null }}
shapes={defaultShapes}
tools={defaultTools}
autoFocus
onMount={(editorApp) => {
editor = editorApp
}}
>
<Canvas />
<div data-testid="canvas-1" />
</TldrawEditor>
)
await screen.findByTestId('canvas-1')
@ -220,24 +314,23 @@ describe('Custom shapes', () => {
}
const tools = [CardTool]
const shapes = { card: { util: CardUtil } }
const shapes = [defineShape('card', { util: CardUtil })]
it('Uses custom shapes', async () => {
let editor = {} as Editor
render(
await act(async () => (
<TldrawEditor
shapes={shapes}
tools={tools}
autoFocus
onMount={(editorApp) => {
editor = editorApp
}}
>
<Canvas />
<div data-testid="canvas-1" />
</TldrawEditor>
))
<TldrawEditor
components={{ ErrorFallback: null }}
shapes={shapes}
tools={tools}
autoFocus
onMount={(editorApp) => {
editor = editorApp
}}
>
<Canvas />
<div data-testid="canvas-1" />
</TldrawEditor>
)
await screen.findByTestId('canvas-1')
@ -247,6 +340,7 @@ describe('Custom shapes', () => {
})
expect(editor.shapeUtils.card).toBeTruthy()
checkAllShapes(editor, ['group', 'embed', 'bookmark', 'image', 'text', 'card'])
const id = createShapeId()

View file

@ -5,6 +5,7 @@ import { ShapeUtil } from '../../editor/shapeutils/ShapeUtil'
import { TestEditor } from '../TestEditor'
import { defaultShapes } from '../../config/defaultShapes'
import { defineShape } from '../../config/defineShape'
import { getSnapLines } from '../testutils/getSnapLines'
type __TopLeftSnapOnlyShape = any
@ -40,6 +41,10 @@ class __TopLeftSnapOnlyShapeUtil extends ShapeUtil<__TopLeftSnapOnlyShape> {
}
}
const __TopLeftSnapOnlyShape = defineShape('__test_top_left_snap_only', {
util: __TopLeftSnapOnlyShapeUtil,
})
let editor: TestEditor
afterEach(() => {
@ -753,12 +758,7 @@ describe('custom snapping points', () => {
beforeEach(() => {
editor?.dispose()
editor = new TestEditor({
shapes: {
...defaultShapes,
__test_top_left_snap_only: {
util: __TopLeftSnapOnlyShapeUtil,
},
},
shapes: [...defaultShapes, __TopLeftSnapOnlyShape],
// x───────┐
// │ T │
// │ │

View file

@ -1,6 +1,7 @@
import { PageRecordType } from '@tldraw/tlschema'
import { promiseWithResolve } from '@tldraw/utils'
import { createTLStore } from '../../config/createTLStore'
import { defaultShapes } from '../../config/defaultShapes'
import { TLLocalSyncClient } from './TLLocalSyncClient'
import * as idb from './indexedDb'
@ -24,7 +25,7 @@ class BroadcastChannelMock {
}
function testClient(channel = new BroadcastChannelMock('test')) {
const store = createTLStore()
const store = createTLStore({ shapes: defaultShapes })
const onLoad = jest.fn(() => {
return
})

View file

@ -15,7 +15,7 @@ const clearAll = async () => {
beforeEach(async () => {
await clearAll()
})
const schema = createTLSchema()
const schema = createTLSchema({ shapes: {} })
describe('storeSnapshotInIndexedDb', () => {
it("creates documents if they don't exist", async () => {
await storeSnapshotInIndexedDb({

View file

@ -8,6 +8,7 @@ import { Editor } from '@tldraw/editor';
import { MigrationFailureReason } from '@tldraw/store';
import { Result } from '@tldraw/utils';
import { SerializedSchema } from '@tldraw/store';
import { TLSchema } from '@tldraw/editor';
import { TLStore } from '@tldraw/editor';
import { TLUiToastsContextType } from '@tldraw/ui';
import { TLUiTranslationKey } from '@tldraw/ui';
@ -39,8 +40,8 @@ export interface LegacyTldrawDocument {
export function parseAndLoadDocument(editor: Editor, document: string, msg: (id: TLUiTranslationKey) => string, addToast: TLUiToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise<void>;
// @public (undocumented)
export function parseTldrawJsonFile({ json, store, }: {
store: TLStore;
export function parseTldrawJsonFile({ json, schema, }: {
schema: TLSchema;
json: string;
}): Result<TLStore, TldrawFileParseError>;

View file

@ -5,6 +5,7 @@ import {
TLAsset,
TLAssetId,
TLRecord,
TLSchema,
TLStore,
} from '@tldraw/editor'
import {
@ -82,9 +83,9 @@ export type TldrawFileParseError =
/** @public */
export function parseTldrawJsonFile({
json,
store,
schema,
}: {
store: TLStore
schema: TLSchema
json: string
}): Result<TLStore, TldrawFileParseError> {
// first off, we parse .json file and check it matches the general shape of
@ -121,7 +122,7 @@ export function parseTldrawJsonFile({
let migrationResult: MigrationResult<StoreSnapshot<TLRecord>>
try {
const storeSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r as TLRecord]))
migrationResult = store.schema.migrateStoreSnapshot(storeSnapshot, data.schema)
migrationResult = schema.migrateStoreSnapshot(storeSnapshot, data.schema)
} catch (e) {
// junk data in the migration
return Result.err({ type: 'invalidRecords', cause: e })
@ -138,6 +139,7 @@ export function parseTldrawJsonFile({
return Result.ok(
createTLStore({
initialData: migrationResult.value,
schema,
})
)
} catch (e) {
@ -216,7 +218,7 @@ export async function parseAndLoadDocument(
forceDarkMode?: boolean
) {
const parseFileResult = parseTldrawJsonFile({
store: createTLStore(),
schema: editor.store.schema,
json: document,
})
if (!parseFileResult.ok) {

View file

@ -1,13 +1,11 @@
import { createShapeId, createTLStore, TLStore } from '@tldraw/editor'
import { createShapeId, createTLStore, defaultShapes, TLStore } from '@tldraw/editor'
import { MigrationFailureReason, UnknownRecord } from '@tldraw/store'
import { assert } from '@tldraw/utils'
import { parseTldrawJsonFile as _parseTldrawJsonFile, TldrawFile } from '../lib/file'
const parseTldrawJsonFile = (store: TLStore, json: string) =>
_parseTldrawJsonFile({
store,
json,
})
const schema = createTLStore({ shapes: defaultShapes }).schema
const parseTldrawJsonFile = (store: TLStore, json: string) => _parseTldrawJsonFile({ schema, json })
function serialize(file: TldrawFile): string {
return JSON.stringify(file)
@ -15,21 +13,21 @@ function serialize(file: TldrawFile): string {
describe('parseTldrawJsonFile', () => {
it('returns an error if the file is not json', () => {
const store = createTLStore()
const store = createTLStore({ shapes: defaultShapes })
const result = parseTldrawJsonFile(store, 'not json')
assert(!result.ok)
expect(result.error.type).toBe('notATldrawFile')
})
it("returns an error if the file doesn't look like a tldraw file", () => {
const store = createTLStore()
const store = createTLStore({ shapes: defaultShapes })
const result = parseTldrawJsonFile(store, JSON.stringify({ not: 'a tldraw file' }))
assert(!result.ok)
expect(result.error.type).toBe('notATldrawFile')
})
it('returns an error if the file version is too old', () => {
const store = createTLStore()
const store = createTLStore({ shapes: defaultShapes })
const result = parseTldrawJsonFile(
store,
serialize({
@ -43,7 +41,7 @@ describe('parseTldrawJsonFile', () => {
})
it('returns an error if the file version is too new', () => {
const store = createTLStore()
const store = createTLStore({ shapes: defaultShapes })
const result = parseTldrawJsonFile(
store,
serialize({
@ -57,7 +55,7 @@ describe('parseTldrawJsonFile', () => {
})
it('returns an error if migrations fail', () => {
const store = createTLStore()
const store = createTLStore({ shapes: defaultShapes })
const serializedSchema = store.schema.serialize()
serializedSchema.storeVersion = 100
const result = parseTldrawJsonFile(
@ -72,7 +70,7 @@ describe('parseTldrawJsonFile', () => {
assert(result.error.type === 'migrationFailed')
expect(result.error.reason).toBe(MigrationFailureReason.TargetVersionTooOld)
const store2 = createTLStore()
const store2 = createTLStore({ shapes: defaultShapes })
const serializedSchema2 = store2.schema.serialize()
serializedSchema2.recordVersions.shape.version = 100
const result2 = parseTldrawJsonFile(
@ -90,7 +88,7 @@ describe('parseTldrawJsonFile', () => {
})
it('returns an error if a record is invalid', () => {
const store = createTLStore()
const store = createTLStore({ shapes: defaultShapes })
const result = parseTldrawJsonFile(
store,
serialize({
@ -115,7 +113,7 @@ describe('parseTldrawJsonFile', () => {
})
it('returns a store if the file is valid', () => {
const store = createTLStore()
const store = createTLStore({ shapes: defaultShapes })
const result = parseTldrawJsonFile(
store,
serialize({

View file

@ -1,5 +1,5 @@
import { act } from '@testing-library/react'
import { TldrawEditor } from '@tldraw/editor'
import { Tldraw } from './Tldraw'
let originalFetch: typeof window.fetch
beforeEach(() => {
@ -20,9 +20,9 @@ afterEach(() => {
describe('<Tldraw />', () => {
it('Renders without crashing', async () => {
await act(async () => (
<TldrawEditor autoFocus>
<Tldraw autoFocus>
<div data-testid="canvas-1" />
</TldrawEditor>
</Tldraw>
))
})
})

View file

@ -1,13 +1,26 @@
import { Canvas, TldrawEditor, TldrawEditorProps } from '@tldraw/editor'
import {
Canvas,
TldrawEditor,
TldrawEditorProps,
defaultShapes,
defaultTools,
} from '@tldraw/editor'
import { ContextMenu, TldrawUi, TldrawUiProps } from '@tldraw/ui'
import { useMemo } from 'react'
/** @public */
export function Tldraw(props: TldrawEditorProps & TldrawUiProps) {
const { children, ...rest } = props
const withDefaults = {
...rest,
shapes: useMemo(() => [...defaultShapes, ...(rest.shapes ?? [])], [rest.shapes]),
tools: useMemo(() => [...defaultTools, ...(rest.tools ?? [])], [rest.tools]),
}
return (
<TldrawEditor {...rest}>
<TldrawUi {...rest}>
<TldrawEditor {...withDefaults}>
<TldrawUi {...withDefaults}>
<ContextMenu>
<Canvas />
</ContextMenu>

View file

@ -18,6 +18,12 @@ import { UnknownRecord } from '@tldraw/store';
// @internal (undocumented)
export const alignValidator: T.Validator<"end" | "middle" | "start">;
// @internal (undocumented)
export const arrowShapeMigrations: Migrations;
// @internal (undocumented)
export const arrowShapeProps: ShapeProps<TLArrowShape>;
// @public
export const assetIdValidator: T.Validator<TLAssetId>;
@ -30,6 +36,12 @@ export const AssetRecordType: RecordType<TLAsset, "props" | "type">;
// @internal (undocumented)
export const assetValidator: T.Validator<TLAsset>;
// @internal (undocumented)
export const bookmarkShapeMigrations: Migrations;
// @internal (undocumented)
export const bookmarkShapeProps: ShapeProps<TLBookmarkShape>;
// @public
export interface Box2dModel {
// (undocumented)
@ -45,12 +57,12 @@ export interface Box2dModel {
// @public (undocumented)
export const CameraRecordType: RecordType<TLCamera, never>;
// @public
export const canvasUiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
// @internal (undocumented)
export function CLIENT_FIXUP_SCRIPT(persistedStore: StoreSnapshot<TLRecord>): StoreSnapshot<TLRecord>;
// @public
export const colorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
// @internal (undocumented)
export const colorValidator: T.Validator<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
@ -73,7 +85,9 @@ export const createPresenceStateDerivation: ($user: Signal<{
export function createShapeId(id?: string): TLShapeId;
// @public (undocumented)
export function createShapeValidator<Type extends string, Props extends object>(type: Type, props: T.Validator<Props>): T.ObjectValidator<{
export function createShapeValidator<Type extends string, Props extends object>(type: Type, props?: {
[K in keyof Props]: T.Validatable<Props[K]>;
}): T.ObjectValidator<{
id: TLShapeId;
typeName: "shape";
x: number;
@ -84,13 +98,13 @@ export function createShapeValidator<Type extends string, Props extends object>(
type: Type;
isLocked: boolean;
opacity: number;
props: Props;
props: Props | Record<string, unknown>;
}>;
// @public
export function createTLSchema(opts?: {
customShapes: Record<string, SchemaShapeInfo>;
}): StoreSchema<TLRecord, TLStoreProps>;
export function createTLSchema({ shapes }: {
shapes: Record<string, SchemaShapeInfo>;
}): TLSchema;
// @internal (undocumented)
export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">;
@ -98,6 +112,12 @@ export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">;
// @public (undocumented)
export const DocumentRecordType: RecordType<TLDocument, never>;
// @internal (undocumented)
export const drawShapeMigrations: Migrations;
// @internal (undocumented)
export const drawShapeProps: ShapeProps<TLDrawShape>;
// @public (undocumented)
export const EMBED_DEFINITIONS: readonly [{
readonly type: "tldraw";
@ -285,6 +305,9 @@ export type EmbedDefinition = {
readonly fromEmbedUrl: (url: string) => string | undefined;
};
// @internal (undocumented)
export const embedShapeMigrations: Migrations;
// @public
export const embedShapePermissionDefaults: {
readonly 'allow-downloads-without-user-activation': false;
@ -303,6 +326,9 @@ export const embedShapePermissionDefaults: {
readonly 'allow-forms': true;
};
// @internal (undocumented)
export const embedShapeProps: ShapeProps<TLEmbedShape>;
// @internal (undocumented)
export const fillValidator: T.Validator<"none" | "pattern" | "semi" | "solid">;
@ -315,18 +341,54 @@ export function fixupRecord(oldRecord: TLRecord): {
// @internal (undocumented)
export const fontValidator: T.Validator<"draw" | "mono" | "sans" | "serif">;
// @internal (undocumented)
export const frameShapeMigrations: Migrations;
// @internal (undocumented)
export const frameShapeProps: ShapeProps<TLFrameShape>;
// @internal (undocumented)
export const geoShapeMigrations: Migrations;
// @internal (undocumented)
export const geoShapeProps: ShapeProps<TLGeoShape>;
// @internal (undocumented)
export const geoValidator: T.Validator<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
// @public (undocumented)
export function getDefaultTranslationLocale(): TLLanguage['locale'];
// @internal (undocumented)
export const groupShapeMigrations: Migrations;
// @internal (undocumented)
export const groupShapeProps: ShapeProps<TLGroupShape>;
// @internal (undocumented)
export const highlightShapeMigrations: Migrations;
// @internal (undocumented)
export const highlightShapeProps: ShapeProps<TLHighlightShape>;
// @internal (undocumented)
export const iconShapeMigrations: Migrations;
// @internal (undocumented)
export const iconShapeProps: ShapeProps<TLIconShape>;
// @internal (undocumented)
export const iconValidator: T.Validator<"activity" | "airplay" | "alert-circle" | "alert-octagon" | "alert-triangle" | "align-center" | "align-justify" | "align-left" | "align-right" | "anchor" | "aperture" | "archive" | "arrow-down-circle" | "arrow-down-left" | "arrow-down-right" | "arrow-down" | "arrow-left-circle" | "arrow-left" | "arrow-right-circle" | "arrow-right" | "arrow-up-circle" | "arrow-up-left" | "arrow-up-right" | "arrow-up" | "at-sign" | "award" | "bar-chart-2" | "bar-chart" | "battery-charging" | "battery" | "bell-off" | "bell" | "bluetooth" | "bold" | "book-open" | "book" | "bookmark" | "briefcase" | "calendar" | "camera-off" | "camera" | "cast" | "check-circle" | "check-square" | "check" | "chevron-down" | "chevron-left" | "chevron-right" | "chevron-up" | "chevrons-down" | "chevrons-left" | "chevrons-right" | "chevrons-up" | "chrome" | "circle" | "clipboard" | "clock" | "cloud-drizzle" | "cloud-lightning" | "cloud-off" | "cloud-rain" | "cloud-snow" | "cloud" | "codepen" | "codesandbox" | "coffee" | "columns" | "command" | "compass" | "copy" | "corner-down-left" | "corner-down-right" | "corner-left-down" | "corner-left-up" | "corner-right-down" | "corner-right-up" | "corner-up-left" | "corner-up-right" | "cpu" | "credit-card" | "crop" | "crosshair" | "database" | "delete" | "disc" | "divide-circle" | "divide-square" | "divide" | "dollar-sign" | "download-cloud" | "download" | "dribbble" | "droplet" | "edit-2" | "edit-3" | "edit" | "external-link" | "eye-off" | "eye" | "facebook" | "fast-forward" | "feather" | "figma" | "file-minus" | "file-plus" | "file-text" | "file" | "film" | "filter" | "flag" | "folder-minus" | "folder-plus" | "folder" | "framer" | "frown" | "geo" | "gift" | "git-branch" | "git-commit" | "git-merge" | "git-pull-request" | "github" | "gitlab" | "globe" | "grid" | "hard-drive" | "hash" | "headphones" | "heart" | "help-circle" | "hexagon" | "home" | "image" | "inbox" | "info" | "instagram" | "italic" | "key" | "layers" | "layout" | "life-buoy" | "link-2" | "link" | "linkedin" | "list" | "loader" | "lock" | "log-in" | "log-out" | "mail" | "map-pin" | "map" | "maximize-2" | "maximize" | "meh" | "menu" | "message-circle" | "message-square" | "mic-off" | "mic" | "minimize-2" | "minimize" | "minus-circle" | "minus-square" | "minus" | "monitor" | "moon" | "more-horizontal" | "more-vertical" | "mouse-pointer" | "move" | "music" | "navigation-2" | "navigation" | "octagon" | "package" | "paperclip" | "pause-circle" | "pause" | "pen-tool" | "percent" | "phone-call" | "phone-forwarded" | "phone-incoming" | "phone-missed" | "phone-off" | "phone-outgoing" | "phone" | "pie-chart" | "play-circle" | "play" | "plus-circle" | "plus-square" | "plus" | "pocket" | "power" | "printer" | "radio" | "refresh-ccw" | "refresh-cw" | "repeat" | "rewind" | "rotate-ccw" | "rotate-cw" | "rss" | "save" | "scissors" | "search" | "send" | "server" | "settings" | "share-2" | "share" | "shield-off" | "shield" | "shopping-bag" | "shopping-cart" | "shuffle" | "sidebar" | "skip-back" | "skip-forward" | "slack" | "slash" | "sliders" | "smartphone" | "smile" | "speaker" | "square" | "star" | "stop-circle" | "sun" | "sunrise" | "sunset" | "table" | "tablet" | "tag" | "target" | "terminal" | "thermometer" | "thumbs-down" | "thumbs-up" | "toggle-left" | "toggle-right" | "tool" | "trash-2" | "trash" | "trello" | "trending-down" | "trending-up" | "triangle" | "truck" | "tv" | "twitch" | "twitter" | "type" | "umbrella" | "underline" | "unlock" | "upload-cloud" | "upload" | "user-check" | "user-minus" | "user-plus" | "user-x" | "user" | "users" | "video-off" | "video" | "voicemail" | "volume-1" | "volume-2" | "volume-x" | "volume" | "watch" | "wifi-off" | "wifi" | "wind" | "x-circle" | "x-octagon" | "x-square" | "x" | "youtube" | "zap-off" | "zap" | "zoom-in" | "zoom-out">;
// @internal (undocumented)
export function idValidator<Id extends RecordId<UnknownRecord>>(prefix: Id['__type__']['typeName']): T.Validator<Id>;
// @internal (undocumented)
export const imageShapeMigrations: Migrations;
// @internal (undocumented)
export const imageShapeProps: ShapeProps<TLImageShape>;
// @public (undocumented)
export const InstancePageStateRecordType: RecordType<TLInstancePageState, "pageId">;
@ -450,6 +512,18 @@ export const LANGUAGES: readonly [{
readonly label: "繁體中文 (台灣)";
}];
// @internal (undocumented)
export const lineShapeMigrations: Migrations;
// @internal (undocumented)
export const lineShapeProps: ShapeProps<TLLineShape>;
// @internal (undocumented)
export const noteShapeMigrations: Migrations;
// @internal (undocumented)
export const noteShapeProps: ShapeProps<TLNoteShape>;
// @internal (undocumented)
export const opacityValidator: T.Validator<number>;
@ -468,18 +542,37 @@ export const PointerRecordType: RecordType<TLPointer, never>;
// @internal (undocumented)
export const rootShapeMigrations: Migrations;
// @public (undocumented)
export type SchemaShapeInfo = {
migrations?: Migrations;
props?: Record<string, {
validate: (prop: any) => any;
}>;
};
// @internal (undocumented)
export const scribbleValidator: T.Validator<TLScribble>;
// @public (undocumented)
export const shapeIdValidator: T.Validator<TLShapeId>;
// @public (undocumented)
export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
[K in keyof Shape['props']]: T.Validator<Shape['props'][K]>;
};
// @internal (undocumented)
export const sizeValidator: T.Validator<"l" | "m" | "s" | "xl">;
// @internal (undocumented)
export const splineValidator: T.Validator<"cubic" | "line">;
// @internal (undocumented)
export const textShapeMigrations: Migrations;
// @internal (undocumented)
export const textShapeProps: ShapeProps<TLTextShape>;
// @public (undocumented)
export const TL_ALIGN_TYPES: Set<"end" | "middle" | "start">;
@ -487,7 +580,10 @@ export const TL_ALIGN_TYPES: Set<"end" | "middle" | "start">;
export const TL_ARROWHEAD_TYPES: Set<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
// @public
export const TL_COLOR_TYPES: Set<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
export const TL_CANVAS_UI_COLOR_TYPES: Set<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
// @public (undocumented)
export const TL_COLOR_TYPES: Set<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
// @public (undocumented)
export const TL_DASH_TYPES: Set<"dashed" | "dotted" | "draw" | "solid">;
@ -649,7 +745,7 @@ export interface TLCamera extends BaseRecord<'camera', TLCameraId> {
export type TLCameraId = RecordId<TLCamera>;
// @public
export type TLColor = SetValue<typeof TL_COLOR_TYPES>;
export type TLCanvasUiColor = SetValue<typeof TL_CANVAS_UI_COLOR_TYPES>;
// @public (undocumented)
export interface TLColorStyle extends TLBaseStyle {
@ -660,12 +756,12 @@ export interface TLColorStyle extends TLBaseStyle {
}
// @public (undocumented)
export type TLColorType = SetValue<typeof TL_COLOR_TYPES_2>;
export type TLColorType = SetValue<typeof TL_COLOR_TYPES>;
// @public
export interface TLCursor {
// (undocumented)
color: TLColor;
color: TLCanvasUiColor;
// (undocumented)
rotation: number;
// (undocumented)
@ -960,11 +1056,14 @@ export const TLPOINTER_ID: TLPointerId;
// @public (undocumented)
export type TLRecord = TLAsset | TLCamera | TLDocument | TLInstance | TLInstancePageState | TLInstancePresence | TLPage | TLPointer | TLShape;
// @public (undocumented)
export type TLSchema = StoreSchema<TLRecord, TLStoreProps>;
// @public
export type TLScribble = {
points: Vec2dModel[];
size: number;
color: TLColor;
color: TLCanvasUiColor;
opacity: number;
state: SetValue<typeof TL_SCRIBBLE_STATES>;
delay: number;
@ -1107,6 +1206,12 @@ export interface Vec2dModel {
// @internal (undocumented)
export const verticalAlignValidator: T.Validator<"end" | "middle" | "start">;
// @internal (undocumented)
export const videoShapeMigrations: Migrations;
// @internal (undocumented)
export const videoShapeProps: ShapeProps<TLVideoShape>;
// (No @packageDocumentation comment for this package)
```

View file

@ -1,4 +1,5 @@
import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/store'
import { mapObjectMapValues } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore'
import { AssetRecordType } from './records/TLAsset'
@ -11,84 +12,17 @@ import { PointerRecordType } from './records/TLPointer'
import { InstancePresenceRecordType } from './records/TLPresence'
import { TLRecord } from './records/TLRecord'
import { TLShape, rootShapeMigrations } from './records/TLShape'
import { arrowShapeMigrations, arrowShapeValidator } from './shapes/TLArrowShape'
import { bookmarkShapeMigrations, bookmarkShapeValidator } from './shapes/TLBookmarkShape'
import { drawShapeMigrations, drawShapeValidator } from './shapes/TLDrawShape'
import { embedShapeMigrations, embedShapeTypeValidator } from './shapes/TLEmbedShape'
import { frameShapeMigrations, frameShapeValidator } from './shapes/TLFrameShape'
import { geoShapeMigrations, geoShapeValidator } from './shapes/TLGeoShape'
import { groupShapeMigrations, groupShapeValidator } from './shapes/TLGroupShape'
import { highlightShapeMigrations, highlightShapeValidator } from './shapes/TLHighlightShape'
import { imageShapeMigrations, imageShapeValidator } from './shapes/TLImageShape'
import { lineShapeMigrations, lineShapeValidator } from './shapes/TLLineShape'
import { noteShapeMigrations, noteShapeValidator } from './shapes/TLNoteShape'
import { textShapeMigrations, textShapeValidator } from './shapes/TLTextShape'
import { videoShapeMigrations, videoShapeValidator } from './shapes/TLVideoShape'
import { createShapeValidator } from './shapes/TLBaseShape'
import { storeMigrations } from './store-migrations'
/** @public */
export type SchemaShapeInfo = {
migrations?: Migrations
validator?: { validate: (record: any) => any }
props?: Record<string, { validate: (prop: any) => any }>
}
const coreShapes: Record<string, SchemaShapeInfo> = {
group: {
migrations: groupShapeMigrations,
validator: groupShapeValidator,
},
bookmark: {
migrations: bookmarkShapeMigrations,
validator: bookmarkShapeValidator,
},
embed: {
migrations: embedShapeMigrations,
validator: embedShapeTypeValidator,
},
image: {
migrations: imageShapeMigrations,
validator: imageShapeValidator,
},
text: {
migrations: textShapeMigrations,
validator: textShapeValidator,
},
video: {
migrations: videoShapeMigrations,
validator: videoShapeValidator,
},
}
const defaultShapes: Record<string, SchemaShapeInfo> = {
arrow: {
migrations: arrowShapeMigrations,
validator: arrowShapeValidator,
},
draw: {
migrations: drawShapeMigrations,
validator: drawShapeValidator,
},
frame: {
migrations: frameShapeMigrations,
validator: frameShapeValidator,
},
geo: {
migrations: geoShapeMigrations,
validator: geoShapeValidator,
},
line: {
migrations: lineShapeMigrations,
validator: lineShapeValidator,
},
note: {
migrations: noteShapeMigrations,
validator: noteShapeValidator,
},
highlight: {
migrations: highlightShapeMigrations,
validator: highlightShapeValidator,
},
}
/** @public */
export type TLSchema = StoreSchema<TLRecord, TLStoreProps>
/**
* Create a TLSchema with custom shapes. Custom shapes cannot override default shapes.
@ -96,45 +30,26 @@ const defaultShapes: Record<string, SchemaShapeInfo> = {
* @param opts - Options
*
* @public */
export function createTLSchema(
opts = {} as {
customShapes: Record<string, SchemaShapeInfo>
}
) {
const { customShapes } = opts
for (const key in customShapes) {
if (key in coreShapes) {
throw Error(`Can't override default shape ${key}!`)
}
}
const allShapeEntries = Object.entries({ ...coreShapes, ...defaultShapes, ...customShapes })
export function createTLSchema({ shapes }: { shapes: Record<string, SchemaShapeInfo> }): TLSchema {
const ShapeRecordType = createRecordType<TLShape>('shape', {
migrations: defineMigrations({
currentVersion: rootShapeMigrations.currentVersion,
firstVersion: rootShapeMigrations.firstVersion,
migrators: rootShapeMigrations.migrators,
subTypeKey: 'type',
subTypeMigrations: {
...Object.fromEntries(
allShapeEntries.map(([k, v]) => [k, v.migrations ?? defineMigrations({})])
),
},
subTypeMigrations: mapObjectMapValues(shapes, (k, v) => v.migrations ?? defineMigrations({})),
}),
scope: 'document',
validator: T.model(
'shape',
T.union('type', {
...Object.fromEntries(
allShapeEntries.map(([k, v]) => [k, (v.validator as T.Validator<any>) ?? T.any])
),
})
T.union(
'type',
mapObjectMapValues(shapes, (type, { props }) => createShapeValidator(type, props))
)
),
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false, opacity: 1 }))
return StoreSchema.create<TLRecord, TLStoreProps>(
return StoreSchema.create(
{
asset: AssetRecordType,
camera: CameraRecordType,

View file

@ -9,9 +9,13 @@ export { type TLBookmarkAsset } from './assets/TLBookmarkAsset'
export { type TLImageAsset } from './assets/TLImageAsset'
export { type TLVideoAsset } from './assets/TLVideoAsset'
export { createPresenceStateDerivation } from './createPresenceStateDerivation'
export { createTLSchema } from './createTLSchema'
export { createTLSchema, type SchemaShapeInfo, type TLSchema } from './createTLSchema'
export { CLIENT_FIXUP_SCRIPT, fixupRecord } from './fixup'
export { TL_COLOR_TYPES, colorTypeValidator, type TLColor } from './misc/TLColor'
export {
TL_CANVAS_UI_COLOR_TYPES,
canvasUiColorTypeValidator,
type TLCanvasUiColor,
} from './misc/TLColor'
export { type TLCursor, type TLCursorType } from './misc/TLCursor'
export { type TLHandle, type TLHandleType } from './misc/TLHandle'
export { scribbleValidator, type TLScribble } from './misc/TLScribble'
@ -63,6 +67,8 @@ export {
type TLUnknownShape,
} from './records/TLShape'
export {
arrowShapeMigrations,
arrowShapeProps,
type TLArrowShape,
type TLArrowShapeProps,
type TLArrowTerminal,
@ -72,27 +78,54 @@ export {
createShapeValidator,
parentIdValidator,
shapeIdValidator,
type ShapeProps,
type TLBaseShape,
} from './shapes/TLBaseShape'
export { type TLBookmarkShape } from './shapes/TLBookmarkShape'
export { type TLDrawShape, type TLDrawShapeSegment } from './shapes/TLDrawShape'
export {
bookmarkShapeMigrations,
bookmarkShapeProps,
type TLBookmarkShape,
} from './shapes/TLBookmarkShape'
export {
drawShapeMigrations,
drawShapeProps,
type TLDrawShape,
type TLDrawShapeSegment,
} from './shapes/TLDrawShape'
export {
EMBED_DEFINITIONS,
embedShapeMigrations,
embedShapePermissionDefaults,
embedShapeProps,
type EmbedDefinition,
type TLEmbedShape,
type TLEmbedShapePermissions,
} from './shapes/TLEmbedShape'
export { type TLFrameShape } from './shapes/TLFrameShape'
export { type TLGeoShape } from './shapes/TLGeoShape'
export { type TLGroupShape } from './shapes/TLGroupShape'
export { type TLHighlightShape } from './shapes/TLHighlightShape'
export { type TLIconShape } from './shapes/TLIconShape'
export { type TLImageCrop, type TLImageShape, type TLImageShapeProps } from './shapes/TLImageShape'
export { type TLLineShape } from './shapes/TLLineShape'
export { type TLNoteShape } from './shapes/TLNoteShape'
export { type TLTextShape, type TLTextShapeProps } from './shapes/TLTextShape'
export { type TLVideoShape } from './shapes/TLVideoShape'
export { frameShapeMigrations, frameShapeProps, type TLFrameShape } from './shapes/TLFrameShape'
export { geoShapeMigrations, geoShapeProps, type TLGeoShape } from './shapes/TLGeoShape'
export { groupShapeMigrations, groupShapeProps, type TLGroupShape } from './shapes/TLGroupShape'
export {
highlightShapeMigrations,
highlightShapeProps,
type TLHighlightShape,
} from './shapes/TLHighlightShape'
export { iconShapeMigrations, iconShapeProps, type TLIconShape } from './shapes/TLIconShape'
export {
imageShapeMigrations,
imageShapeProps,
type TLImageCrop,
type TLImageShape,
type TLImageShapeProps,
} from './shapes/TLImageShape'
export { lineShapeMigrations, lineShapeProps, type TLLineShape } from './shapes/TLLineShape'
export { noteShapeMigrations, noteShapeProps, type TLNoteShape } from './shapes/TLNoteShape'
export {
textShapeMigrations,
textShapeProps,
type TLTextShape,
type TLTextShapeProps,
} from './shapes/TLTextShape'
export { videoShapeMigrations, videoShapeProps, type TLVideoShape } from './shapes/TLVideoShape'
export {
TL_ALIGN_TYPES,
alignValidator,
@ -106,7 +139,12 @@ export {
type TLArrowheadType,
} from './styles/TLArrowheadStyle'
export { TL_STYLE_TYPES, type TLStyleType } from './styles/TLBaseStyle'
export { colorValidator, type TLColorStyle, type TLColorType } from './styles/TLColorStyle'
export {
TL_COLOR_TYPES,
colorValidator,
type TLColorStyle,
type TLColorType,
} from './styles/TLColorStyle'
export {
TL_DASH_TYPES,
dashValidator,

View file

@ -5,7 +5,7 @@ import { SetValue } from '../util-types'
* The colors used by tldraw's default shapes.
*
* @public */
export const TL_COLOR_TYPES = new Set([
export const TL_CANVAS_UI_COLOR_TYPES = new Set([
'accent',
'white',
'black',
@ -19,10 +19,10 @@ export const TL_COLOR_TYPES = new Set([
* A type for the colors used by tldraw's default shapes.
*
* @public */
export type TLColor = SetValue<typeof TL_COLOR_TYPES>
export type TLCanvasUiColor = SetValue<typeof TL_CANVAS_UI_COLOR_TYPES>
/**
* A validator for the colors used by tldraw's default shapes.
*
* @public */
export const colorTypeValidator = T.setEnum(TL_COLOR_TYPES)
export const canvasUiColorTypeValidator = T.setEnum(TL_CANVAS_UI_COLOR_TYPES)

View file

@ -1,6 +1,6 @@
import { T } from '@tldraw/validate'
import { SetValue } from '../util-types'
import { TLColor, colorTypeValidator } from './TLColor'
import { TLCanvasUiColor, canvasUiColorTypeValidator } from './TLColor'
/**
* The cursor types used by tldraw's default shapes.
@ -44,14 +44,14 @@ export const cursorTypeValidator = T.setEnum(TL_CURSOR_TYPES)
*
* @public */
export interface TLCursor {
color: TLColor
color: TLCanvasUiColor
type: TLCursorType
rotation: number
}
/** @internal */
export const cursorValidator: T.Validator<TLCursor> = T.object({
color: colorTypeValidator,
color: canvasUiColorTypeValidator,
type: cursorTypeValidator,
rotation: T.number,
})

View file

@ -1,6 +1,6 @@
import { T } from '@tldraw/validate'
import { SetValue } from '../util-types'
import { TLColor, colorTypeValidator } from './TLColor'
import { TLCanvasUiColor, canvasUiColorTypeValidator } from './TLColor'
import { Vec2dModel } from './geometry-types'
/**
@ -16,7 +16,7 @@ export const TL_SCRIBBLE_STATES = new Set(['starting', 'paused', 'active', 'stop
export type TLScribble = {
points: Vec2dModel[]
size: number
color: TLColor
color: TLCanvasUiColor
opacity: number
state: SetValue<typeof TL_SCRIBBLE_STATES>
delay: number
@ -26,7 +26,7 @@ export type TLScribble = {
export const scribbleValidator: T.Validator<TLScribble> = T.object({
points: T.arrayOf(T.point),
size: T.positiveNumber,
color: colorTypeValidator,
color: canvasUiColorTypeValidator,
opacity: T.number,
state: T.setEnum(TL_SCRIBBLE_STATES),
delay: T.number,

View file

@ -9,7 +9,7 @@ import { TLFillType, fillValidator } from '../styles/TLFillStyle'
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { SetValue } from '../util-types'
import { TLBaseShape, createShapeValidator, shapeIdValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape, shapeIdValidator } from './TLBaseShape'
/** @public */
export const TL_ARROW_TERMINAL_TYPE = new Set(['binding', 'point'] as const)
@ -62,23 +62,20 @@ export const arrowTerminalValidator: T.Validator<TLArrowTerminal> = T.union('typ
})
/** @internal */
export const arrowShapeValidator: T.Validator<TLArrowShape> = createShapeValidator(
'arrow',
T.object({
labelColor: colorValidator,
color: colorValidator,
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
arrowheadStart: arrowheadValidator,
arrowheadEnd: arrowheadValidator,
font: fontValidator,
start: arrowTerminalValidator,
end: arrowTerminalValidator,
bend: T.number,
text: T.string,
})
)
export const arrowShapeProps: ShapeProps<TLArrowShape> = {
labelColor: colorValidator,
color: colorValidator,
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
arrowheadStart: arrowheadValidator,
arrowheadEnd: arrowheadValidator,
font: fontValidator,
start: arrowTerminalValidator,
end: arrowTerminalValidator,
bend: T.number,
text: T.string,
}
const Versions = {
AddLabelColor: 1,

View file

@ -32,7 +32,7 @@ export const shapeIdValidator = idValidator<TLShapeId>('shape')
/** @public */
export function createShapeValidator<Type extends string, Props extends object>(
type: Type,
props: T.Validator<Props>
props?: { [K in keyof Props]: T.Validatable<Props[K]> }
) {
return T.object({
id: shapeIdValidator,
@ -45,6 +45,11 @@ export function createShapeValidator<Type extends string, Props extends object>(
type: T.literal(type),
isLocked: T.boolean,
opacity: opacityValidator,
props,
props: props ? T.object(props) : T.unknownObject,
})
}
/** @public */
export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
[K in keyof Shape['props']]: T.Validator<Shape['props'][K]>
}

View file

@ -2,7 +2,7 @@ import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { assetIdValidator } from '../assets/TLBaseAsset'
import { TLAssetId } from '../records/TLAsset'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLBookmarkShapeProps = {
@ -16,15 +16,12 @@ export type TLBookmarkShapeProps = {
export type TLBookmarkShape = TLBaseShape<'bookmark', TLBookmarkShapeProps>
/** @internal */
export const bookmarkShapeValidator: T.Validator<TLBookmarkShape> = createShapeValidator(
'bookmark',
T.object({
w: T.nonZeroNumber,
h: T.nonZeroNumber,
assetId: assetIdValidator.nullable(),
url: T.string,
})
)
export const bookmarkShapeProps: ShapeProps<TLBookmarkShape> = {
w: T.nonZeroNumber,
h: T.nonZeroNumber,
assetId: assetIdValidator.nullable(),
url: T.string,
}
const Versions = {
NullAssetId: 1,

View file

@ -6,7 +6,7 @@ import { TLDashType, dashValidator } from '../styles/TLDashStyle'
import { TLFillType, fillValidator } from '../styles/TLFillStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { SetValue } from '../util-types'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
const TL_DRAW_SHAPE_SEGMENT_TYPE = new Set(['free', 'straight'] as const)
@ -39,19 +39,16 @@ export type TLDrawShapeProps = {
export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
/** @internal */
export const drawShapeValidator: T.Validator<TLDrawShape> = createShapeValidator(
'draw',
T.object({
color: colorValidator,
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
segments: T.arrayOf(drawShapeSegmentValidator),
isComplete: T.boolean,
isClosed: T.boolean,
isPen: T.boolean,
})
)
export const drawShapeProps: ShapeProps<TLDrawShape> = {
color: colorValidator,
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
segments: T.arrayOf(drawShapeSegmentValidator),
isComplete: T.boolean,
isClosed: T.boolean,
isPen: T.boolean,
}
const Versions = {
AddInPen: 1,

View file

@ -1,6 +1,6 @@
import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
// Only allow multiplayer embeds. If we add additional routes later for example '/help' this won't match
const TLDRAW_APP_RE = /(^\/r\/[^/]+\/?$)/
@ -561,22 +561,19 @@ export type TLEmbedShapeProps = {
export type TLEmbedShape = TLBaseShape<'embed', TLEmbedShapeProps>
/** @internal */
export const embedShapeTypeValidator: T.Validator<TLEmbedShape> = createShapeValidator(
'embed',
T.object({
w: T.nonZeroNumber,
h: T.nonZeroNumber,
url: T.string,
tmpOldUrl: T.string.optional(),
doesResize: T.boolean,
overridePermissions: T.dict(
T.setEnum(
new Set(Object.keys(embedShapePermissionDefaults) as (keyof TLEmbedShapePermissions)[])
),
T.boolean.optional()
).optional(),
})
)
export const embedShapeProps: ShapeProps<TLEmbedShape> = {
w: T.nonZeroNumber,
h: T.nonZeroNumber,
url: T.string,
tmpOldUrl: T.string.optional(),
doesResize: T.boolean,
overridePermissions: T.dict(
T.setEnum(
new Set(Object.keys(embedShapePermissionDefaults) as (keyof TLEmbedShapePermissions)[])
),
T.boolean.optional()
).optional(),
}
/** @public */
export type EmbedDefinition = {

View file

@ -1,6 +1,6 @@
import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { createShapeValidator, TLBaseShape } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
type TLFrameShapeProps = {
w: number
@ -12,14 +12,11 @@ type TLFrameShapeProps = {
export type TLFrameShape = TLBaseShape<'frame', TLFrameShapeProps>
/** @internal */
export const frameShapeValidator: T.Validator<TLFrameShape> = createShapeValidator(
'frame',
T.object({
w: T.nonZeroNumber,
h: T.nonZeroNumber,
name: T.string,
})
)
export const frameShapeProps: ShapeProps<TLFrameShape> = {
w: T.nonZeroNumber,
h: T.nonZeroNumber,
name: T.string,
}
/** @internal */
export const frameShapeMigrations = defineMigrations({})

View file

@ -8,7 +8,7 @@ import { TLFontType, fontValidator } from '../styles/TLFontStyle'
import { TLGeoType, geoValidator } from '../styles/TLGeoStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLVerticalAlignType, verticalAlignValidator } from '../styles/TLVerticalAlignStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLGeoShapeProps = {
@ -32,25 +32,22 @@ export type TLGeoShapeProps = {
export type TLGeoShape = TLBaseShape<'geo', TLGeoShapeProps>
/** @internal */
export const geoShapeValidator: T.Validator<TLGeoShape> = createShapeValidator(
'geo',
T.object({
geo: geoValidator,
labelColor: colorValidator,
color: colorValidator,
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
font: fontValidator,
align: alignValidator,
verticalAlign: verticalAlignValidator,
url: T.string,
w: T.nonZeroNumber,
h: T.nonZeroNumber,
growY: T.positiveNumber,
text: T.string,
})
)
export const geoShapeProps: ShapeProps<TLGeoShape> = {
geo: geoValidator,
labelColor: colorValidator,
color: colorValidator,
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
font: fontValidator,
align: alignValidator,
verticalAlign: verticalAlignValidator,
url: T.string,
w: T.nonZeroNumber,
h: T.nonZeroNumber,
growY: T.positiveNumber,
text: T.string,
}
const Versions = {
AddUrlProp: 1,

View file

@ -1,6 +1,5 @@
import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { createShapeValidator, TLBaseShape } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLGroupShapeProps = { [key in never]: undefined }
@ -9,10 +8,7 @@ export type TLGroupShapeProps = { [key in never]: undefined }
export type TLGroupShape = TLBaseShape<'group', TLGroupShapeProps>
/** @internal */
export const groupShapeValidator: T.Validator<TLGroupShape> = createShapeValidator(
'group',
T.object({})
)
export const groupShapeProps: ShapeProps<TLGroupShape> = {}
/** @internal */
export const groupShapeMigrations = defineMigrations({})

View file

@ -2,7 +2,7 @@ import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
import { TLDrawShapeSegment, drawShapeSegmentValidator } from './TLDrawShape'
/** @public */
@ -18,16 +18,13 @@ export type TLHighlightShapeProps = {
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
/** @internal */
export const highlightShapeValidator: T.Validator<TLHighlightShape> = createShapeValidator(
'highlight',
T.object({
color: colorValidator,
size: sizeValidator,
segments: T.arrayOf(drawShapeSegmentValidator),
isComplete: T.boolean,
isPen: T.boolean,
})
)
export const highlightShapeProps: ShapeProps<TLHighlightShape> = {
color: colorValidator,
size: sizeValidator,
segments: T.arrayOf(drawShapeSegmentValidator),
isComplete: T.boolean,
isPen: T.boolean,
}
/** @internal */
export const highlightShapeMigrations = defineMigrations({})

View file

@ -4,7 +4,7 @@ import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLDashType, dashValidator } from '../styles/TLDashStyle'
import { TLIconType, iconValidator } from '../styles/TLIconStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLIconShapeProps = {
@ -19,16 +19,13 @@ export type TLIconShapeProps = {
export type TLIconShape = TLBaseShape<'icon', TLIconShapeProps>
/** @internal */
export const iconShapeValidator: T.Validator<TLIconShape> = createShapeValidator(
'icon',
T.object({
size: sizeValidator,
icon: iconValidator,
dash: dashValidator,
color: colorValidator,
scale: T.number,
})
)
export const iconShapeProps: ShapeProps<TLIconShape> = {
size: sizeValidator,
icon: iconValidator,
dash: dashValidator,
color: colorValidator,
scale: T.number,
}
/** @internal */
export const iconShapeMigrations = defineMigrations({})

View file

@ -3,7 +3,7 @@ import { T } from '@tldraw/validate'
import { assetIdValidator } from '../assets/TLBaseAsset'
import { Vec2dModel } from '../misc/geometry-types'
import { TLAssetId } from '../records/TLAsset'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLImageCrop = {
@ -30,17 +30,14 @@ export const cropValidator = T.object({
})
/** @internal */
export const imageShapeValidator: T.Validator<TLImageShape> = createShapeValidator(
'image',
T.object({
w: T.nonZeroNumber,
h: T.nonZeroNumber,
playing: T.boolean,
url: T.string,
assetId: assetIdValidator.nullable(),
crop: cropValidator.nullable(),
})
)
export const imageShapeProps: ShapeProps<TLImageShape> = {
w: T.nonZeroNumber,
h: T.nonZeroNumber,
playing: T.boolean,
url: T.string,
assetId: assetIdValidator.nullable(),
crop: cropValidator.nullable(),
}
const Versions = {
AddUrlProp: 1,

View file

@ -5,7 +5,7 @@ import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLDashType, dashValidator } from '../styles/TLDashStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLSplineType, splineValidator } from '../styles/TLSplineStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLLineShapeProps = {
@ -22,16 +22,13 @@ export type TLLineShapeProps = {
export type TLLineShape = TLBaseShape<'line', TLLineShapeProps>
/** @internal */
export const lineShapeValidator: T.Validator<TLLineShape> = createShapeValidator(
'line',
T.object({
color: colorValidator,
dash: dashValidator,
size: sizeValidator,
spline: splineValidator,
handles: T.dict(T.string, handleValidator),
})
)
export const lineShapeProps: ShapeProps<TLLineShape> = {
color: colorValidator,
dash: dashValidator,
size: sizeValidator,
spline: splineValidator,
handles: T.dict(T.string, handleValidator),
}
/** @internal */
export const lineShapeMigrations = defineMigrations({})

View file

@ -5,7 +5,7 @@ import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLVerticalAlignType, verticalAlignValidator } from '../styles/TLVerticalAlignStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLNoteShapeProps = {
@ -23,19 +23,16 @@ export type TLNoteShapeProps = {
export type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>
/** @internal */
export const noteShapeValidator: T.Validator<TLNoteShape> = createShapeValidator(
'note',
T.object({
color: colorValidator,
size: sizeValidator,
font: fontValidator,
align: alignValidator,
verticalAlign: verticalAlignValidator,
growY: T.positiveNumber,
url: T.string,
text: T.string,
})
)
export const noteShapeProps: ShapeProps<TLNoteShape> = {
color: colorValidator,
size: sizeValidator,
font: fontValidator,
align: alignValidator,
verticalAlign: verticalAlignValidator,
growY: T.positiveNumber,
url: T.string,
text: T.string,
}
const Versions = {
AddUrlProp: 1,

View file

@ -4,7 +4,7 @@ import { TLAlignType, alignValidator } from '../styles/TLAlignStyle'
import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLTextShapeProps = {
@ -22,19 +22,16 @@ export type TLTextShapeProps = {
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>
/** @internal */
export const textShapeValidator: T.Validator<TLTextShape> = createShapeValidator(
'text',
T.object({
color: colorValidator,
size: sizeValidator,
font: fontValidator,
align: alignValidator,
w: T.nonZeroNumber,
text: T.string,
scale: T.nonZeroNumber,
autoSize: T.boolean,
})
)
export const textShapeProps: ShapeProps<TLTextShape> = {
color: colorValidator,
size: sizeValidator,
font: fontValidator,
align: alignValidator,
w: T.nonZeroNumber,
text: T.string,
scale: T.nonZeroNumber,
autoSize: T.boolean,
}
const Versions = {
RemoveJustify: 1,

View file

@ -2,7 +2,7 @@ import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { assetIdValidator } from '../assets/TLBaseAsset'
import { TLAssetId } from '../records/TLAsset'
import { TLBaseShape, createShapeValidator } from './TLBaseShape'
import { ShapeProps, TLBaseShape } from './TLBaseShape'
/** @public */
export type TLVideoShapeProps = {
@ -18,17 +18,14 @@ export type TLVideoShapeProps = {
export type TLVideoShape = TLBaseShape<'video', TLVideoShapeProps>
/** @internal */
export const videoShapeValidator: T.Validator<TLVideoShape> = createShapeValidator(
'video',
T.object({
w: T.nonZeroNumber,
h: T.nonZeroNumber,
time: T.number,
playing: T.boolean,
url: T.string,
assetId: assetIdValidator.nullable(),
})
)
export const videoShapeProps: ShapeProps<TLVideoShape> = {
w: T.nonZeroNumber,
h: T.nonZeroNumber,
time: T.number,
playing: T.boolean,
url: T.string,
assetId: assetIdValidator.nullable(),
}
const Versions = {
AddUrlProp: 1,

View file

@ -65,6 +65,11 @@ export function getOwnProperty(obj: object, key: string): unknown;
// @internal (undocumented)
export function hasOwnProperty(obj: object, key: string): boolean;
// @internal (undocumented)
export type Identity<T> = {
[K in keyof T]: T[K];
};
// @public
export function isDefined<T>(value: T): value is typeof value extends undefined ? never : T;
@ -83,12 +88,22 @@ export function lerp(a: number, b: number, t: number): number;
// @public (undocumented)
export function lns(str: string): string;
// @internal
export function mapObjectMapValues<Key extends string, ValueBefore, ValueAfter>(object: {
readonly [K in Key]: ValueBefore;
}, mapper: (key: Key, value: ValueBefore) => ValueAfter): {
[K in Key]: ValueAfter;
};
// @internal (undocumented)
export function minBy<T>(arr: readonly T[], fn: (item: T) => number): T | undefined;
// @public
export function modulate(value: number, rangeA: number[], rangeB: number[], clamp?: boolean): number;
// @internal
export function noop(): void;
// @internal
export function objectMapEntries<Key extends string, Value>(object: {
[K in Key]: Value;
@ -135,6 +150,10 @@ export type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>;
};
// @internal (undocumented)
type Required_2<T, K extends keyof T> = Identity<Omit<T, K> & _Required<Pick<T, K>>>;
export { Required_2 as Required }
// @public (undocumented)
export type Result<T, E> = ErrorResult<E> | OkResult<T>;

View file

@ -10,7 +10,7 @@ export {
} from './lib/control'
export { debounce } from './lib/debounce'
export { annotateError, getErrorAnnotations } from './lib/error'
export { omitFromStackTrace, throttle } from './lib/function'
export { noop, omitFromStackTrace, throttle } from './lib/function'
export { getHashForObject, getHashForString, lns } from './lib/hash'
export { getFirstFromIterable } from './lib/iterable'
export { lerp, modulate, rng } from './lib/number'
@ -19,6 +19,7 @@ export {
filterEntries,
getOwnProperty,
hasOwnProperty,
mapObjectMapValues,
objectMapEntries,
objectMapFromEntries,
objectMapKeys,
@ -26,5 +27,5 @@ export {
} from './lib/object'
export { rafThrottle, throttledRaf } from './lib/raf'
export { sortById } from './lib/sort'
export type { RecursivePartial } from './lib/types'
export type { Identity, RecursivePartial, Required } from './lib/types'
export { isDefined, isNonNull, isNonNullish, structuredClone } from './lib/value'

View file

@ -52,3 +52,10 @@ export function omitFromStackTrace<Args extends Array<unknown>, Return>(
return wrappedFn
}
/**
* Does nothing, but it's really really good at it.
* @internal
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
export function noop(): void {}

View file

@ -120,3 +120,20 @@ export function filterEntries<Key extends string, Value>(
}
return didChange ? (result as { [K in Key]: Value }) : object
}
/**
* Maps the values of one object map to another.
* @returns a new object with the entries mapped
* @internal
*/
export function mapObjectMapValues<Key extends string, ValueBefore, ValueAfter>(
object: { readonly [K in Key]: ValueBefore },
mapper: (key: Key, value: ValueBefore) => ValueAfter
): { [K in Key]: ValueAfter } {
const result = {} as { [K in Key]: ValueAfter }
for (const [key, value] of objectMapEntries(object)) {
const newValue = mapper(key, value)
result[key] = newValue
}
return result
}

View file

@ -2,3 +2,11 @@
export type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>
}
/** @internal */
export type Identity<T> = { [K in keyof T]: T[K] }
type _Required<T> = { [K in keyof T]-?: T[K] }
/** @internal */
export type Required<T, K extends keyof T> = Identity<Omit<T, K> & _Required<Pick<T, K>>>

View file

@ -11,13 +11,13 @@ const any: Validator<any>;
const array: Validator<unknown[]>;
// @public
function arrayOf<T>(itemValidator: Validator<T>): ArrayOfValidator<T>;
function arrayOf<T>(itemValidator: Validatable<T>): ArrayOfValidator<T>;
// @public (undocumented)
class ArrayOfValidator<T> extends Validator<T[]> {
constructor(itemValidator: Validator<T>);
constructor(itemValidator: Validatable<T>);
// (undocumented)
readonly itemValidator: Validator<T>;
readonly itemValidator: Validatable<T>;
// (undocumented)
lengthGreaterThan1(): Validator<T[]>;
// (undocumented)
@ -39,15 +39,15 @@ const boxModel: ObjectValidator<{
}>;
// @public
function dict<Key extends string, Value>(keyValidator: Validator<Key>, valueValidator: Validator<Value>): DictValidator<Key, Value>;
function dict<Key extends string, Value>(keyValidator: Validatable<Key>, valueValidator: Validatable<Value>): DictValidator<Key, Value>;
// @public (undocumented)
class DictValidator<Key extends string, Value> extends Validator<Record<Key, Value>> {
constructor(keyValidator: Validator<Key>, valueValidator: Validator<Value>);
constructor(keyValidator: Validatable<Key>, valueValidator: Validatable<Value>);
// (undocumented)
readonly keyValidator: Validator<Key>;
readonly keyValidator: Validatable<Key>;
// (undocumented)
readonly valueValidator: Validator<Value>;
readonly valueValidator: Validatable<Value>;
}
// @public
@ -59,7 +59,7 @@ function literal<T extends boolean | number | string>(expectedValue: T): Validat
// @public
function model<T extends {
readonly id: string;
}>(name: string, validator: Validator<T>): Validator<T>;
}>(name: string, validator: Validatable<T>): Validator<T>;
// @public
const nonZeroInteger: Validator<number>;
@ -72,22 +72,22 @@ const number: Validator<number>;
// @public
function object<Shape extends object>(config: {
readonly [K in keyof Shape]: Validator<Shape[K]>;
readonly [K in keyof Shape]: Validatable<Shape[K]>;
}): ObjectValidator<Shape>;
// @public (undocumented)
class ObjectValidator<Shape extends object> extends Validator<Shape> {
constructor(config: {
readonly [K in keyof Shape]: Validator<Shape[K]>;
readonly [K in keyof Shape]: Validatable<Shape[K]>;
}, shouldAllowUnknownProperties?: boolean);
// (undocumented)
allowUnknownProperties(): ObjectValidator<Shape>;
// (undocumented)
readonly config: {
readonly [K in keyof Shape]: Validator<Shape[K]>;
readonly [K in keyof Shape]: Validatable<Shape[K]>;
};
extend<Extension extends Record<string, unknown>>(extension: {
readonly [K in keyof Extension]: Validator<Extension[K]>;
readonly [K in keyof Extension]: Validatable<Extension[K]>;
}): ObjectValidator<Shape & Extension>;
}
@ -120,6 +120,7 @@ declare namespace T {
model,
setEnum,
ValidatorFn,
Validatable,
ValidationError,
TypeOf,
Validator,
@ -147,7 +148,7 @@ declare namespace T {
export { T }
// @public (undocumented)
type TypeOf<V extends Validator<unknown>> = V extends Validator<infer T> ? T : never;
type TypeOf<V extends Validatable<unknown>> = V extends Validatable<infer T> ? T : never;
// @public
function union<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(key: Key, config: Config): UnionValidator<Key, Config>;
@ -165,6 +166,11 @@ const unknown: Validator<unknown>;
// @public (undocumented)
const unknownObject: Validator<Record<string, unknown>>;
// @public (undocumented)
type Validatable<T> = {
validate: (value: unknown) => T;
};
// @public (undocumented)
class ValidationError extends Error {
constructor(rawMessage: string, path?: ReadonlyArray<number | string>);
@ -177,7 +183,7 @@ class ValidationError extends Error {
}
// @public (undocumented)
class Validator<T> {
class Validator<T> implements Validatable<T> {
constructor(validationFn: ValidatorFn<T>);
check(name: string, checkFn: (value: T) => void): Validator<T>;
// (undocumented)

View file

@ -3,6 +3,9 @@ import { exhaustiveSwitchError, getOwnProperty, hasOwnProperty } from '@tldraw/u
/** @public */
export type ValidatorFn<T> = (value: unknown) => T
/** @public */
export type Validatable<T> = { validate: (value: unknown) => T }
function formatPath(path: ReadonlyArray<number | string>): string | null {
if (!path.length) {
return null
@ -77,10 +80,10 @@ function typeToString(value: unknown): string {
}
/** @public */
export type TypeOf<V extends Validator<unknown>> = V extends Validator<infer T> ? T : never
export type TypeOf<V extends Validatable<unknown>> = V extends Validatable<infer T> ? T : never
/** @public */
export class Validator<T> {
export class Validator<T> implements Validatable<T> {
constructor(readonly validationFn: ValidatorFn<T>) {}
/**
@ -159,7 +162,7 @@ export class Validator<T> {
/** @public */
export class ArrayOfValidator<T> extends Validator<T[]> {
constructor(readonly itemValidator: Validator<T>) {
constructor(readonly itemValidator: Validatable<T>) {
super((value) => {
const arr = array.validate(value)
for (let i = 0; i < arr.length; i++) {
@ -190,7 +193,7 @@ export class ArrayOfValidator<T> extends Validator<T[]> {
export class ObjectValidator<Shape extends object> extends Validator<Shape> {
constructor(
public readonly config: {
readonly [K in keyof Shape]: Validator<Shape[K]>
readonly [K in keyof Shape]: Validatable<Shape[K]>
},
private readonly shouldAllowUnknownProperties = false
) {
@ -236,7 +239,7 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
* ```
*/
extend<Extension extends Record<string, unknown>>(extension: {
readonly [K in keyof Extension]: Validator<Extension[K]>
readonly [K in keyof Extension]: Validatable<Extension[K]>
}): ObjectValidator<Shape & Extension> {
return new ObjectValidator({ ...this.config, ...extension }) as ObjectValidator<
Shape & Extension
@ -246,7 +249,7 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
// pass this into itself e.g. Config extends UnionObjectSchemaConfig<Key, Config>
type UnionValidatorConfig<Key extends string, Config> = {
readonly [Variant in keyof Config]: Validator<any> & {
readonly [Variant in keyof Config]: Validatable<any> & {
validate: (input: any) => { readonly [K in Key]: Variant }
}
}
@ -292,8 +295,8 @@ export class UnionValidator<
/** @public */
export class DictValidator<Key extends string, Value> extends Validator<Record<Key, Value>> {
constructor(
public readonly keyValidator: Validator<Key>,
public readonly valueValidator: Validator<Value>
public readonly keyValidator: Validatable<Key>,
public readonly valueValidator: Validatable<Value>
) {
super((object) => {
if (typeof object !== 'object' || object === null) {
@ -446,7 +449,7 @@ export const array = new Validator<unknown[]>((value) => {
*
* @public
*/
export function arrayOf<T>(itemValidator: Validator<T>): ArrayOfValidator<T> {
export function arrayOf<T>(itemValidator: Validatable<T>): ArrayOfValidator<T> {
return new ArrayOfValidator(itemValidator)
}
@ -464,7 +467,7 @@ export const unknownObject = new Validator<Record<string, unknown>>((value) => {
* @public
*/
export function object<Shape extends object>(config: {
readonly [K in keyof Shape]: Validator<Shape[K]>
readonly [K in keyof Shape]: Validatable<Shape[K]>
}): ObjectValidator<Shape> {
return new ObjectValidator(config)
}
@ -475,8 +478,8 @@ export function object<Shape extends object>(config: {
* @public
*/
export function dict<Key extends string, Value>(
keyValidator: Validator<Key>,
valueValidator: Validator<Value>
keyValidator: Validatable<Key>,
valueValidator: Validatable<Value>
): DictValidator<Key, Value> {
return new DictValidator(keyValidator, valueValidator)
}
@ -517,7 +520,7 @@ export function union<Key extends string, Config extends UnionValidatorConfig<Ke
*/
export function model<T extends { readonly id: string }>(
name: string,
validator: Validator<T>
validator: Validatable<T>
): Validator<T> {
return new Validator((value) => {
const prefix =