Custom shape examples (#2994)
This PR: - adds a simple custom shape example - adds an interactive shape example - updates editable shape example closes TLD-2118 - [x] `documentation` — Changes to the documentation only[^2] ### Release Notes - adds a simple custom shape example - adds an interactive shape example - updates editable shape example --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
f819a57a05
commit
8658e20ab5
13 changed files with 462 additions and 306 deletions
|
@ -24,5 +24,5 @@
|
||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
"watcher.ts"
|
"watcher.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", ".next"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: Custom shapes / tools
|
title: Custom shape and tool
|
||||||
component: ./CustomConfigExample.tsx
|
component: ./CustomConfigExample.tsx
|
||||||
category: shapes/tools
|
category: shapes/tools
|
||||||
priority: 1
|
priority: 3
|
||||||
---
|
---
|
||||||
|
|
||||||
Create custom shapes / tools
|
Create custom shapes / tools
|
||||||
|
|
145
apps/examples/src/examples/custom-shape/CustomShapeExample.tsx
Normal file
145
apps/examples/src/examples/custom-shape/CustomShapeExample.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import {
|
||||||
|
Geometry2d,
|
||||||
|
HTMLContainer,
|
||||||
|
Rectangle2d,
|
||||||
|
ShapeProps,
|
||||||
|
ShapeUtil,
|
||||||
|
T,
|
||||||
|
TLBaseShape,
|
||||||
|
TLOnResizeHandler,
|
||||||
|
Tldraw,
|
||||||
|
resizeBox,
|
||||||
|
} from 'tldraw'
|
||||||
|
import 'tldraw/tldraw.css'
|
||||||
|
|
||||||
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
// [1]
|
||||||
|
type ICustomShape = TLBaseShape<
|
||||||
|
'my-custom-shape',
|
||||||
|
{
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
// [2]
|
||||||
|
export class MyShapeUtil extends ShapeUtil<ICustomShape> {
|
||||||
|
// [a]
|
||||||
|
static override type = 'my-custom-shape' as const
|
||||||
|
static override props: ShapeProps<ICustomShape> = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
text: T.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// [b]
|
||||||
|
getDefaultProps(): ICustomShape['props'] {
|
||||||
|
return {
|
||||||
|
w: 200,
|
||||||
|
h: 200,
|
||||||
|
text: "I'm a shape!",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [c]
|
||||||
|
override canBind = () => true
|
||||||
|
override canEdit = () => false
|
||||||
|
override canResize = () => true
|
||||||
|
override isAspectRatioLocked = () => false
|
||||||
|
|
||||||
|
// [d]
|
||||||
|
getGeometry(shape: ICustomShape): Geometry2d {
|
||||||
|
return new Rectangle2d({
|
||||||
|
width: shape.props.w,
|
||||||
|
height: shape.props.h,
|
||||||
|
isFilled: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// [e]
|
||||||
|
override onResize: TLOnResizeHandler<any> = (shape, info) => {
|
||||||
|
return resizeBox(shape, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [f]
|
||||||
|
component(shape: ICustomShape) {
|
||||||
|
return <HTMLContainer style={{ backgroundColor: '#efefef' }}>{shape.props.text}</HTMLContainer>
|
||||||
|
}
|
||||||
|
|
||||||
|
// [g]
|
||||||
|
indicator(shape: ICustomShape) {
|
||||||
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [3]
|
||||||
|
const customShape = [MyShapeUtil]
|
||||||
|
export default function CustomShapeExample() {
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw
|
||||||
|
shapeUtils={customShape}
|
||||||
|
onMount={(editor) => {
|
||||||
|
editor.createShape({ type: 'my-custom-shape', x: 100, y: 100 })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Introduction:
|
||||||
|
|
||||||
|
You can create custom shapes in tldraw by creating a shape util and passing it to the Tldraw component.
|
||||||
|
In this example, we'll create a custom shape that is a simple rectangle with some text inside of it.
|
||||||
|
|
||||||
|
[1]
|
||||||
|
Define the shape type. This is a type that extend the `TLBaseShape` generic and defines the shape's
|
||||||
|
props. We need to pass in a unique string literal for the shape's type and an object that defines the
|
||||||
|
shape's props.
|
||||||
|
|
||||||
|
[2]
|
||||||
|
This is our shape util. In tldraw shape utils are classes that define how a shape behaves and renders.
|
||||||
|
We can extend the ShapeUtil class and provide the shape type as a generic. If we extended the
|
||||||
|
BaseBoxShapeUtil class instead, we wouldn't have define methods such as `getGeometry` and `onResize`.
|
||||||
|
|
||||||
|
[a]
|
||||||
|
This is where we define out shape's props and type for the editor. It's important to use the same
|
||||||
|
string for the type as we did in [1]. We need to define the shape's props using tldraw's validator
|
||||||
|
library. The validator will help make sure the store always has shape data we can trust.
|
||||||
|
|
||||||
|
[b]
|
||||||
|
This is a method that returns the default props for our shape.
|
||||||
|
|
||||||
|
[c]
|
||||||
|
Some handy methods for controlling different shape behaviour. You don't have to define these, and
|
||||||
|
they're only shown here so you know they exist. Check out the editable shape example to learn more
|
||||||
|
about creating an editable shape.
|
||||||
|
|
||||||
|
[d]
|
||||||
|
The getGeometry method is what the editor uses for hit-testing, binding etc. We're using the
|
||||||
|
Rectangle2d class from tldraw's geometry library to create a rectangle shape. If we extended the
|
||||||
|
BaseBoxShapeUtil class, we wouldn't have to define this method.
|
||||||
|
|
||||||
|
[e]
|
||||||
|
We're using the resizeBox utility method to handle resizing our shape. If we extended the
|
||||||
|
BaseBoxShapeUtil class, we wouldn't have to define this method.
|
||||||
|
|
||||||
|
[f]
|
||||||
|
The component method defines how our shape renders. We're returning an HTMLContainer here, which
|
||||||
|
is a handy component that tldraw exports. It's essentially a div with some special css. There's a
|
||||||
|
lot of flexibility here, and you can use any React hooks you want and return any valid JSX.
|
||||||
|
|
||||||
|
[g]
|
||||||
|
The indicator is the blue outline around a selected shape. We're just returning a rectangle with the
|
||||||
|
same width and height as the shape here. You can return any valid JSX here.
|
||||||
|
|
||||||
|
[3]
|
||||||
|
This is where we render the Tldraw component with our custom shape. We're passing in our custom shape
|
||||||
|
util as an array to the shapeUtils prop. We're also using the onMount callback to create a shape on
|
||||||
|
the canvas. If you want to learn how to add a tool for your shape, check out the custom config example.
|
||||||
|
If you want to learn how to programmatically control the canvas, check out the Editor API examples.
|
||||||
|
|
||||||
|
*/
|
13
apps/examples/src/examples/custom-shape/README.md
Normal file
13
apps/examples/src/examples/custom-shape/README.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
title: Custom shape
|
||||||
|
component: ./CustomShapeExample.tsx
|
||||||
|
category: shapes/tools
|
||||||
|
priority: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
A simple custom shape.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
You can create custom shapes in tldraw by creating a shape util and passing it to the Tldraw component.
|
||||||
|
In this example, we'll create a custom shape that is a simple rectangle with some text inside of it.
|
|
@ -1,26 +1,19 @@
|
||||||
import { Tldraw } from 'tldraw'
|
import { Tldraw } from 'tldraw'
|
||||||
import 'tldraw/tldraw.css'
|
import 'tldraw/tldraw.css'
|
||||||
import { MyshapeTool } from './my-shape/my-shape-tool'
|
import { EditableShapeUtil } from './EditableShapeUtil'
|
||||||
import { MyshapeUtil } from './my-shape/my-shape-util'
|
|
||||||
import { components, uiOverrides } from './ui-overrides'
|
|
||||||
|
|
||||||
// [1]
|
const customShapeUtils = [EditableShapeUtil]
|
||||||
const customShapeUtils = [MyshapeUtil]
|
|
||||||
const customTools = [MyshapeTool]
|
|
||||||
|
|
||||||
//[2]
|
|
||||||
export default function EditableShapeExample() {
|
export default function EditableShapeExample() {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<Tldraw
|
<Tldraw
|
||||||
// Pass in the array of custom shape classes
|
// Pass in the array of custom shape classes
|
||||||
shapeUtils={customShapeUtils}
|
shapeUtils={customShapeUtils}
|
||||||
// Pass in the array of custom tools
|
// Create a shape when the editor mounts
|
||||||
tools={customTools}
|
onMount={(editor) => {
|
||||||
// Pass in any overrides to the user interface
|
editor.createShape({ type: 'my-editable-shape', x: 100, y: 100 })
|
||||||
overrides={uiOverrides}
|
}}
|
||||||
// pass in the new Keyboard Shortcuts component
|
|
||||||
components={components}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -31,24 +24,12 @@ Introduction:
|
||||||
|
|
||||||
In Tldraw shapes can exist in an editing state. When shapes are in the editing state
|
In Tldraw shapes can exist in an editing state. When shapes are in the editing state
|
||||||
they are focused and can't be dragged, resized or rotated. Shapes enter this state
|
they are focused and can't be dragged, resized or rotated. Shapes enter this state
|
||||||
when they are double-clicked, this means that users can drag and resize shapes without
|
when they are double-clicked. In our default shapes we mostly use this for editing text.
|
||||||
accidentally entering the editing state. In our default shapes we mostly use this for
|
In this example we'll create a shape that renders an emoji and allows the user to change
|
||||||
editing text, but it's also used in our video shape. In this example we'll create a
|
the emoji when the shape is in the editing state.
|
||||||
shape that you could use for a game of Go, but instead of black and white stones, we'll
|
|
||||||
use cats and dogs.
|
|
||||||
|
|
||||||
Most of the relevant code for this is in the my-shape-util.tsx file. We also define a
|
Most of the relevant code for this is in the EditableShapeUtil.tsx file. If you want a more
|
||||||
very simple tool in my-shape-tool.tsx, and make our new tool appear on the toolbar in
|
in-depth explanation of the shape util, check out the custom shape example.
|
||||||
ui-overrides.ts.
|
|
||||||
|
|
||||||
[1]
|
|
||||||
We have to define our array of custom shapes and tools outside of the component. So it
|
|
||||||
doesn't get redefined every time the component re-renders. We'll pass that in to the
|
|
||||||
editors props.
|
|
||||||
|
|
||||||
[2]
|
|
||||||
We pass in our custom shape classes to the Tldraw component as props. We also pass in
|
|
||||||
any uiOverrides we want to use, this is to make sure that our custom tool appears on
|
|
||||||
the toolbar.
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
126
apps/examples/src/examples/editable-shape/EditableShapeUtil.tsx
Normal file
126
apps/examples/src/examples/editable-shape/EditableShapeUtil.tsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import {
|
||||||
|
BaseBoxShapeUtil,
|
||||||
|
HTMLContainer,
|
||||||
|
ShapeProps,
|
||||||
|
T,
|
||||||
|
TLBaseShape,
|
||||||
|
TLOnEditEndHandler,
|
||||||
|
stopEventPropagation,
|
||||||
|
} from 'tldraw'
|
||||||
|
|
||||||
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
const ANIMAL_EMOJIS = ['🐶', '🐱', '🐨', '🐮', '🐴']
|
||||||
|
|
||||||
|
type IMyEditableShape = TLBaseShape<
|
||||||
|
'my-editable-shape',
|
||||||
|
{
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
animal: number
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
export class EditableShapeUtil extends BaseBoxShapeUtil<IMyEditableShape> {
|
||||||
|
static override type = 'my-editable-shape' as const
|
||||||
|
static override props: ShapeProps<IMyEditableShape> = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
animal: T.number,
|
||||||
|
}
|
||||||
|
|
||||||
|
// [1] !!!
|
||||||
|
override canEdit = () => true
|
||||||
|
|
||||||
|
getDefaultProps(): IMyEditableShape['props'] {
|
||||||
|
return {
|
||||||
|
w: 200,
|
||||||
|
h: 200,
|
||||||
|
animal: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [2]
|
||||||
|
component(shape: IMyEditableShape) {
|
||||||
|
// [a]
|
||||||
|
const isEditing = this.editor.getEditingShapeId() === shape.id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HTMLContainer
|
||||||
|
id={shape.id}
|
||||||
|
// [b]
|
||||||
|
onPointerDown={isEditing ? stopEventPropagation : undefined}
|
||||||
|
style={{
|
||||||
|
pointerEvents: isEditing ? 'all' : 'none',
|
||||||
|
backgroundColor: '#efefef',
|
||||||
|
fontSize: 24,
|
||||||
|
padding: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ANIMAL_EMOJIS[shape.props.animal]}
|
||||||
|
{/* [c] */}
|
||||||
|
{isEditing ? (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
this.editor.updateShape({
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
animal: (shape.props.animal + 1) % ANIMAL_EMOJIS.length,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
// [d] when not editing...
|
||||||
|
<p style={{ fontSize: 12 }}>Double Click to Edit</p>
|
||||||
|
)}
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: IMyEditableShape) {
|
||||||
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// [3]
|
||||||
|
override onEditEnd: TLOnEditEndHandler<IMyEditableShape> = (shape) => {
|
||||||
|
this.editor.animateShape(
|
||||||
|
{ ...shape, rotation: shape.rotation + Math.PI * 2 },
|
||||||
|
{ duration: 250 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
This is our shape util, which defines how our shape renders and behaves. For
|
||||||
|
more information on the shape util, check out the custom shape example.
|
||||||
|
|
||||||
|
[1]
|
||||||
|
We override the canEdit method to allow the shape to enter the editing state.
|
||||||
|
|
||||||
|
[2]
|
||||||
|
We want to conditionally render the component based on whether it is being
|
||||||
|
edited or not.
|
||||||
|
|
||||||
|
[a] We can check whether our shape is being edited by comparing the
|
||||||
|
editing shape id to the shape's id.
|
||||||
|
|
||||||
|
[b] We want to allow pointer events when the shape is being edited,
|
||||||
|
and stop event propagation on pointer down. Check out the interactive
|
||||||
|
shape example for more information on this.
|
||||||
|
|
||||||
|
[c] We render a button to change the animal emoji when the shape is being
|
||||||
|
edited.
|
||||||
|
|
||||||
|
[e] We also render a message when the shape is not being edited.
|
||||||
|
|
||||||
|
[3]
|
||||||
|
The onEditEnd method is called when the shape exits the editing state. In this
|
||||||
|
case we rotate the shape 360 degrees.
|
||||||
|
|
||||||
|
*/
|
|
@ -2,11 +2,18 @@
|
||||||
title: Editable shape
|
title: Editable shape
|
||||||
component: ./EditableShapeExample.tsx
|
component: ./EditableShapeExample.tsx
|
||||||
category: shapes/tools
|
category: shapes/tools
|
||||||
priority: 1
|
priority: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
A custom shape that you can edit by double-clicking it.
|
A custom shape that you can edit by double-clicking it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Learn how you can have a shape that enters an editing state, and have a side effect run when editing has finished.
|
In Tldraw, the Editor can have one editing shape at a time. When in its editing state, the editor will ignore events until the user exits the editing state by pressing Escape or clicking on the canvas.
|
||||||
|
|
||||||
|
Only shapes with a `canEdit` flag that returns true may become editable. A user may begin editing a shape by double clicking on the editable shape, or selecting the editable shape and pressing enter.
|
||||||
|
|
||||||
|
Many of our shapes use editing to allow for interactions inside of the shape. For example, a text shape behaves like a text graphic until the user begins editing it—and only then can the user use their keyboard to edit the text. Note that a shape can be interactive regardless of whether it's the editor's editing shape—the "editing" mechanic is just a way of managing a common pattern in canvas appliations.
|
||||||
|
|
||||||
|
In this example we'll create a shape that renders an emoji and allows the user to change the emoji when the shape is in the editing state.
|
||||||
|
Most of the relevant code for this is in the EditableShapeUtil.tsx file. If you want a more in-depth explanation of the shape util, check out the custom shape example.
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { BaseBoxShapeTool } from 'tldraw'
|
|
||||||
export class MyshapeTool extends BaseBoxShapeTool {
|
|
||||||
static override id = 'Myshape'
|
|
||||||
static override initial = 'idle'
|
|
||||||
override shapeType = 'Myshape'
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This file contains our custom tool. The tool is a StateNode with the `id` "Myshape".
|
|
||||||
|
|
||||||
We get a lot of functionality for free by extending the BaseBoxShapeTool. but we can
|
|
||||||
handle events in our own way by overriding methods like onDoubleClick. For an example
|
|
||||||
of a tool with more custom functionality, check out the screenshot-tool example.
|
|
||||||
|
|
||||||
*/
|
|
|
@ -1,196 +0,0 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
import { useState } from 'react'
|
|
||||||
import {
|
|
||||||
HTMLContainer,
|
|
||||||
Rectangle2d,
|
|
||||||
ShapeProps,
|
|
||||||
ShapeUtil,
|
|
||||||
T,
|
|
||||||
TLBaseShape,
|
|
||||||
TLOnEditEndHandler,
|
|
||||||
TLOnResizeHandler,
|
|
||||||
resizeBox,
|
|
||||||
structuredClone,
|
|
||||||
useIsEditing,
|
|
||||||
} from 'tldraw'
|
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
|
||||||
|
|
||||||
// [1]
|
|
||||||
type IMyshape = TLBaseShape<
|
|
||||||
'Myshape',
|
|
||||||
{
|
|
||||||
w: number
|
|
||||||
h: number
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
export class MyshapeUtil extends ShapeUtil<IMyshape> {
|
|
||||||
// [2]
|
|
||||||
static override type = 'Myshape' as const
|
|
||||||
static override props: ShapeProps<IMyshape> = {
|
|
||||||
w: T.number,
|
|
||||||
h: T.number,
|
|
||||||
}
|
|
||||||
|
|
||||||
// [3]
|
|
||||||
override isAspectRatioLocked = (_shape: IMyshape) => true
|
|
||||||
override canResize = (_shape: IMyshape) => true
|
|
||||||
override canBind = (_shape: IMyshape) => true
|
|
||||||
|
|
||||||
// [4]
|
|
||||||
override canEdit = () => true
|
|
||||||
|
|
||||||
// [5]
|
|
||||||
getDefaultProps(): IMyshape['props'] {
|
|
||||||
return {
|
|
||||||
w: 170,
|
|
||||||
h: 165,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// [6]
|
|
||||||
getGeometry(shape: IMyshape) {
|
|
||||||
return new Rectangle2d({
|
|
||||||
width: shape.props.w,
|
|
||||||
height: shape.props.h,
|
|
||||||
isFilled: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// [7]
|
|
||||||
component(shape: IMyshape) {
|
|
||||||
// [a]
|
|
||||||
const isEditing = useIsEditing(shape.id)
|
|
||||||
|
|
||||||
const [animal, setAnimal] = useState<boolean>(true)
|
|
||||||
|
|
||||||
// [b]
|
|
||||||
return (
|
|
||||||
<HTMLContainer id={shape.id}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
border: '1px solid rgba(0, 0, 0, 0.1)',
|
|
||||||
boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.12), 0px 1px 3px rgba(0, 0, 0, 0.04)',
|
|
||||||
display: 'flex',
|
|
||||||
borderRadius: '50%',
|
|
||||||
height: '100%',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
pointerEvents: 'all',
|
|
||||||
backgroundColor: animal ? 'hsl(180, 34%, 86%)' : 'hsl(10, 34%, 86%)',
|
|
||||||
position: 'relative',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
display: isEditing ? 'block' : 'none',
|
|
||||||
border: 'none',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 50,
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '8px 8px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
backgroundColor: 'hsl(120, 54%, 46%)',
|
|
||||||
color: '#fff',
|
|
||||||
textDecoration: 'none',
|
|
||||||
boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.12), 0px 1px 3px rgba(0, 0, 0, 0.04)',
|
|
||||||
}}
|
|
||||||
disabled={!isEditing}
|
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
|
||||||
onClick={() => setAnimal((prev) => !prev)}
|
|
||||||
>
|
|
||||||
Change
|
|
||||||
</button>
|
|
||||||
<p style={{ fontSize: shape.props.h / 1.5, margin: 0 }}>{animal ? '🐶' : '🐱'}</p>
|
|
||||||
</div>
|
|
||||||
</HTMLContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// [8]
|
|
||||||
indicator(shape: IMyshape) {
|
|
||||||
const isEditing = useIsEditing(shape.id)
|
|
||||||
return <rect stroke={isEditing ? 'red' : 'blue'} width={shape.props.w} height={shape.props.h} />
|
|
||||||
}
|
|
||||||
|
|
||||||
// [9]
|
|
||||||
override onResize: TLOnResizeHandler<IMyshape> = (shape, info) => {
|
|
||||||
return resizeBox(shape, info)
|
|
||||||
}
|
|
||||||
|
|
||||||
// [10]
|
|
||||||
override onEditEnd: TLOnEditEndHandler<IMyshape> = (shape) => {
|
|
||||||
const frame1 = structuredClone(shape)
|
|
||||||
const frame2 = structuredClone(shape)
|
|
||||||
|
|
||||||
frame1.x = shape.x + 1.2
|
|
||||||
frame2.x = shape.x - 1.2
|
|
||||||
|
|
||||||
this.editor.animateShape(frame1, { duration: 50 })
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.editor.animateShape(frame2, { duration: 50 })
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.editor.animateShape(shape, { duration: 100 })
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This is a utility class for the Myshape shape. This is where you define the shape's behavior,
|
|
||||||
how it renders (its component and indicator), and how it handles different events.
|
|
||||||
|
|
||||||
[1]
|
|
||||||
This is where we define the shape's type for Typescript. We can extend the TLBaseShape type,
|
|
||||||
providing a unique string to identify the shape and the shape's props. We only need height
|
|
||||||
and width for this shape.
|
|
||||||
|
|
||||||
[2]
|
|
||||||
We define the shape's type and props for the editor. We can use tldraw's validator library to
|
|
||||||
define the shape's properties. In this case, we define the width and height properties as numbers.
|
|
||||||
|
|
||||||
[3]
|
|
||||||
Some methods we can override to define specific beahviour for the shape. For this shape, we don't
|
|
||||||
want the aspect ratio to change, we want it to resize, and sure it can bind, why not. Who doesn't
|
|
||||||
love arrows?
|
|
||||||
|
|
||||||
[4]
|
|
||||||
This is the important one. We set canEdit to true. This means that the shape can enter the editing
|
|
||||||
state.
|
|
||||||
|
|
||||||
[5]
|
|
||||||
This will be the default props for the shape when you create it via clicking.
|
|
||||||
|
|
||||||
[6]
|
|
||||||
We define the getGeometry method. This method returns the geometry of the shape. In this case,
|
|
||||||
a Rectangle2d object.
|
|
||||||
|
|
||||||
[7]
|
|
||||||
We define the component method. This controls what the shape looks like and it returns JSX.
|
|
||||||
|
|
||||||
[a] We can use the useIsEditing hook to check if the shape is in the editing state. If it is,
|
|
||||||
we want our shape to render differently.
|
|
||||||
|
|
||||||
[b] The HTML container is a really handy wrapper for custom shapes, it essentially creates a
|
|
||||||
div with some helpful css for you. We can use the isEditing variable to conditionally
|
|
||||||
render the shape. We also use the useState hook to toggle between a cat and a dog.
|
|
||||||
|
|
||||||
[8]
|
|
||||||
The indicator method is the blue box that appears around the shape when it's selected. We can
|
|
||||||
make it appear red if the shape is in the editing state by using the useIsEditing hook.
|
|
||||||
|
|
||||||
[9]
|
|
||||||
The onResize method is where we handle the resizing of the shape. We use the resizeBox helper
|
|
||||||
to handle the resizing for us.
|
|
||||||
|
|
||||||
[10]
|
|
||||||
The onEditEnd method is called when the shape exits the editing state. In the tldraw codebase we
|
|
||||||
mostly use this for trimming text fields in shapes. In this case, we use it to animate the shape
|
|
||||||
when it exits the editing state.
|
|
||||||
|
|
||||||
*/
|
|
|
@ -1,60 +0,0 @@
|
||||||
import {
|
|
||||||
DefaultKeyboardShortcutsDialog,
|
|
||||||
DefaultKeyboardShortcutsDialogContent,
|
|
||||||
TLComponents,
|
|
||||||
TLUiOverrides,
|
|
||||||
TldrawUiMenuItem,
|
|
||||||
toolbarItem,
|
|
||||||
useTools,
|
|
||||||
} from 'tldraw'
|
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
|
||||||
|
|
||||||
export const uiOverrides: TLUiOverrides = {
|
|
||||||
tools(editor, tools) {
|
|
||||||
// Create a tool item in the ui's context.
|
|
||||||
tools.Myshape = {
|
|
||||||
id: 'Myshape',
|
|
||||||
icon: 'color',
|
|
||||||
label: 'Myshape',
|
|
||||||
kbd: 'c',
|
|
||||||
onSelect: () => {
|
|
||||||
editor.setCurrentTool('Myshape')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return tools
|
|
||||||
},
|
|
||||||
toolbar(_app, toolbar, { tools }) {
|
|
||||||
// Add the tool item from the context to the toolbar.
|
|
||||||
toolbar.splice(4, 0, toolbarItem(tools.Myshape))
|
|
||||||
return toolbar
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const components: TLComponents = {
|
|
||||||
KeyboardShortcutsDialog: (props) => {
|
|
||||||
const tools = useTools()
|
|
||||||
return (
|
|
||||||
<DefaultKeyboardShortcutsDialog {...props}>
|
|
||||||
<DefaultKeyboardShortcutsDialogContent />
|
|
||||||
{/* Ideally, we'd interleave this into the tools group */}
|
|
||||||
<TldrawUiMenuItem {...tools['Myshape']} />
|
|
||||||
</DefaultKeyboardShortcutsDialog>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
This file contains overrides for the Tldraw UI. These overrides are used to add your custom tools
|
|
||||||
to the toolbar and the keyboard shortcuts menu.
|
|
||||||
|
|
||||||
We do this by providing a custom toolbar override to the Tldraw component. This override is a
|
|
||||||
function that takes the current editor, the default toolbar items, and the default tools.
|
|
||||||
It returns the new toolbar items. We use the toolbarItem helper to create a new toolbar item
|
|
||||||
for our custom tool. We then splice it into the toolbar items array at the 4th index. This puts
|
|
||||||
it after the eraser tool. We'll pass our overrides object into the Tldraw component's `overrides`
|
|
||||||
prop.
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Tldraw } from 'tldraw'
|
||||||
|
import 'tldraw/tldraw.css'
|
||||||
|
|
||||||
|
import { myInteractiveShape } from './my-interactive-shape-util'
|
||||||
|
|
||||||
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
// [1]
|
||||||
|
const customShapeUtils = [myInteractiveShape]
|
||||||
|
|
||||||
|
//[2]
|
||||||
|
export default function InteractiveShapeExample() {
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw
|
||||||
|
shapeUtils={customShapeUtils}
|
||||||
|
onMount={(editor) => {
|
||||||
|
editor.createShape({ type: 'my-interactive-shape', x: 100, y: 100 })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
By default the editor handles pointer events, but sometimes you want to handle
|
||||||
|
interactions on your shape in your own ways, for example via a button. You can do this
|
||||||
|
by using the css property `pointer events: all` and stopping event propagation. In
|
||||||
|
this example we want our todo shape to have a checkbox so the user can mark them as
|
||||||
|
done.
|
||||||
|
|
||||||
|
Check out my-interactive-shape-util.tsx to see how we create the shape.
|
||||||
|
*/
|
14
apps/examples/src/examples/interactive-shape/README.md
Normal file
14
apps/examples/src/examples/interactive-shape/README.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
title: Interactive shape
|
||||||
|
component: ./InteractiveShapeExample.tsx
|
||||||
|
category: shapes/tools
|
||||||
|
priority: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
A custom shape that has its own onClick interactions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
By default the editor handles pointer events, but sometimes you want to handle interactions on your shape in your own ways, for example via a button. You can do this by using the css property `pointer events: all` and stopping event propagation. In this example we want our todo shape to have a checkbox so the user can mark them as done.
|
||||||
|
|
||||||
|
Check out my-interactive-shape-util.tsx to see how we create the shape.
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { BaseBoxShapeUtil, HTMLContainer, ShapeProps, T, TLBaseShape } from 'tldraw'
|
||||||
|
|
||||||
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
type IMyInteractiveShape = TLBaseShape<
|
||||||
|
'my-interactive-shape',
|
||||||
|
{
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
checked: boolean
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
export class myInteractiveShape extends BaseBoxShapeUtil<IMyInteractiveShape> {
|
||||||
|
static override type = 'my-interactive-shape' as const
|
||||||
|
static override props: ShapeProps<IMyInteractiveShape> = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
checked: T.boolean,
|
||||||
|
text: T.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultProps(): IMyInteractiveShape['props'] {
|
||||||
|
return {
|
||||||
|
w: 230,
|
||||||
|
h: 230,
|
||||||
|
checked: false,
|
||||||
|
text: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [1]
|
||||||
|
component(shape: IMyInteractiveShape) {
|
||||||
|
return (
|
||||||
|
<HTMLContainer
|
||||||
|
style={{
|
||||||
|
padding: 16,
|
||||||
|
height: shape.props.h,
|
||||||
|
width: shape.props.w,
|
||||||
|
// [a] This is where we allow pointer events on our shape
|
||||||
|
pointerEvents: 'all',
|
||||||
|
backgroundColor: '#efefef',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={shape.props.checked}
|
||||||
|
onChange={() =>
|
||||||
|
this.editor.updateShape<IMyInteractiveShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: 'my-interactive-shape',
|
||||||
|
props: { checked: !shape.props.checked },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// [b] This is where we stop event propagation
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter a todo..."
|
||||||
|
readOnly={shape.props.checked}
|
||||||
|
value={shape.props.text}
|
||||||
|
onChange={(e) =>
|
||||||
|
this.editor.updateShape<IMyInteractiveShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: 'my-interactive-shape',
|
||||||
|
props: { text: e.currentTarget.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// [c]
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
if (!shape.props.checked) {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [5]
|
||||||
|
indicator(shape: IMyInteractiveShape) {
|
||||||
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is a custom shape, for a more in-depth look at how to create a custom shape,
|
||||||
|
see our custom shape example.
|
||||||
|
|
||||||
|
[1]
|
||||||
|
This is where we describe how our shape will render
|
||||||
|
|
||||||
|
[a] We need to set pointer-events to all so that we can interact with our shape. This CSS property is
|
||||||
|
set to "none" off by default. We need to manually opt-in to accepting pointer events by setting it to
|
||||||
|
'all' or 'auto'.
|
||||||
|
|
||||||
|
[b] We need to stop event propagation so that the editor doesn't select the shape
|
||||||
|
when we click on the checkbox. The 'canvas container' forwards events that it receives
|
||||||
|
on to the editor, so stopping propagation here prevents the event from reaching the canvas.
|
||||||
|
|
||||||
|
[c] If the shape is not checked, we stop event propagation so that the editor doesn't
|
||||||
|
select the shape when we click on the input. If the shape is checked then we allow that event to
|
||||||
|
propagate to the canvas and then get sent to the editor, triggering clicks or drags as usual.
|
||||||
|
|
||||||
|
*/
|
Loading…
Reference in a new issue