UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

347 lines (342 loc) 12.7 kB
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