Editable shape example (#2853)
This PR adds an example for an editable shape. I wanted to show the onEditEnd method so I just made the shape do a little wiggle. Closes #2592 ### Change Type - [x] `documentation` — Changes to the documentation only[^2] ### Release Notes - Adds an editable shape example
This commit is contained in:
parent
8b390dddb1
commit
d1151a7af5
6 changed files with 340 additions and 1 deletions
|
@ -2,7 +2,7 @@ import { Tldraw } from '@tldraw/tldraw'
|
|||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { CardShapeTool } from './CardShape/CardShapeTool'
|
||||
import { CardShapeUtil } from './CardShape/CardShapeUtil'
|
||||
import { uiOverrides } from './ui-overrides'
|
||||
import { components, uiOverrides } from './ui-overrides'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
|
@ -21,6 +21,8 @@ export default function CustomConfigExample() {
|
|||
tools={customTools}
|
||||
// Pass in any overrides to the user interface
|
||||
overrides={uiOverrides}
|
||||
// Pass in the new Keybaord Shortcuts component
|
||||
components={components}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { CatDogTool } from './my-shape/my-shape-tool'
|
||||
import { CatDogUtil } from './my-shape/my-shape-util'
|
||||
import { components, uiOverrides } from './ui-overrides'
|
||||
|
||||
// [1]
|
||||
const customShapeUtils = [CatDogUtil]
|
||||
const customTools = [CatDogTool]
|
||||
|
||||
//[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}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
[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.
|
||||
|
||||
*/
|
12
apps/examples/src/examples/editable-shape/README.md
Normal file
12
apps/examples/src/examples/editable-shape/README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: Editable Shape
|
||||
component: ./EditableShapeExample.tsx
|
||||
category: shapes/tools
|
||||
priority: 1
|
||||
---
|
||||
|
||||
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.
|
|
@ -0,0 +1,15 @@
|
|||
import { BaseBoxShapeTool } from '@tldraw/tldraw'
|
||||
export class CatDogTool extends BaseBoxShapeTool {
|
||||
static override id = 'catdog'
|
||||
static override initial = 'idle'
|
||||
override shapeType = 'catdog'
|
||||
}
|
||||
|
||||
/*
|
||||
This file contains our custom tool. The tool is a StateNode with the `id` "catdog".
|
||||
|
||||
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.
|
||||
|
||||
*/
|
|
@ -0,0 +1,196 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import {
|
||||
HTMLContainer,
|
||||
Rectangle2d,
|
||||
ShapeProps,
|
||||
ShapeUtil,
|
||||
T,
|
||||
TLBaseShape,
|
||||
TLOnEditEndHandler,
|
||||
TLOnResizeHandler,
|
||||
resizeBox,
|
||||
structuredClone,
|
||||
useIsEditing,
|
||||
} from '@tldraw/tldraw'
|
||||
import { useState } from 'react'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
// [1]
|
||||
type ICatDog = TLBaseShape<
|
||||
'catdog',
|
||||
{
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
>
|
||||
|
||||
export class CatDogUtil extends ShapeUtil<ICatDog> {
|
||||
// [2]
|
||||
static override type = 'catdog' as const
|
||||
static override props: ShapeProps<ICatDog> = {
|
||||
w: T.number,
|
||||
h: T.number,
|
||||
}
|
||||
|
||||
// [3]
|
||||
override isAspectRatioLocked = (_shape: ICatDog) => true
|
||||
override canResize = (_shape: ICatDog) => true
|
||||
override canBind = (_shape: ICatDog) => true
|
||||
|
||||
// [4]
|
||||
override canEdit = () => true
|
||||
|
||||
// [5]
|
||||
getDefaultProps(): ICatDog['props'] {
|
||||
return {
|
||||
w: 170,
|
||||
h: 165,
|
||||
}
|
||||
}
|
||||
|
||||
// [6]
|
||||
getGeometry(shape: ICatDog) {
|
||||
return new Rectangle2d({
|
||||
width: shape.props.w,
|
||||
height: shape.props.h,
|
||||
isFilled: true,
|
||||
})
|
||||
}
|
||||
|
||||
// [7]
|
||||
component(shape: ICatDog) {
|
||||
// [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: ICatDog) {
|
||||
const isEditing = useIsEditing(shape.id)
|
||||
return <rect stroke={isEditing ? 'red' : 'blue'} width={shape.props.w} height={shape.props.h} />
|
||||
}
|
||||
|
||||
// [9]
|
||||
override onResize: TLOnResizeHandler<ICatDog> = (shape, info) => {
|
||||
return resizeBox(shape, info)
|
||||
}
|
||||
|
||||
// [10]
|
||||
override onEditEnd: TLOnEditEndHandler<ICatDog> = (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 catdog 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.
|
||||
|
||||
*/
|
60
apps/examples/src/examples/editable-shape/ui-overrides.tsx
Normal file
60
apps/examples/src/examples/editable-shape/ui-overrides.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import {
|
||||
DefaultKeyboardShortcutsDialog,
|
||||
DefaultKeyboardShortcutsDialogContent,
|
||||
TLComponents,
|
||||
TLUiOverrides,
|
||||
TldrawUiMenuItem,
|
||||
toolbarItem,
|
||||
useTools,
|
||||
} from '@tldraw/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.catdog = {
|
||||
id: 'catdog',
|
||||
icon: 'color',
|
||||
label: 'Catdog',
|
||||
kbd: 'c',
|
||||
onSelect: () => {
|
||||
editor.setCurrentTool('catdog')
|
||||
},
|
||||
}
|
||||
return tools
|
||||
},
|
||||
toolbar(_app, toolbar, { tools }) {
|
||||
// Add the tool item from the context to the toolbar.
|
||||
toolbar.splice(4, 0, toolbarItem(tools.catdog))
|
||||
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['catdog']} />
|
||||
</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.
|
||||
|
||||
|
||||
*/
|
Loading…
Add table
Add a link
Reference in a new issue