Add custom tool examples (#3064)

Two examples:

One in the UI section that shows how to add a tool to the toolbar along
with an icon

One in the shapes and tools section that shows a simple sticker tool
with no child states

I'll go over the copy again before it's merged, but don't want to spend
too long on it right now in case the feeling is that these should both
be a single example.

Next: The [minimal
example](https://tldraw.dev/examples/editor-api/only-editor) is
currently the best example we have of a tool with child states. I think
this should be adapted and copied/moved over to the custom shapes and
tools category.

closes tld-2266

- [x] `documentation` — Changes to the documentation only[^2]

### Release Notes

- Adds a simple custom tool example
This commit is contained in:
Taha 2024-03-05 09:27:53 +00:00 committed by GitHub
parent 0813e54ca2
commit e543797b81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 245 additions and 0 deletions

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 20.4l-1.8-1.6C4.8 13.4 2 10.5 2 7.2 2 4.1 4.1 2 7.2 2c1.9 0 3.7 1 4.8 2.6C13.1 3 15 2 16.8 2 19.9 2 22 4.1 22 7.2c0 3.3-2.8 6.2-8.2 11.6L12 20.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 232 B

View file

@ -0,0 +1,12 @@
---
title: Add a tool to the Toolbar
component: ./ToolInToolbarExample.tsx
category: ui
priority: 1
---
Add your custom tool to the toolbar
---
You can make an icon for your custom tool appear on tldraw's toolbar. To do this you will need to override the toolbar component, pass in a custom component for the keyboard shortcuts dialog, and pass in an asset url for your icon. This example shows how to do that. For more information on how to implement custom tools, check out the custom tool example.

View file

@ -0,0 +1,115 @@
import {
DefaultKeyboardShortcutsDialog,
DefaultKeyboardShortcutsDialogContent,
TLComponents,
TLUiAssetUrlOverrides,
TLUiOverrides,
Tldraw,
TldrawUiMenuItem,
toolbarItem,
useTools,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { StickerTool } from './sticker-tool-util'
// There's a guide at the bottom of this file!
// [1]
const uiOverrides: TLUiOverrides = {
tools(editor, tools) {
// Create a tool item in the ui's context.
tools.sticker = {
id: 'sticker',
icon: 'heart-icon',
label: 'Sticker',
kbd: 's',
onSelect: () => {
editor.setCurrentTool('sticker')
},
}
return tools
},
toolbar(_app, toolbar, { tools }) {
// Add the tool item from the context to the toolbar.
toolbar.splice(4, 0, toolbarItem(tools.sticker))
return toolbar
},
}
// [2]
const components: TLComponents = {
KeyboardShortcutsDialog: (props) => {
const tools = useTools()
return (
<DefaultKeyboardShortcutsDialog {...props}>
<DefaultKeyboardShortcutsDialogContent />
{/* Ideally, we'd interleave this into the tools group */}
<TldrawUiMenuItem {...tools['sticker']} />
</DefaultKeyboardShortcutsDialog>
)
},
}
// [3]
export const customAssetUrls: TLUiAssetUrlOverrides = {
icons: {
'heart-icon': '/heart-icon.svg',
},
}
// [4]
const customTools = [StickerTool]
export default function CustomToolExample() {
return (
<div className="tldraw__editor">
<Tldraw
// Pass in the array of custom tool classes
tools={customTools}
// Set the initial state to the sticker tool
initialState="sticker"
// Pass in our ui overrides
overrides={uiOverrides}
// pass in our custom components
components={components}
// pass in our custom asset urls
assetUrls={customAssetUrls}
/>
</div>
)
}
/*
Introduction:
You can make an icon for your custom tool appear on tldraw's toolbar. To do this
you will need to override the toolbar component, pass in a custom component for
the keyboard shortcuts dialog, and pass in an asset url for your icon. This
example shows how to do that. For more information on how to implement custom
tools, check out the custom tool example.
[1]
First, we define the uiOverrides object. We can override the tools function to
add our custom tool to the ui's context. We can also override the toolbar function
to add our custom tool to the toolbar. We are going to splice it into the toolbar
so it appears in between the eraser and arrow tools.
[2]
Next, we want to override the default keyboard shortcuts dialog so that the
shortcut for our custom tool appears in the dialog. We don't want to change its
appearance very much, so we can use the DefaultKeyboardShortcutsDialog component
and pass in the DefaultKeyboardShortcutsDialogContent component. With the useTools
hook, we can get the tools from context and pass in the sticker tool to the keyboard
shortcuts dialog. This will make the keyboard shortcut for the sticker tool appear
in the dialog.
[3]
We need to make sure the editor knows where to find the icon for our custom tool.
We do this by defining the customAssetUrls object and passing in the asset url for
our icon.
[4]
Finally, we define the customTools array. This array contains the custom tool
class. We then pass the customTools array, the uiOverrides object, the
components object, and the customAssetUrls object to the Tldraw component as
props. This will make the icon for the custom tool appear on the toolbar.
*/

View file

@ -0,0 +1,22 @@
import { StateNode } from 'tldraw'
// Check out the custom tool example for a more detailed explanation of the tool class.
const OFFSET = 12
export class StickerTool extends StateNode {
static override id = 'sticker'
override onEnter = () => {
this.editor.setCursor({ type: 'cross', rotation: 0 })
}
override onPointerDown = () => {
const { currentPagePoint } = this.editor.inputs
this.editor.createShape({
type: 'text',
x: currentPagePoint.x - OFFSET,
y: currentPagePoint.y - OFFSET,
props: { text: '❤️' },
})
}
}

View file

@ -0,0 +1,81 @@
import { StateNode, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!
const OFFSET = 12
// [1]
class StickerTool extends StateNode {
static override id = 'sticker'
// [a]
override onEnter = () => {
this.editor.setCursor({ type: 'cross', rotation: 0 })
}
// [b]
override onPointerDown = () => {
const { currentPagePoint } = this.editor.inputs
this.editor.createShape({
type: 'text',
x: currentPagePoint.x - OFFSET,
y: currentPagePoint.y - OFFSET,
props: { text: '❤️' },
})
}
}
// [2]
const customTools = [StickerTool]
export default function CustomToolExample() {
return (
<div className="tldraw__editor">
<Tldraw
// Pass in the array of custom tool classes
tools={customTools}
// Set the initial state to the sticker tool
initialState="sticker"
// hide the ui
hideUi
// Put some helpful text on the canvas
onMount={(editor) => {
editor.createShape({
type: 'text',
x: 100,
y: 100,
props: { text: 'Click anywhere to add a sticker' },
})
}}
/>
</div>
)
}
/*
Introduction:
Tools are nodes in tldraw's state machine. They are responsible for handling user input.
You can create custom tools by extending the `StateNode` class and overriding its methods.
In this example we make a very simple sticker tool that adds a heart emoji to the canvas
when you click.
[1]
We extend the `StateNode` class to create a new tool called `StickerTool`. We set its id
to "sticker". We are not implementing any child states in this example, so we don't need
to set an initial state or define any children states. To see an example of a custom tool
with child states, check out the screenshot tool or minimal examples.
[a] The onEnter method is called when the tool is activated. We use it to set the cursor
to a crosshair.
[b] The onPointerDown method is called when the user clicks on the canvas. We use it to
create a new shape at the click location. We can get the click location from the
editor's inputs.
[2]
We pass our custom tool to the Tldraw component using the `tools` prop. We also set the
initial state to our custom tool. We hide the ui and add some helpful text to the canvas
using the `onMount` prop. This is not necessary for the tool to work but it helps make the
example more visually clear.
*/

View file

@ -0,0 +1,12 @@
---
title: Custom tool
component: ./CustomToolExample.tsx
category: shapes/tools
priority: 1
---
Create a custom tool
---
Tools are nodes in tldraw's state machine. They are responsible for handling user input. You can create custom tools by extending the `StateNode` class and overriding its methods. In this example we make a very simple sticker tool that adds a heart emoji to the canvas when you click.