Drag and drop

You can create custom shapes that can be dragged and dropped onto each other.

import {
	Circle2d,
	Geometry2d,
	HTMLContainer,
	Rectangle2d,
	ShapeUtil,
	TLBaseShape,
	TLShape,
	Tldraw,
} from 'tldraw'
import 'tldraw/tldraw.css'

// There's a guide at the bottom of this file!

// [1]
type MyGridShape = TLBaseShape<'my-grid-shape', Record<string, never>>
type MyCounterShape = TLBaseShape<'my-counter-shape', Record<string, never>>

// [2]
const SLOT_SIZE = 100
class MyCounterShapeUtil extends ShapeUtil<MyCounterShape> {
	static override type = 'my-counter-shape' as const

	override canResize() {
		return false
	}
	override hideResizeHandles() {
		return true
	}

	getDefaultProps(): MyCounterShape['props'] {
		return {}
	}

	getGeometry(): Geometry2d {
		return new Circle2d({ radius: SLOT_SIZE / 2 - 10, isFilled: true })
	}

	component() {
		return (
			<HTMLContainer
				style={{
					backgroundColor: '#e03131',
					border: '1px solid #ff8787',
					borderRadius: '50%',
				}}
			/>
		)
	}

	indicator() {
		return <circle r={SLOT_SIZE / 2 - 10} cx={SLOT_SIZE / 2 - 10} cy={SLOT_SIZE / 2 - 10} />
	}
}

// [3]
class MyGridShapeUtil extends ShapeUtil<MyGridShape> {
	static override type = 'my-grid-shape' as const

	getDefaultProps(): MyGridShape['props'] {
		return {}
	}

	getGeometry(): Geometry2d {
		return new Rectangle2d({
			width: SLOT_SIZE * 5,
			height: SLOT_SIZE * 2,
			isFilled: true,
		})
	}

	override canResize() {
		return false
	}
	override hideResizeHandles() {
		return true
	}

	// [a]
	override canDropShapes(_shape: MyGridShape, shapes: TLShape[]) {
		if (shapes.every((s) => s.type === 'my-counter-shape')) {
			return true
		}
		return false
	}

	// [b]
	override onDragShapesOver(shape: MyGridShape, shapes: TLShape[]) {
		if (!shapes.every((child) => child.parentId === shape.id)) {
			this.editor.reparentShapes(shapes, shape.id)
		}
	}

	// [c]
	override onDragShapesOut(_shape: MyGridShape, shapes: TLShape[]) {
		this.editor.reparentShapes(shapes, this.editor.getCurrentPageId())
	}

	component() {
		return (
			<HTMLContainer
				style={{
					backgroundColor: '#efefef',
					borderRight: '1px solid #ccc',
					borderBottom: '1px solid #ccc',
					backgroundSize: `${SLOT_SIZE}px ${SLOT_SIZE}px`,
					backgroundImage: `
						linear-gradient(to right, #ccc 1px, transparent 1px),
						linear-gradient(to bottom, #ccc 1px, transparent 1px)
					`,
				}}
			/>
		)
	}

	indicator() {
		return <rect width={SLOT_SIZE * 5} height={SLOT_SIZE * 2} />
	}
}

export default function DragAndDropExample() {
	return (
		<div className="tldraw__editor">
			<Tldraw
				shapeUtils={[MyGridShapeUtil, MyCounterShapeUtil]}
				onMount={(editor) => {
					editor.createShape({ type: 'my-grid-shape', x: 100, y: 100 })
					editor.createShape({ type: 'my-counter-shape', x: 700, y: 100 })
					editor.createShape({ type: 'my-counter-shape', x: 750, y: 200 })
					editor.createShape({ type: 'my-counter-shape', x: 770, y: 300 })
				}}
			/>
		</div>
	)
}

/*

This example demonstrates how to use the drag-and-drop system.

[1] Define some shape types. For the purposes of this example, we'll define two
shapes: a grid and a counter.

[2] Make a shape util for the first shape. For this example, we'll make a simple
red circle that you drag and drop onto the other shape.

[3] Make the other shape util. In this example, we'll make a grid that you can
place the the circle counters onto.

    [a] Use the `canDropShapes` method to specify which shapes can be dropped onto
    the grid shape.

    [b] Use the `onDragShapesOver` method to reparent counters to the grid shape
    when they are dragged on top.

    [c] Use the `onDragShapesOut` method to reparent counters back to the page
    when they are dragged off.

*/
Prev
Custom tool
Next
Interactive shape