import { tlenv, useContainer, useEditor, useReactor, useValue } from '@tldraw/editor'
import classNames from 'classnames'
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
import { TLUiAssetUrlOverrides } from './assetUrls'
import { SkipToMainContent } from './components/A11y'
import { TldrawUiButton } from './components/primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from './components/primitives/Button/TldrawUiButtonIcon'
import { PORTRAIT_BREAKPOINT, PORTRAIT_BREAKPOINTS } from './constants'
import { useActions } from './context/actions'
import { useBreakpoint } from './context/breakpoints'
import { TLUiComponents, useTldrawUiComponents } from './context/components'
import {
TLUiContextProviderProps,
TldrawUiContextProvider,
} from './context/TldrawUiContextProvider'
import { useNativeClipboardEvents } from './hooks/useClipboardEvents'
import { useEditorEvents } from './hooks/useEditorEvents'
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
import { useReadonly } from './hooks/useReadonly'
import { useDirection, useTranslation } from './hooks/useTranslation/useTranslation'
/** @public */
export interface TldrawUiProps extends TLUiContextProviderProps {
/**
* The component's children.
*/
children?: ReactNode
/**
* Whether to hide the user interface and only display the canvas.
*/
hideUi?: boolean
/**
* Overrides for the UI components.
*/
components?: TLUiComponents
/**
* Additional items to add to the debug menu (will be deprecated)
*/
renderDebugMenuItems?(): React.ReactNode
/** Asset URL override. */
assetUrls?: TLUiAssetUrlOverrides
}
/**
* @public
* @react
*/
export const TldrawUi = React.memo(function TldrawUi({
renderDebugMenuItems,
children,
hideUi,
components,
...rest
}: TldrawUiProps) {
return (
{children}
)
})
interface TldrawUiContentProps {
hideUi?: boolean
shareZone?: ReactNode
topZone?: ReactNode
renderDebugMenuItems?(): React.ReactNode
}
const TldrawUiInner = React.memo(function TldrawUiInner({
children,
hideUi,
...rest
}: TldrawUiContentProps & { children: ReactNode }) {
// The hideUi prop should prevent the UI from mounting.
// If we ever need want the UI to mount and preserve state, then
// we should change this behavior and hide the UI via CSS instead.
// Keyboard shortcuts and clipboard events should always be mounted,
// even when the UI is hidden.
useKeyboardShortcuts()
useNativeClipboardEvents()
return (
<>
{children}
{hideUi ? null : }
>
)
})
const TldrawUiContent = React.memo(function TldrawUI() {
const editor = useEditor()
const msg = useTranslation()
const breakpoint = useBreakpoint()
const isReadonlyMode = useReadonly()
const isFocusMode = useValue('focus', () => editor.getInstanceState().isFocusMode, [editor])
const isDebugMode = useValue('debug', () => editor.getInstanceState().isDebugMode, [editor])
const container = useContainer()
const dir = useDirection()
const locale = useValue('locale', () => editor.user.getLocale(), [editor])
useEffect(() => {
container.dir = dir
container.lang = locale
}, [container, dir, locale])
const {
SharePanel,
TopPanel,
MenuPanel,
StylePanel,
Toolbar,
HelpMenu,
NavigationPanel,
HelperButtons,
DebugPanel,
Toasts,
Dialogs,
A11y,
} = useTldrawUiComponents()
useEditorEvents()
const rIsEditingAnything = useRef(false)
const rHidingTimeout = useRef(-1 as any)
const [hideToolbarWhileEditing, setHideToolbarWhileEditing] = useState(false)
useReactor(
'update hide toolbar while delayed',
() => {
const isMobileEnvironment = tlenv.isIos || tlenv.isAndroid
if (!isMobileEnvironment) return
const editingShape = editor.getEditingShapeId()
if (editingShape === null) {
if (rIsEditingAnything.current) {
rIsEditingAnything.current = false
clearTimeout(rHidingTimeout.current)
if (tlenv.isAndroid) {
// On Android, hide it after 150ms
rHidingTimeout.current = editor.timers.setTimeout(() => {
setHideToolbarWhileEditing(false)
}, 150)
} else {
// On iOS, just hide it immediately
setHideToolbarWhileEditing(false)
}
}
return
}
if (!rIsEditingAnything.current) {
rIsEditingAnything.current = true
clearTimeout(rHidingTimeout.current)
setHideToolbarWhileEditing(true)
}
},
[]
)
const { 'toggle-focus-mode': toggleFocus } = useActions()
const { breakpointsAbove, breakpointsBelow } = useMemo(() => {
const breakpointsAbove = []
const breakpointsBelow = []
for (let bp = 0; bp < PORTRAIT_BREAKPOINTS.length; bp++) {
if (bp <= breakpoint) {
breakpointsAbove.push(bp)
} else {
breakpointsBelow.push(bp)
}
}
return { breakpointsAbove, breakpointsBelow }
}, [breakpoint])
return (
{isFocusMode ? (
toggleFocus.onSelect('menu')}
>
) : (
<>
{MenuPanel && }
{HelperButtons && }
{TopPanel && }
{SharePanel && }
{StylePanel && breakpoint >= PORTRAIT_BREAKPOINT.TABLET_SM && !isReadonlyMode && (
)}
{NavigationPanel && }
{Toolbar && }
{HelpMenu && }
{isDebugMode && DebugPanel &&
}
{A11y &&
}
>
)}
{Toasts &&
}
{Dialogs &&
}
)
})
/** @public @react */
export function TldrawUiInFrontOfTheCanvas() {
const { RichTextToolbar, ImageToolbar, VideoToolbar, CursorChatBubble, FollowingIndicator } =
useTldrawUiComponents()
return (
<>
{RichTextToolbar && }
{ImageToolbar && }
{VideoToolbar && }
{FollowingIndicator && }
{CursorChatBubble && }
>
)
}