@tldraw/editor
Version:
tldraw infinite canvas SDK (editor).
347 lines (342 loc) • 12.7 kB
text/typescript
import { Awaitable } from '@tldraw/utils'
import { ComponentType, Fragment } from 'react'
import { DEFAULT_CAMERA_OPTIONS } from './constants'
import type { Editor } from './editor/Editor'
import { TLContent } from './editor/types/clipboard-types'
import { TLExternalContent } from './editor/types/external-content'
import { TLCameraOptions } from './editor/types/misc-types'
import { VecLike } from './primitives/Vec'
import { TLDeepLinkOptions } from './utils/deepLinks'
import { TLTextOptions } from './utils/richText'
/**
* Identifies how a clipboard write was triggered (copy vs cut, keyboard vs menu).
*
* @public
*/
export interface TLClipboardWriteInfo {
readonly operation: 'copy' | 'cut'
readonly source: 'native' | 'menu'
}
/**
* Raw clipboard paste payload, before tldraw parses clipboard contents into {@link TLExternalContent}.
*
* - `native-event`: from the `paste` event — `clipboardData` is available synchronously (unlike async
* `navigator.clipboard.read()`).
* - `clipboard-read`: from an explicit `navigator.clipboard.read()` call — only `ClipboardItem[]`
* exists
* (no `DataTransfer`).
*
* @public
*/
export type TLClipboardPasteRawInfo =
| {
readonly editor: Editor
readonly source: 'native-event'
readonly event: ClipboardEvent
readonly clipboardData: DataTransfer | null
readonly point: VecLike | undefined
}
| {
readonly editor: Editor
readonly source: 'clipboard-read'
readonly clipboardItems: readonly ClipboardItem[]
readonly point: VecLike | undefined
}
/**
* Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.
*
* @example
* ```tsx
* const options: Partial<TldrawOptions> = {
* maxPages: 3,
* maxShapesPerPage: 1000,
* }
*
* function MyTldrawComponent() {
* return <Tldraw options={options} />
* }
* ```
*
* @public
*/
export interface TldrawOptions {
readonly maxShapesPerPage: number
readonly maxFilesAtOnce: number
readonly maxPages: number
readonly animationMediumMs: number
readonly followChaseViewportSnap: number
readonly doubleClickDurationMs: number
readonly multiClickDurationMs: number
readonly coarseDragDistanceSquared: number
readonly dragDistanceSquared: number
readonly uiDragDistanceSquared: number
readonly uiCoarseDragDistanceSquared: number
readonly defaultSvgPadding: number
readonly cameraSlideFriction: number
readonly gridSteps: readonly {
readonly min: number
readonly mid: number
readonly step: number
}[]
readonly collaboratorInactiveTimeoutMs: number
readonly collaboratorIdleTimeoutMs: number
readonly collaboratorCheckIntervalMs: number
readonly cameraMovingTimeoutMs: number
readonly hitTestMargin: number
readonly edgeScrollDelay: number
readonly edgeScrollEaseDuration: number
readonly edgeScrollSpeed: number
readonly edgeScrollDistance: number
readonly coarsePointerWidth: number
readonly coarseHandleRadius: number
readonly handleRadius: number
readonly longPressDurationMs: number
readonly textShadowLod: number
readonly adjacentShapeMargin: number
readonly flattenImageBoundsExpand: number
readonly flattenImageBoundsPadding: number
readonly laserDelayMs: number
/**
* How long (in milliseconds) to fade all laser scribbles after the session ends.
* The total points across all scribbles will be removed proportionally over this duration.
* Defaults to 500ms (0.5 seconds).
*/
readonly laserFadeoutMs: number
readonly maxExportDelayMs: number
readonly tooltipDelayMs: number
/**
* How long should previews created by {@link Editor.createTemporaryAssetPreview} last before
* they expire? Defaults to 3 minutes.
*/
readonly temporaryAssetPreviewLifetimeMs: number
readonly actionShortcutsLocation: 'menu' | 'toolbar' | 'swap'
readonly createTextOnCanvasDoubleClick: boolean
/**
* The react provider to use when exporting an image. This is useful if your shapes depend on
* external context providers. By default, this is `React.Fragment`.
*/
readonly exportProvider: ComponentType<{ children: React.ReactNode }>
/**
* By default, the toolbar items are accessible via number shortcuts according to their order. To disable this, set this option to false.
*/
readonly enableToolbarKeyboardShortcuts: boolean
/**
* The maximum number of fonts that will be loaded while blocking the main rendering of the
* canvas. If there are more than this number of fonts needed, we'll just show the canvas right
* away and let the fonts load in in the background.
*/
readonly maxFontsToLoadBeforeRender: number
/**
* If you have a CSP policy that blocks inline styles, you can use this prop to provide a
* nonce to use in the editor's styles.
*/
readonly nonce: string | undefined
/**
* Branding name of the app, currently only used for adding aria-label for the application.
*/
readonly branding?: string
/**
* Whether to use debounced zoom level for certain rendering optimizations. When true,
* `editor.getEfficientZoomLevel()` returns a cached zoom value while the camera is moving,
* reducing re-renders. When false, it always returns the current zoom level.
*/
readonly debouncedZoom: boolean
/**
* The number of shapes that must be on the page for the debounced zoom level to be used.
* Defaults to 500 shapes.
*/
readonly debouncedZoomThreshold: number
/**
* Whether to allow spacebar panning. When true, the spacebar will pan the camera when held down.
* When false, the spacebar will not pan the camera.
*/
readonly spacebarPanning: boolean
/**
* Whether to allow right-click + drag to pan the camera. When true, right-click + drag pans the
* camera and a static right-click opens the context menu at the release position. When false,
* right-click opens the context menu on press (no drag-to-pan).
*/
readonly rightClickPanning: boolean
/**
* The default padding (in pixels) used when zooming to fit content in the viewport.
* This affects methods like `zoomToFit()`, `zoomToSelection()`, and `zoomToBounds()`.
* The actual padding used is the minimum of this value and 28% of the viewport width.
* Defaults to 128 pixels.
*/
readonly zoomToFitPadding: number
/**
* The distance (in screen pixels) at which shapes snap to guides and other shapes.
*/
readonly snapThreshold: number
/**
* Options for the editor's camera. These are the initial camera options.
* Use {@link Editor.setCameraOptions} to update camera options at runtime.
*/
readonly camera: Partial<TLCameraOptions>
/**
* Options for the editor's text rendering. These include TipTap configuration and
* font handling. These are the initial text options and cannot be changed at runtime.
*/
readonly text: TLTextOptions
/**
* Options for syncing the editor's camera state with the URL. Set to `true` to enable
* with default options, or pass an options object to customize behavior.
*
* @example
* ```tsx
* // Enable with defaults
* <Tldraw options={{ deepLinks: true }} />
*
* // Enable with custom options
* <Tldraw options={{ deepLinks: { param: 'd', debounceMs: 500 } }} />
* ```
*/
readonly deepLinks: true | TLDeepLinkOptions | undefined
/**
* Whether the quick-zoom brush preserves its screen-pixel size when the user
* zooms the overview. When true, zooming in shrinks the target viewport (higher
* return zoom); zooming out expands it. When false, the brush keeps the original
* viewport's page dimensions regardless of overview zoom changes.
*/
readonly quickZoomPreservesScreenBounds: boolean
/**
* Called before content is written to the clipboard during a copy or cut operation.
* Receives the serialized content (shapes, bindings, assets) and can filter or transform
* it before it reaches the clipboard.
*
* Return a modified `TLContent` object to change what is copied or cut. Return `false` to
* cancel the clipboard write (for cut, the selected shapes are not removed). Return `void`
* (or `undefined`) to pass through unchanged. You may return a `Promise` of those values if
* the hook is async.
*
* @example
* ```tsx
* // Filter out "locked" shapes from copy
* onBeforeCopyToClipboard({ content, operation }) {
* return {
* ...content,
* shapes: content.shapes.filter(s => !s.meta.locked),
* rootShapeIds: content.rootShapeIds.filter(id =>
* content.shapes.find(s => s.id === id && !s.meta.locked)
* ),
* }
* }
* ```
*/
onBeforeCopyToClipboard?(
info: { editor: Editor; content: TLContent } & TLClipboardWriteInfo
): Awaitable<TLContent | false | void>
/**
* Called before pasted content is processed and shapes are created. Receives the parsed
* external content from the clipboard and can filter, transform, or cancel it.
*
* Return `false` to cancel the paste. Return a modified content object to transform it.
* Return `void` (or `undefined`) to pass through unchanged. You may return a `Promise` of
* those values if the hook is async.
*
* This only fires for clipboard paste operations (keyboard shortcuts and menu actions),
* not for file drops or programmatic `putExternalContent` calls.
*
* @example
* ```tsx
* // Block pasting of image files
* onBeforePasteFromClipboard({ content }) {
* if (content.type === 'files') {
* const nonImages = content.files.filter(f => !f.type.startsWith('image/'))
* if (nonImages.length === 0) return false
* return { ...content, files: nonImages }
* }
* }
* ```
*/
onBeforePasteFromClipboard?(info: {
editor: Editor
content: TLExternalContent<unknown>
source: 'native-event' | 'clipboard-read'
point?: VecLike
}): Awaitable<TLExternalContent<unknown> | false | void>
/**
* Called first for keyboard and menu paste, **before** tldraw handles or parses clipboard data
* (and before {@link TldrawOptions.onBeforePasteFromClipboard}).
*
* Return `false` to cancel tldraw's default paste handling for this gesture (same convention as
* {@link TldrawOptions.onBeforePasteFromClipboard}). Use this when you handle paste yourself from
* raw clipboard data, or to block the gesture entirely. Return `void` (or `undefined`) to continue.
*/
onClipboardPasteRaw?(info: TLClipboardPasteRawInfo): false | void
/**
* Called when content is dropped on the canvas. Provides the page position
* where the drop occurred and the underlying drag event object.
* Return true to prevent default drop handling (files, URLs, etc.)
*/
experimental__onDropOnCanvas?(options: {
point: VecLike
event: React.DragEvent<Element>
}): boolean
}
/** @public */
export const defaultTldrawOptions = {
maxShapesPerPage: 4000,
maxFilesAtOnce: 100,
maxPages: 40,
animationMediumMs: 320,
followChaseViewportSnap: 2,
doubleClickDurationMs: 450,
multiClickDurationMs: 200,
coarseDragDistanceSquared: 36, // 6 squared
dragDistanceSquared: 16, // 4 squared
uiDragDistanceSquared: 16, // 4 squared
// it's really easy to accidentally drag from the toolbar on mobile, so we use a much larger
// threshold than usual here to try and prevent accidental drags.
uiCoarseDragDistanceSquared: 625, // 25 squared
defaultSvgPadding: 32,
cameraSlideFriction: 0.09,
gridSteps: [
{ min: -1, mid: 0.15, step: 64 },
{ min: 0.05, mid: 0.375, step: 16 },
{ min: 0.15, mid: 1, step: 4 },
{ min: 0.7, mid: 2.5, step: 1 },
],
collaboratorInactiveTimeoutMs: 60000,
collaboratorIdleTimeoutMs: 3000,
collaboratorCheckIntervalMs: 1200,
cameraMovingTimeoutMs: 64,
hitTestMargin: 8,
edgeScrollDelay: 200,
edgeScrollEaseDuration: 200,
edgeScrollSpeed: 25,
edgeScrollDistance: 8,
coarsePointerWidth: 12,
coarseHandleRadius: 20,
handleRadius: 12,
longPressDurationMs: 500,
textShadowLod: 0.35,
adjacentShapeMargin: 10,
flattenImageBoundsExpand: 64,
flattenImageBoundsPadding: 16,
laserDelayMs: 1200,
laserFadeoutMs: 500,
maxExportDelayMs: 5000,
tooltipDelayMs: 700,
temporaryAssetPreviewLifetimeMs: 180000,
actionShortcutsLocation: 'swap',
createTextOnCanvasDoubleClick: true,
exportProvider: Fragment,
enableToolbarKeyboardShortcuts: true,
maxFontsToLoadBeforeRender: Infinity,
nonce: undefined,
debouncedZoom: true,
debouncedZoomThreshold: 500,
spacebarPanning: true,
rightClickPanning: true,
zoomToFitPadding: 128,
snapThreshold: 8,
camera: DEFAULT_CAMERA_OPTIONS,
text: {},
deepLinks: undefined,
quickZoomPreservesScreenBounds: true,
onBeforeCopyToClipboard: undefined,
onBeforePasteFromClipboard: undefined,
onClipboardPasteRaw: undefined,
experimental__onDropOnCanvas: undefined,
} as const satisfies TldrawOptions