diff --git a/apps/examples/src/examples/custom-config/CustomConfigExample.tsx b/apps/examples/src/examples/custom-config/CustomConfigExample.tsx index 4dde422e7..095186dd3 100644 --- a/apps/examples/src/examples/custom-config/CustomConfigExample.tsx +++ b/apps/examples/src/examples/custom-config/CustomConfigExample.tsx @@ -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} /> ) diff --git a/apps/examples/src/examples/editable-shape/EditableShapeExample.tsx b/apps/examples/src/examples/editable-shape/EditableShapeExample.tsx new file mode 100644 index 000000000..ed1fcfeb7 --- /dev/null +++ b/apps/examples/src/examples/editable-shape/EditableShapeExample.tsx @@ -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 ( +
+ +
+ ) +} + +/* +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. + + */ diff --git a/apps/examples/src/examples/editable-shape/README.md b/apps/examples/src/examples/editable-shape/README.md new file mode 100644 index 000000000..b3170b8e6 --- /dev/null +++ b/apps/examples/src/examples/editable-shape/README.md @@ -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. diff --git a/apps/examples/src/examples/editable-shape/my-shape/my-shape-tool.tsx b/apps/examples/src/examples/editable-shape/my-shape/my-shape-tool.tsx new file mode 100644 index 000000000..bb1eac667 --- /dev/null +++ b/apps/examples/src/examples/editable-shape/my-shape/my-shape-tool.tsx @@ -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. + +*/ diff --git a/apps/examples/src/examples/editable-shape/my-shape/my-shape-util.tsx b/apps/examples/src/examples/editable-shape/my-shape/my-shape-util.tsx new file mode 100644 index 000000000..615a24f8f --- /dev/null +++ b/apps/examples/src/examples/editable-shape/my-shape/my-shape-util.tsx @@ -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 { + // [2] + static override type = 'catdog' as const + static override props: ShapeProps = { + 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(true) + + // [b] + return ( + +
+ +

{animal ? '🐶' : '🐱'}

+
+
+ ) + } + + // [8] + indicator(shape: ICatDog) { + const isEditing = useIsEditing(shape.id) + return + } + + // [9] + override onResize: TLOnResizeHandler = (shape, info) => { + return resizeBox(shape, info) + } + + // [10] + override onEditEnd: TLOnEditEndHandler = (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. + +*/ diff --git a/apps/examples/src/examples/editable-shape/ui-overrides.tsx b/apps/examples/src/examples/editable-shape/ui-overrides.tsx new file mode 100644 index 000000000..a37d8e304 --- /dev/null +++ b/apps/examples/src/examples/editable-shape/ui-overrides.tsx @@ -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 ( + + + {/* Ideally, we'd interleave this into the tools group */} + + + ) + }, +} + +/* + +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. + + +*/