import { DEFAULT_SUPPORTED_IMAGE_TYPES, DEFAULT_SUPPORT_VIDEO_TYPES, TLEditorComponents, TLOnMountHandler, TLTextOptions, TldrawEditor, TldrawEditorBaseProps, TldrawEditorStoreProps, defaultUserPreferences, mergeArraysAndReplaceDefaults, useEditor, useEditorComponents, useOnMount, useShallowArrayIdentity, useShallowObjectIdentity, } from '@tldraw/editor' import { TLAnyAssetUtilConstructor } from '@tldraw/editor' import { useMemo } from 'react' import { ImageAssetUtil } from './assets/ImageAssetUtil' import { VideoAssetUtil } from './assets/VideoAssetUtil' import { defaultAssetUtils } from './defaultAssetUtils' import { defaultBindingUtils } from './defaultBindingUtils' import { TLEmbedDefinition } from './defaultEmbedDefinitions' import { TLExternalContentProps, registerDefaultExternalContentHandlers, } from './defaultExternalContentHandlers' import { defaultOverlayUtils } from './defaultOverlayUtils' import { defaultShapeTools } from './defaultShapeTools' import { defaultShapeUtils } from './defaultShapeUtils' import { registerDefaultSideEffects } from './defaultSideEffects' import { defaultTools } from './defaultTools' import { EmbedShapeUtil } from './shapes/embed/EmbedShapeUtil' import { allDefaultFontFaces } from './shapes/shared/defaultFonts' import { TLUiAssetUrlOverrides, useDefaultUiAssetUrlsWithOverrides } from './ui/assetUrls' import { LoadingScreen } from './ui/components/LoadingScreen' import { Spinner } from './ui/components/Spinner' import { AssetUrlsProvider } from './ui/context/asset-urls' import { TLUiComponents, useTldrawUiComponents } from './ui/context/components' import { useUiEvents } from './ui/context/events' import { useToasts } from './ui/context/toasts' import { TldrawUiTranslationProvider, useTranslation, } from './ui/hooks/useTranslation/useTranslation' import { useMergedTranslationOverrides } from './ui/overrides' import { TldrawUi, TldrawUiInFrontOfTheCanvas, TldrawUiProps } from './ui/TldrawUi' import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls' import { defaultAddFontsFromNode, tipTapDefaultExtensions } from './utils/text/richText' /** * Override the default react components used by the editor and UI. Set components to null to * disable them entirely. * * @example * ```tsx * import {Tldraw, TLComponents} from 'tldraw' * * const components: TLComponents = { * Scribble: MyCustomScribble, * } * * export function MyApp() { * return * } * ``` * * * @public */ export interface TLComponents extends TLEditorComponents, TLUiComponents {} /** @public */ export interface TldrawBaseProps extends TldrawUiProps, TldrawEditorBaseProps, TLExternalContentProps { /** Urls for custom assets. * * ⚠︎ Important! This must be memoized (with useMemo) or defined outside of any React component. */ assetUrls?: TLUiAssetUrlOverrides /** Overrides for tldraw's components. * * ⚠︎ Important! This must be memoized (with useMemo) or defined outside of any React component. */ components?: TLComponents /** Custom definitions for tldraw's embeds. * * ⚠︎ Important! This must be memoized (with useMemo) or defined outside of any React component. * * @deprecated Use `EmbedShapeUtil.configure({ embedDefinitions: embeds })` instead. */ embeds?: TLEmbedDefinition[] /** * Text options for the editor. * * @deprecated Use `options.text` instead. This prop will be removed in a future release. */ textOptions?: TLTextOptions /** * The locale to use for the editor's UI. When set, this takes priority over * both the browser's language preferences (`navigator.languages`) and the * user's locale preference (e.g. via * `editor.user.updateUserPreferences({ locale: '...' })`), giving the * application explicit control over the displayed language. * * @example * ```tsx * * ``` */ locale?: string } /** @public */ export type TldrawProps = TldrawBaseProps & TldrawEditorStoreProps const allDefaultTools = [...defaultTools, ...defaultShapeTools] function configureDefaultAssetUtils( assetUtils: readonly TLAnyAssetUtilConstructor[], overrides: Pick< TLExternalContentProps, 'maxImageDimension' | 'acceptedImageMimeTypes' | 'acceptedVideoMimeTypes' > ): readonly TLAnyAssetUtilConstructor[] { const { maxImageDimension, acceptedImageMimeTypes, acceptedVideoMimeTypes } = overrides const needsImageConfig = maxImageDimension !== undefined || acceptedImageMimeTypes !== undefined const needsVideoConfig = acceptedVideoMimeTypes !== undefined if (!needsImageConfig && !needsVideoConfig) return assetUtils return assetUtils.map((util) => { if (needsImageConfig && util.type === 'image') { return (util as typeof ImageAssetUtil).configure({ ...(maxImageDimension !== undefined && { maxDimension: maxImageDimension }), ...(acceptedImageMimeTypes !== undefined && { supportedMimeTypes: acceptedImageMimeTypes }), }) } if (needsVideoConfig && util.type === 'video') { return (util as typeof VideoAssetUtil).configure({ ...(acceptedVideoMimeTypes !== undefined && { supportedMimeTypes: acceptedVideoMimeTypes }), }) } return util }) } /** @public @react */ export function Tldraw(props: TldrawProps) { const { children, maxImageDimension, maxAssetSize, acceptedImageMimeTypes, acceptedVideoMimeTypes, onMount, components = {}, shapeUtils = [], bindingUtils = [], assetUtils = [], overlayUtils = [], tools = [], // needs to be here for backwards compatibility // eslint-disable-next-line @typescript-eslint/no-deprecated embeds, options, locale, // needs to be here for backwards compatibility with TldrawEditor // eslint-disable-next-line @typescript-eslint/no-deprecated textOptions: _textOptions, ...rest } = props const _components = useShallowObjectIdentity(components) const CustomInFrontOfTheCanvas = components?.InFrontOfTheCanvas const InFrontOfTheCanvas = useMemo(() => { if (rest.hideUi) return CustomInFrontOfTheCanvas ?? null if (!CustomInFrontOfTheCanvas) return TldrawUiInFrontOfTheCanvas return () => ( <> ) }, [rest.hideUi, CustomInFrontOfTheCanvas]) const componentsWithDefault = useMemo( () => ({ Spinner, LoadingScreen, ..._components, InFrontOfTheCanvas, }), [_components, InFrontOfTheCanvas] ) const _shapeUtils = useShallowArrayIdentity(shapeUtils) const shapeUtilsWithDefaults = useMemo( () => mergeArraysAndReplaceDefaults('type', _shapeUtils, defaultShapeUtils), [_shapeUtils] ) const _bindingUtils = useShallowArrayIdentity(bindingUtils) const bindingUtilsWithDefaults = useMemo( () => mergeArraysAndReplaceDefaults('type', _bindingUtils, defaultBindingUtils), [_bindingUtils] ) const _assetUtils = useShallowArrayIdentity(assetUtils) const assetUtilsWithDefaults = useMemo( () => configureDefaultAssetUtils( mergeArraysAndReplaceDefaults('type', _assetUtils, defaultAssetUtils), { maxImageDimension, acceptedImageMimeTypes, acceptedVideoMimeTypes } ), [_assetUtils, maxImageDimension, acceptedImageMimeTypes, acceptedVideoMimeTypes] ) const _overlayUtils = useShallowArrayIdentity(overlayUtils) const overlayUtilsWithDefaults = useMemo( () => mergeArraysAndReplaceDefaults('type', _overlayUtils, defaultOverlayUtils), [_overlayUtils] ) const _tools = useShallowArrayIdentity(tools) const toolsWithDefaults = useMemo( () => mergeArraysAndReplaceDefaults('id', _tools, allDefaultTools), [_tools] ) const _imageMimeTypes = useShallowArrayIdentity( acceptedImageMimeTypes ?? DEFAULT_SUPPORTED_IMAGE_TYPES ) const _videoMimeTypes = useShallowArrayIdentity( acceptedVideoMimeTypes ?? DEFAULT_SUPPORT_VIDEO_TYPES ) // Merge deprecated textOptions prop with options.textOptions // options.textOptions takes precedence over the deprecated textOptions prop const _mergedTextOptions = options?.text ?? _textOptions const textOptionsWithDefaults = useMemo((): TLTextOptions => { return { addFontsFromNode: defaultAddFontsFromNode, ..._mergedTextOptions, tipTapConfig: { extensions: tipTapDefaultExtensions, ..._mergedTextOptions?.tipTapConfig, }, } }, [_mergedTextOptions]) const optionsWithDefaults = useMemo( () => ({ ...options, text: textOptionsWithDefaults, }), [options, textOptionsWithDefaults] ) const mediaMimeTypes = useMemo( () => [..._imageMimeTypes, ..._videoMimeTypes], [_imageMimeTypes, _videoMimeTypes] ) const assets = useDefaultEditorAssetsWithOverrides(rest.assetUrls) const embedShapeUtil = shapeUtilsWithDefaults.find((util) => util.type === 'embed') if (embedShapeUtil && embeds) { // eslint-disable-next-line @typescript-eslint/no-deprecated EmbedShapeUtil.setEmbedDefinitions(embeds) } return ( // We provide an extra higher layer of asset+translations providers here so that // loading UI (which is rendered outside of TldrawUi) may be translated. // Ideally we would refactor to hoist all the UI context providers we can up here. Maybe later. {children} ) } // We put these hooks into a component here so that they can run inside of the context provided by TldrawEditor and TldrawUi. function InsideOfEditorAndUiContext({ maxImageDimension, maxAssetSize, acceptedImageMimeTypes, acceptedVideoMimeTypes, onMount, }: TLExternalContentProps & { onMount?: TLOnMountHandler }) { const editor = useEditor() const toasts = useToasts() const msg = useTranslation() const trackEvent = useUiEvents() useOnMount(() => { const unsubs: (void | (() => void) | undefined)[] = [] unsubs.push(registerDefaultSideEffects(editor)) // now that the editor has mounted (and presumably pre-loaded the fonts actually in use in // the document), we want to preload the other default font faces in the background. These // won't be directly used, but mean that when adding text the user can switch between fonts // quickly, without having to wait for them to load in. editor.fonts.requestFonts(allDefaultFontFaces) // Also preload any custom font faces defined in themes const themes = editor.getThemes() for (const theme of Object.values(themes)) { for (const key of Object.keys(theme.fonts)) { const font = theme.fonts[key as keyof typeof theme.fonts] if (font.faces?.length) { editor.fonts.requestFonts(font.faces) } } } editor.once('edit', () => trackEvent('edit', { source: 'unknown' })) // for content handling, first we register the default handlers... registerDefaultExternalContentHandlers(editor, { maxImageDimension, maxAssetSize, acceptedImageMimeTypes, acceptedVideoMimeTypes, toasts, msg, }) // ...then we call the store's on mount which may override them... unsubs.push(editor.store.props.onMount(editor)) // ...then we run the user's onMount prop, which may override things again. unsubs.push(onMount?.(editor)) return () => { unsubs.forEach((fn) => fn?.()) } }) const { Canvas } = useEditorComponents() const { ContextMenu } = useTldrawUiComponents() if (ContextMenu) { // should wrap canvas return } if (Canvas) { return } return null }