import { TLAsset, TLInstance, TLInstanceId, TLStore, TLUser, TLUserId } from '@tldraw/tlschema' import { Store } from '@tldraw/tlstore' import { annotateError } from '@tldraw/utils' import React, { useCallback, useSyncExternalStore } from 'react' import { App } from './app/App' import { EditorAssetUrls, defaultEditorAssetUrls } from './assetUrls' import { OptionalErrorBoundary } from './components/ErrorBoundary' import { SyncedStore } from './config/SyncedStore' import { TldrawEditorConfig } from './config/TldrawEditorConfig' import { DefaultErrorFallback } from './components/DefaultErrorFallback' import { AppContext } from './hooks/useApp' import { ContainerProvider, useContainer } from './hooks/useContainer' import { useCursor } from './hooks/useCursor' import { useDarkMode } from './hooks/useDarkMode' import { EditorComponentsProvider, TLEditorComponents, useEditorComponents, } from './hooks/useEditorComponents' import { useEvent } from './hooks/useEvent' import { useForceUpdate } from './hooks/useForceUpdate' import { usePreloadAssets } from './hooks/usePreloadAssets' import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix' import { useZoomCss } from './hooks/useZoomCss' /** @public */ export interface TldrawEditorProps { children?: any /** Overrides for the tldraw components */ components?: Partial /** Whether to display the dark mode. */ isDarkMode?: boolean /** A configuration defining major customizations to the app, such as custom shapes and new tools */ config?: TldrawEditorConfig /** * Called when the app has mounted. * * @example * * ```ts * function TldrawEditor() { * return app.selectAll()} /> * } * ``` * * @param app - The app instance. */ onMount?: (app: App) => void /** * Called when the app generates a new asset from a file, such as when an image is dropped into * the canvas. * * @example * * ```ts * const app = new App({ * onCreateAssetFromFile: (file) => uploadFileAndCreateAsset(file), * }) * ``` * * @param file - The file to generate an asset from. * @param id - The id to be assigned to the resulting asset. */ onCreateAssetFromFile?: (file: File) => Promise /** * Called when a URL is converted to a bookmark. This callback should return the metadata for the * bookmark. * * @example * * ```ts * app.onCreateBookmarkFromUrl(url, id) * ``` * * @param url - The url that was created. * @public */ onCreateBookmarkFromUrl?: ( url: string ) => Promise<{ image: string; title: string; description: string }> /** * The Store instance to use for keeping the app's data. This may be prepopulated, e.g. by loading * from a server or database. */ store?: TLStore | SyncedStore /** The id of the current user. If not given, one will be generated. */ userId?: TLUserId /** * The id of the app instance (e.g. a browser tab if the app will have only one tldraw app per * tab). If not given, one will be generated. */ instanceId?: TLInstanceId /** Asset URLs */ assetUrls?: EditorAssetUrls /** Whether to automatically focus the editor when it mounts. */ autoFocus?: boolean } declare global { interface Window { tldrawReady: boolean } } /** @public */ export function TldrawEditor(props: TldrawEditorProps) { const [container, setContainer] = React.useState(null) const { components, ...rest } = props const ErrorFallback = components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback return (
: null} onError={(error) => annotateError(error, { tags: { origin: 'react.tldraw-before-app' } })} > {container && ( )}
) } function TldrawEditorBeforeLoading({ config = TldrawEditorConfig.default, userId, instanceId, store, ...props }: TldrawEditorProps) { const { done: preloadingComplete, error: preloadingError } = usePreloadAssets( props.assetUrls ?? defaultEditorAssetUrls ) store ??= config.createStore({ userId: userId ?? TLUser.createId(), instanceId: instanceId ?? TLInstance.createId(), }) let loadedStore if (!(store instanceof Store)) { if (store.error) { // for error handling, we fall back to the default error boundary. // if users want to handle this error differently, they can render // their own error screen before the TldrawEditor component throw store.error } if (!store.store) { return Connecting... } loadedStore = store.store } else { loadedStore = store } if (instanceId && loadedStore.props.instanceId !== instanceId) { console.error( `The store's instanceId (${loadedStore.props.instanceId}) does not match the instanceId prop (${instanceId}). This may cause unexpected behavior.` ) } if (userId && loadedStore.props.userId !== userId) { console.error( `The store's userId (${loadedStore.props.userId}) does not match the userId prop (${userId}). This may cause unexpected behavior.` ) } if (preloadingError) { return Could not load assets. Please refresh the page. } if (!preloadingComplete) { return Loading assets... } return } function TldrawEditorAfterLoading({ onMount, config, isDarkMode, children, onCreateAssetFromFile, onCreateBookmarkFromUrl, store, autoFocus, }: Omit & { config: TldrawEditorConfig store: TLStore }) { const container = useContainer() const [app, setApp] = React.useState(null) const { ErrorFallback } = useEditorComponents() React.useLayoutEffect(() => { const app = new App({ store, getContainer: () => container, config, }) setApp(app) if (autoFocus) { app.focus() } ;(window as any).app = app return () => { app.dispose() setApp((prevApp) => (prevApp === app ? null : prevApp)) } }, [container, config, store, autoFocus]) React.useEffect(() => { if (app) { // Overwrite the default onCreateAssetFromFile handler. if (onCreateAssetFromFile) { app.onCreateAssetFromFile = onCreateAssetFromFile } if (onCreateBookmarkFromUrl) { app.onCreateBookmarkFromUrl = onCreateBookmarkFromUrl } } }, [app, onCreateAssetFromFile, onCreateBookmarkFromUrl]) const onMountEvent = useEvent((app: App) => { onMount?.(app) app.emit('mount') }) React.useEffect(() => { if (app) { // Set the initial theme state. if (isDarkMode !== undefined) { app.updateUserDocumentSettings({ isDarkMode }) } // Run onMount window.tldrawReady = true onMountEvent(app) } }, [app, onMountEvent, isDarkMode]) const crashingError = useSyncExternalStore( useCallback( (onStoreChange) => { if (app) { app.on('crash', onStoreChange) return () => app.off('crash', onStoreChange) } return () => { // noop } }, [app] ), () => app?.crashingError ?? null ) if (!app) { return null } return ( // the top-level tldraw component also renders an error boundary almost // identical to this one. the reason we have two is because this one has // access to `App`, which means that here we can enrich errors with data // from app for reporting, and also still attempt to render the user's // document in the event of an error to reassure them that their work is // not lost. : null} onError={(error) => app.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })} > {crashingError ? ( ) : ( {children} )} ) } function Layout({ children }: { children: any }) { useZoomCss() useCursor() useDarkMode() useSafariFocusOutFix() useForceUpdate() return children } function Crash({ crashingError }: { crashingError: unknown }): null { throw crashingError } /** @public */ export function LoadingScreen({ children }: { children: any }) { const { Spinner } = useEditorComponents() return (
{Spinner ? : null} {children}
) } /** @public */ export function ErrorScreen({ children }: { children: any }) { return
{children}
}