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:
parent
4b680d9451
commit
1927f88041
72 changed files with 1081 additions and 673 deletions
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import { TLBaseShape } from '@tldraw/tldraw'
|
||||
|
||||
export type CardShape = TLBaseShape<
|
||||
'card',
|
||||
{
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
>
|
|
@ -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,
|
||||
})
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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!`)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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],
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
26
packages/editor/src/lib/config/defineShape.ts
Normal file
26
packages/editor/src/lib/config/defineShape.ts
Normal 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 }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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"`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 │
|
||||
// │ │
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
```
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]>
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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({})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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({})
|
||||
|
|
|
@ -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({})
|
||||
|
|
|
@ -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({})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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({})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>>>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in a new issue