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",
|
||||
"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
|
||||
category: shapes/tools
|
||||
priority: 1
|
||||
priority: 3
|
||||
---
|
||||
|
||||
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/tldraw.css'
|
||||
import { MyshapeTool } from './my-shape/my-shape-tool'
|
||||
import { MyshapeUtil } from './my-shape/my-shape-util'
|
||||
import { components, uiOverrides } from './ui-overrides'
|
||||
import { EditableShapeUtil } from './EditableShapeUtil'
|
||||
|
||||
// [1]
|
||||
const customShapeUtils = [MyshapeUtil]
|
||||
const customTools = [MyshapeTool]
|
||||
const customShapeUtils = [EditableShapeUtil]
|
||||
|
||||
//[2]
|
||||
export default function EditableShapeExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
// Pass in the array of custom shape classes
|
||||
shapeUtils={customShapeUtils}
|
||||
// Pass in the array of custom tools
|
||||
tools={customTools}
|
||||
// Pass in any overrides to the user interface
|
||||
overrides={uiOverrides}
|
||||
// pass in the new Keyboard Shortcuts component
|
||||
components={components}
|
||||
// Create a shape when the editor mounts
|
||||
onMount={(editor) => {
|
||||
editor.createShape({ type: 'my-editable-shape', x: 100, y: 100 })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -31,24 +24,12 @@ Introduction:
|
|||
|
||||
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
|
||||
when they are double-clicked, this means that users can drag and resize shapes without
|
||||
accidentally entering the editing state. In our default shapes we mostly use this for
|
||||
editing text, but it's also used in our video shape. In this example we'll create a
|
||||
shape that you could use for a game of Go, but instead of black and white stones, we'll
|
||||
use cats and dogs.
|
||||
when they are double-clicked. In our default shapes we mostly use this for editing text.
|
||||
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 my-shape-util.tsx file. We also define a
|
||||
very simple tool in my-shape-tool.tsx, and make our new tool appear on the toolbar in
|
||||
ui-overrides.ts.
|
||||
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]
|
||||
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
|
||||
component: ./EditableShapeExample.tsx
|
||||
category: shapes/tools
|
||||
priority: 1
|
||||
priority: 2
|
||||
---
|
||||
|
||||
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