UNPKG

@tldraw/editor

Version:

A tiny little drawing app (editor).

749 lines (677 loc) • 19.2 kB
/* eslint-disable @typescript-eslint/no-unused-vars */ import { EMPTY_ARRAY } from '@tldraw/state' import { LegacyMigrations, MigrationSequence } from '@tldraw/store' import { RecordProps, TLHandle, TLPropsMigrations, TLShape, TLShapeCrop, TLShapePartial, TLUnknownShape, } from '@tldraw/tlschema' import { ReactElement } from 'react' import { Box, SelectionHandle } from '../../primitives/Box' import { Vec } from '../../primitives/Vec' import { Geometry2d } from '../../primitives/geometry/Geometry2d' import type { Editor } from '../Editor' import { TLFontFace } from '../managers/FontManager' import { BoundsSnapGeometry } from '../managers/SnapManager/BoundsSnaps' import { HandleSnapGeometry } from '../managers/SnapManager/HandleSnaps' import { SvgExportContext } from '../types/SvgExportContext' import { TLResizeHandle } from '../types/selection-types' /** @public */ export interface TLShapeUtilConstructor< T extends TLUnknownShape, U extends ShapeUtil<T> = ShapeUtil<T>, > { new (editor: Editor): U type: T['type'] props?: RecordProps<T> migrations?: LegacyMigrations | TLPropsMigrations | MigrationSequence } /** * Options passed to {@link ShapeUtil.canBind}. A binding that could be made. At least one of * `fromShapeType` or `toShapeType` will belong to this shape util. * * @public */ export interface TLShapeUtilCanBindOpts<Shape extends TLUnknownShape = TLUnknownShape> { /** The type of shape referenced by the `fromId` of the binding. */ fromShapeType: string /** The type of shape referenced by the `toId` of the binding. */ toShapeType: string /** The type of binding. */ bindingType: string } /** * Options passed to {@link ShapeUtil.canBeLaidOut}. * * @public */ export interface TLShapeUtilCanBeLaidOutOpts { /** The type of action causing the layout. */ type?: 'align' | 'distribute' | 'pack' | 'stack' | 'flip' | 'stretch' /** The other shapes being laid out */ shapes?: TLShape[] } /** Additional options for the {@link ShapeUtil.getGeometry} method. * * @public */ export interface TLGeometryOpts { /** The context in which the geometry is being requested. */ context?: string } /** @public */ export interface TLShapeUtilCanvasSvgDef { key: string component: React.ComponentType } /** @public */ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> { /** Configure this shape utils {@link ShapeUtil.options | `options`}. */ static configure<T extends TLShapeUtilConstructor<any, any>>( this: T, options: T extends new (...args: any[]) => { options: infer Options } ? Partial<Options> : never ): T { // @ts-expect-error -- typescript has no idea what's going on here but it's fine return class extends this { // @ts-expect-error options = { ...this.options, ...options } } } constructor(public editor: Editor) {} /** * Options for this shape util. If you're implementing a custom shape util, you can override * this to provide customization options for your shape. If using an existing shape util, you * can customizing this by calling {@link ShapeUtil.configure}. */ options = {} /** * Props allow you to define the shape's properties in a way that the editor can understand. * This has two main uses: * * 1. Validation. Shapes will be validated using these props to stop bad data from being saved. * 2. Styles. Each {@link @tldraw/tlschema#StyleProp} in the props can be set on many shapes at * once, and will be remembered from one shape to the next. * * @example * ```tsx * import {T, TLBaseShape, TLDefaultColorStyle, DefaultColorStyle, ShapeUtil} from 'tldraw' * * type MyShape = TLBaseShape<'mine', { * color: TLDefaultColorStyle, * text: string, * }> * * class MyShapeUtil extends ShapeUtil<MyShape> { * static props = { * // we use tldraw's built-in color style: * color: DefaultColorStyle, * // validate that the text prop is a string: * text: T.string, * } * } * ``` */ static props?: RecordProps<TLUnknownShape> /** * Migrations allow you to make changes to a shape's props over time. Read the * {@link https://www.tldraw.dev/docs/persistence#Shape-props-migrations | shape prop migrations} * guide for more information. */ static migrations?: LegacyMigrations | TLPropsMigrations | MigrationSequence /** * The type of the shape util, which should match the shape's type. * * @public */ static type: string /** * Get the default props for a shape. * * @public */ abstract getDefaultProps(): Shape['props'] /** * Get the shape's geometry. * * @param shape - The shape. * @param opts - Additional options for the request. * @public */ abstract getGeometry(shape: Shape, opts?: TLGeometryOpts): Geometry2d /** * Get a JSX element for the shape (as an HTML element). * * @param shape - The shape. * @public */ abstract component(shape: Shape): any /** * Get JSX describing the shape's indicator (as an SVG element). * * @param shape - The shape. * @public */ abstract indicator(shape: Shape): any /** * Get the font faces that should be rendered in the document in order for this shape to render * correctly. * * @param shape - The shape. * @public */ getFontFaces(shape: Shape): TLFontFace[] { return EMPTY_ARRAY } /** * Whether the shape can be snapped to by another shape. * * @param shape - The shape. * @public */ canSnap(_shape: Shape): boolean { return true } /** * Whether the shape can be scrolled while editing. * * @public */ canScroll(_shape: Shape): boolean { return false } /** * Whether the shape can be bound to. See {@link TLShapeUtilCanBindOpts} for details. * * @public */ canBind(_opts: TLShapeUtilCanBindOpts): boolean { return true } /** * Whether the shape can be double clicked to edit. * * @public */ canEdit(_shape: Shape): boolean { return false } /** * Whether the shape can be resized. * * @public */ canResize(_shape: Shape): boolean { return true } /** * Whether the shape can be edited in read-only mode. * * @public */ canEditInReadOnly(_shape: Shape): boolean { return false } /** * Whether the shape can be cropped. * * @public */ canCrop(_shape: Shape): boolean { return false } /** * Whether the shape can participate in layout functions such as alignment or distribution. * * @param shape - The shape. * @param info - Additional context information: the type of action causing the layout and the * @public * * @public */ canBeLaidOut(_shape: Shape, _info: TLShapeUtilCanBeLaidOutOpts): boolean { return true } /** * Does this shape provide a background for its children? If this is true, * then any children with a `renderBackground` method will have their * backgrounds rendered _above_ this shape. Otherwise, the children's * backgrounds will be rendered above either the next ancestor that provides * a background, or the canvas background. * * @internal */ providesBackgroundForChildren(_shape: Shape): boolean { return false } /** * Whether the shape should hide its resize handles when selected. * * @public */ hideResizeHandles(_shape: Shape): boolean { return false } /** * Whether the shape should hide its rotation handles when selected. * * @public */ hideRotateHandle(_shape: Shape): boolean { return false } /** * Whether the shape should hide its selection bounds background when selected. * * @public */ hideSelectionBoundsBg(_shape: Shape): boolean { return false } /** * Whether the shape should hide its selection bounds foreground when selected. * * @public */ hideSelectionBoundsFg(_shape: Shape): boolean { return false } /** * Whether the shape's aspect ratio is locked. * * @public */ isAspectRatioLocked(_shape: Shape): boolean { return false } /** * Get a JSX element for the shape (as an HTML element) to be rendered as part of the canvas background - behind any other shape content. * * @param shape - The shape. * @internal */ backgroundComponent?(shape: Shape): any /** * Get the interpolated props for an animating shape. This is an optional method. * * @example * * ```ts * util.getInterpolatedProps?.(startShape, endShape, t) * ``` * * @param startShape - The initial shape. * @param endShape - The initial shape. * @param progress - The normalized progress between zero (start) and 1 (end). * @public */ getInterpolatedProps?(startShape: Shape, endShape: Shape, progress: number): Shape['props'] /** * Get an array of handle models for the shape. This is an optional method. * * @example * * ```ts * util.getHandles?.(myShape) * ``` * * @param shape - The shape. * @public */ getHandles?(shape: Shape): TLHandle[] /** * Get whether the shape can receive children of a given type. * * @param shape - The shape. * @param type - The shape type. * @public */ canReceiveNewChildrenOfType(_shape: Shape, _type: TLShape['type']) { return false } /** * Get whether the shape can receive children of a given type. * * @param shape - The shape type. * @param shapes - The shapes that are being dropped. * @public */ canDropShapes(_shape: Shape, _shapes: TLShape[]) { return false } /** * Get the shape as an SVG object. * * @param shape - The shape. * @param ctx - The export context for the SVG - used for adding e.g. \<def\>s * @returns An SVG element. * @public */ toSvg?(shape: Shape, ctx: SvgExportContext): ReactElement | null | Promise<ReactElement | null> /** * Get the shape's background layer as an SVG object. * * @param shape - The shape. * @param ctx - ctx - The export context for the SVG - used for adding e.g. \<def\>s * @returns An SVG element. * @public */ toBackgroundSvg?( shape: Shape, ctx: SvgExportContext ): ReactElement | null | Promise<ReactElement | null> /** @internal */ expandSelectionOutlinePx(shape: Shape): number | Box { return 0 } /** * Return elements to be added to the \<defs\> section of the canvases SVG context. This can be * used to define SVG content (e.g. patterns & masks) that can be referred to by ID from svg * elements returned by `component`. * * Each def should have a unique `key`. If multiple defs from different shapes all have the same * key, only one will be used. */ getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] { return [] } /** * Get the geometry to use when snapping to this this shape in translate/resize operations. See * {@link BoundsSnapGeometry} for details. */ getBoundsSnapGeometry(_shape: Shape): BoundsSnapGeometry { return {} } /** * Get the geometry to use when snapping handles to this shape. See {@link HandleSnapGeometry} * for details. */ getHandleSnapGeometry(_shape: Shape): HandleSnapGeometry { return {} } getText(_shape: Shape): string | undefined { return undefined } // Events /** * A callback called just before a shape is created. This method provides a last chance to modify * the created shape. * * @example * * ```ts * onBeforeCreate = (next) => { * return { ...next, x: next.x + 1 } * } * ``` * * @param next - The next shape. * @returns The next shape or void. * @public */ onBeforeCreate?(next: Shape): Shape | void /** * A callback called just before a shape is updated. This method provides a last chance to modify * the updated shape. * * @example * * ```ts * onBeforeUpdate = (prev, next) => { * if (prev.x === next.x) { * return { ...next, x: next.x + 1 } * } * } * ``` * * @param prev - The previous shape. * @param next - The next shape. * @returns The next shape or void. * @public */ onBeforeUpdate?(prev: Shape, next: Shape): Shape | void /** * A callback called when a shape changes from a crop. * * @param shape - The shape at the start of the crop. * @param info - Info about the crop. * @returns A change to apply to the shape, or void. * @public */ onCrop?( shape: Shape, info: TLCropInfo<Shape> ): Omit<TLShapePartial<Shape>, 'id' | 'type'> | undefined | void /** * A callback called when some other shapes are dragged over this one. * * @example * * ```ts * onDragShapesOver = (shape, shapes) => { * this.editor.reparentShapes(shapes, shape.id) * } * ``` * * @param shape - The shape. * @param shapes - The shapes that are being dragged over this one. * @public */ onDragShapesOver?(shape: Shape, shapes: TLShape[]): void /** * A callback called when some other shapes are dragged out of this one. * * @param shape - The shape. * @param shapes - The shapes that are being dragged out. * @public */ onDragShapesOut?(shape: Shape, shapes: TLShape[]): void /** * A callback called when some other shapes are dropped over this one. * * @param shape - The shape. * @param shapes - The shapes that are being dropped over this one. * @public */ onDropShapesOver?(shape: Shape, shapes: TLShape[]): void /** * A callback called when a shape starts being resized. * * @param shape - The shape. * @returns A change to apply to the shape, or void. * @public */ onResizeStart?(shape: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape changes from a resize. * * @param shape - The shape at the start of the resize. * @param info - Info about the resize. * @returns A change to apply to the shape, or void. * @public */ onResize?( shape: Shape, info: TLResizeInfo<Shape> ): Omit<TLShapePartial<Shape>, 'id' | 'type'> | undefined | void /** * A callback called when a shape finishes resizing. * * @param initial - The shape at the start of the resize. * @param current - The current shape. * @returns A change to apply to the shape, or void. * @public */ onResizeEnd?(initial: Shape, current: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape starts being translated. * * @param shape - The shape. * @returns A change to apply to the shape, or void. * @public */ onTranslateStart?(shape: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape changes from a translation. * * @param initial - The shape at the start of the translation. * @param current - The current shape. * @returns A change to apply to the shape, or void. * @public */ onTranslate?(initial: Shape, current: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape finishes translating. * * @param initial - The shape at the start of the translation. * @param current - The current shape. * @returns A change to apply to the shape, or void. * @public */ onTranslateEnd?(initial: Shape, current: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape's handle changes. * * @param shape - The current shape. * @param info - An object containing the handle and whether the handle is 'precise' or not. * @returns A change to apply to the shape, or void. * @public */ onHandleDrag?(shape: Shape, info: TLHandleDragInfo<Shape>): TLShapePartial<Shape> | void /** * A callback called when a shape starts being rotated. * * @param shape - The shape. * @returns A change to apply to the shape, or void. * @public */ onRotateStart?(shape: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape changes from a rotation. * * @param initial - The shape at the start of the rotation. * @param current - The current shape. * @returns A change to apply to the shape, or void. * @public */ onRotate?(initial: Shape, current: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape finishes rotating. * * @param initial - The shape at the start of the rotation. * @param current - The current shape. * @returns A change to apply to the shape, or void. * @public */ onRotateEnd?(initial: Shape, current: Shape): TLShapePartial<Shape> | void /** * Not currently used. * * @internal */ onBindingChange?(shape: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape's children change. * * @param shape - The shape. * @returns An array of shape updates, or void. * @public */ onChildrenChange?(shape: Shape): TLShapePartial[] | void /** * A callback called when a shape's handle is double clicked. * * @param shape - The shape. * @param handle - The handle that is double-clicked. * @returns A change to apply to the shape, or void. * @public */ onDoubleClickHandle?(shape: Shape, handle: TLHandle): TLShapePartial<Shape> | void /** * A callback called when a shape's edge is double clicked. * * @param shape - The shape. * @returns A change to apply to the shape, or void. * @public */ onDoubleClickEdge?(shape: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape is double clicked. * * @param shape - The shape. * @returns A change to apply to the shape, or void. * @public */ onDoubleClick?(shape: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape is clicked. * * @param shape - The shape. * @returns A change to apply to the shape, or void. * @public */ onClick?(shape: Shape): TLShapePartial<Shape> | void /** * A callback called when a shape finishes being editing. * * @param shape - The shape. * @public */ onEditEnd?(shape: Shape): void } /** * Info about a crop. * @param handle - The handle being dragged. * @param change - The distance the handle is moved. * @param initialShape - The shape at the start of the resize. * @public */ export interface TLCropInfo<T extends TLShape> { handle: SelectionHandle change: Vec crop: TLShapeCrop uncroppedSize: { w: number; h: number } initialShape: T } /** * The type of resize. * * 'scale_shape' - The shape is being scaled, usually as part of a larger selection. * * 'resize_bounds' - The user is directly manipulating an individual shape's bounds using a resize * handle. It is up to shape util implementers to decide how they want to handle the two * situations. * * @public */ export type TLResizeMode = 'scale_shape' | 'resize_bounds' /** * Info about a resize. * @param newPoint - The new local position of the shape. * @param handle - The handle being dragged. * @param mode - The type of resize. * @param scaleX - The scale in the x-axis. * @param scaleY - The scale in the y-axis. * @param initialBounds - The bounds of the shape at the start of the resize. * @param initialShape - The shape at the start of the resize. * @public */ export interface TLResizeInfo<T extends TLShape> { newPoint: Vec handle: TLResizeHandle mode: TLResizeMode scaleX: number scaleY: number initialBounds: Box initialShape: T } /* -------------------- Dragging -------------------- */ /** @public */ export interface TLHandleDragInfo<T extends TLShape> { handle: TLHandle isPrecise: boolean initial?: T | undefined }