Editor
The Editor
class is the main way of controlling tldraw's editor. You can use it to manage the editor's internal state, make changes to the document, or respond to changes that have occurred.
By design, the Editor
's surface area is very large. Almost everything is available through it. Need to create some shapes? Use Editor.createShapes
. Need to delete them? Use Editor.deleteShapes
. Need a sorted array of every shape on the current page? Use Editor.getCurrentPageShapesSorted
.
This page gives a broad idea of how the Editor
class is organized and some of the architectural concepts involved. The full reference is available in the Editor
API.
Store
The editor holds the raw state of the document in its Editor.store
property. Data is kept here as a table of JSON serializable records.
For example, the store contains a TLPage
record for each page in the current document, as well as an TLInstancePageState
record for each page that stores information about the editor's state for that page, and a single TLInstance
for each editor instance which stores the id of the user's current page.
The editor also exposes many computed values which are derived from other records in the store. For example, Editor.getSelectedShapeIds
is a method that returns the editor's current selected shape ids for its current page.
You can use these properties directly or you can use them in signals.
import { track, useEditor } from 'tldraw'
export const SelectedShapeIdsCount = track(() => {
const editor = useEditor()
return <div>{editor.getSelectedShapeIds().length}</div>
})
Changing the state
The Editor
class has many methods for updating its state. For example, you can change the current page's selection using Editor.setSelectedShapes
. You can also use other convenience methods, such as Editor.select
, Editor.selectAll
, or Editor.selectNone
.
editor.selectNone()
editor.select(myShapeId, myOtherShapeId)
editor.getSelectedShapeIds() // [myShapeId, myOtherShapeId]
Each change to the state happens within a transaction. You can batch changes into a single transaction using the Editor.batch
method. It's a good idea to batch wherever possible, as this reduces the overhead for persisting or distributing those changes.
Listening for changes, and merging changes from other sources
For information about how to synchronize the store with other processes, i.e. how to get data out and put data in, see the Persistence page.
Undo and redo
The history stack in tldraw contains two types of data: "marks" and "commands". Commands have their own undo
and redo
methods that describe how the state should change when the command is undone or redone.
You can call Editor.mark
to add a mark to the history stack with the given id
.
editor.mark('my-id')
// do some stuff
editor.bailToMark('my-id')
When you call Editor.undo
, the editor will undo each command until it finds either a mark or the start of the stack. When you call Editor.redo
, the editor will redo each command until it finds either a mark or the end of the stack.
// A
editor.mark('duplicate everything')
editor.selectAll()
editor.duplicateShapes(editor.getSelectedShapeIds())
// B
editor.undo() // will return to A
editor.redo() // will return to B
You can call Editor.bail
to undo and delete all commands in the stack until the first mark.
// A
editor.mark('duplicate everything')
editor.selectAll()
editor.duplicateShapes(editor.getSelectedShapeIds())
// B
editor.bail() // will return to A
editor.redo() // will do nothing
You can use Editor.bailToMark
to undo and delete all commands and marks until you reach a mark with the given id
.
// A
editor.mark('first')
editor.selectAll()
// B
editor.mark('second')
editor.duplicateShapes(editor.getSelectedShapeIds())
// C
editor.bailToMark('first') // will return to A
Events
The Editor
class receives events from its Editor.dispatch
method. When the Editor
receives an event, it is first handled internally to update Editor.inputs
and other state before, and then sent into to the editor's state chart.
You shouldn't need to use the Editor.dispatch
method directly, however you may write code in the state chart that responds to these events. See the Tools page to learn how to do that, or read below for a more detailed information about the state chart itself.
State Chart
The Editor
class has a "state chart", or a tree of StateNode
instances, that contain the logic for the editor's tools such as the select tool or the draw tool. User interactions such as moving the cursor will produce different changes to the state depending on which nodes are active.
Each node can be active or inactive. Each state node may also have zero or more children. When a state is active, and if the state has children, one (and only one) of its children must also be active. When a state node receives an event from its parent, it has the opportunity to handle the event before passing the event to its active child. The node can handle an event in any way: it can ignore the event, update records in the store, or run a transition that changes which states nodes are active.
When a user interaction is sent to the editor via its Editor.dispatch
method, this event is sent to the editor's root state node (Editor.root
) and passed then down through the chart's active states until either it reaches a leaf node or until one of those nodes produces a transaction.
Path
You can get the editor's current "path" of active states via editor.root.path
. In the above example, the value would be "root.select.idle"
.
You can check whether a path is active via Editor.isIn
, or else check whether multiple paths are active via Editor.isInAny
.
editor.store.path // 'root.select.idle'
editor.isIn('root.select') // true
editor.isIn('root.select.idle') // true
editor.isIn('root.select.pointing_shape') // false
editor.isInAny('editor.select.idle', 'editor.select.pointing_shape') // true
Note that the paths you pass to Editor.isIn
or Editor.isInAny
can be the full path or a partial of the start of the path. For example, if the full path is root.select.idle
, then Editor.isIn
would return true for the paths root
, root.select
, or root.select.idle
.
If all you're interested in is the state below
root
, there is a convenience method,Editor.getCurrentToolId
, that can help with the editor's currently selected tool.
import { track, useEditor } from 'tldraw'
export const BubbleToolUi = track(() => {
const editor = useEditor()
// Only show the UI if the bubble tool is active
if (!editor.getCurrentToolId() === 'bubble') return null
return <div>Creating bubble</div>
})
Side effects
The Editor.sideEffects
object lets you register callbacks for key parts of the lifecycle of records in the Store.
You can register callbacks for before or after a record is created, changed, or deleted.
These callbacks are useful for applying constraints, maintaining relationships, or checking the integrity of different records in the document.
For example, we use side effects to create a new TLCamera
record every time a new page is made.
The "before" callbacks allow you to modify the record itself, but shouldn't be used for modifying other records. You can create a different record in the place of what was asked, prevent a change (or make a different one) to an existing record, or stop something from being deleted.
The "after" callbacks let you make changes to other records in response to something happening. You could create, update, or delete any related record, but you should avoid changing the same record that triggered the change.
For example, if you wanted to know every time a new arrow is created, you could register a handler like this:
editor.sideEffects.registerAfterCreateHandler('shape', (newShape) => {
if (newShape.type === 'arrow') {
console.log('A new arrow shape was created', newShape)
}
})
Side effect handlers are also given a source
argument - either "user"
or "remote"
.
This indicates whether the change originated from the current user, or from another remote user in the same multiplayer room.
You could use this to e.g. prevent the current user from deleting shapes, but allow deletions from others in the same room.
Inputs
The Editor.inputs
object holds information about the user's current input state, including their cursor position (in page space and screen space), which keys are pressed, what their multi-click state is, and whether they are dragging, pointing, pinching, and so on.
Note that the modifier keys include a short delay after being released in order to prevent certain errors when modeling interactions. For example, when a user releases the "Shift" key, editor.inputs.shiftKey
will remain true
for another 100 milliseconds or so.
This property is stored as regular data. It is not reactive.
Editor instance state
The Editor.getInstanceState
method returns settings that relate to each individual instance of the editor. In the case that the user has the same editor open in multiple tabs, or if there are multiple editors on the same page, then each editor will have its own instance state. See the TLInstance
docs to learn more about the record itself.
User preferences
The editor's user preferences are shared between all instances. See the TLUserPreferences
docs for more about the user preferences.
Camera and coordinates
The editor offers many methods and properties relating to the part of the infinite canvas that is displayed in the component. This section includes key concepts and methods that you can use to change or control which parts of the canvas are visible.
Viewport
The viewport is the rectangular area contained by the editor.
| Method | Description |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Editor.getViewportScreenBounds
| A Box
that describes the size and position of the component's canvas in actual screen pixels. |
| Editor.getViewportPageBounds
| A Box
that describes the size and position of the part of the current page that is displayed in the viewport. |
Screen vs. page coordinates
In tldraw, coordinates can either be in page or screen space.
A "screen point" refers to the point's distance from the top left corner of the component.
A "page point" refers to the point's distance from the "zero point" of the canvas.
When the camera is at {x: 0, y: 0, z: 0}
, the screen point and page point will be identical. As the camera moves, however, the viewport will display a different part of the page; and so a screen point will correspond to a different page point.
| Method | Description |
| ------------------------ | ---------------------------------------------- |
| Editor.screenToPage
| Convert a point in screen space to page space. |
| Editor.pageToScreen
| Convert a point in page space to screen space. |
You can get the user's pointer position in both screen and page space.
const {
// The user's most recent page / screen points
currentPagePoint,
currentScreenPoint,
// The user's previous page / screen points
previousPagePoint,
previousScreenPoint,
// The last place where the most recent pointer down occurred
originPagePoint,
originScreenPoint,
} = editor.inputs
Camera options
You can use the editor's camera options to configure the behavior of the editor's camera. There are many options available.
wheelBehavior
When set to 'pan'
, scrolling the mousewheel will pan the camera. When set to 'zoom'
, scrolling the mousewheel will zoom the camera. When set to none
, it will have no effect.
panSpeed
The speed at which the camera pans. A pan can occur when the user holds the spacebar and drags, holds the middle mouse button and drags, drags while using the hand tool, or scrolls the mousewheel. The default value is 1
. A value of 0.5
would be twice as slow as default. A value of 2
would be twice as fast. When set to 0
, the camera will not pan.
zoomSpeed
The speed at which the camera zooms. A zoom can occur when the user pinches or scrolls the mouse wheel. The default value is 1
. A value of 0.5
would be twice as slow as default. A value of 2
would be twice as fast. When set to 0
, the camera will not zoom.
zoomSteps
The camera's "zoom steps" are an array of discrete zoom levels that the camera will move between when using the "zoom in" or "zoom out" controls.
The first number in the zoomSteps
array defines the camera's minimum zoom level. The last number in the zoomSteps
array defines the camera's maximum zoom level.
If the constraints
are provided, then the actual value for the camera's zoom will be be calculated by multiplying the value from the zoomSteps
array with the value from the baseZoom
. See the baseZoom
property for more information.
isLocked
Whether the camera is locked. When the camera is locked, the camera will not move.
constraints
By default the camera is free to move anywhere on the infinite canvas. However, you may provide the camera with a constraints
object that constrains the camera based on a relationship between bounds
(in page space) and the viewport
(in screen space).
constraints.bounds
A box model describing the bounds in page space.
constraints.padding
An object with padding to apply to the x
and y
dimensions of the viewport. The padding is in screen space.
constraints.origin
An object with an origin for the x
and y
dimensions. Depending on the behavior
, the origin may be used to position the bounds within the viewport.
For example, when the behavior
is fixed
and the origin.x
is 0
, the bounds will be placed with its left side touching the left side of the viewport. When origin.x
is 1
the bounds will be placed with its right side touching the right side of the viewport. By default the origin for each dimension is .5. This places the bounds in the center of the viewport.
constraints.initialZoom
The initialZoom
option defines the camera's initial zoom level and what the zoom should be when when the camera is reset. The zoom it produces is based on the value provided:
| Value | Description |
| ------------- | ---------------------------------------------------------------------------------------------- |
| default
| Sets the initial zoom to 100%. |
| fit-x
| The x axis will completely fill the viewport bounds. |
| fit-y
| The y axis will completely fill the viewport bounds. |
| fit-min
| The smaller axis will completely fill the viewport bounds. |
| fit-max
| The larger axis will completely fill the viewport bounds. |
| fit-x-100
| The x axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. |
| fit-y-100
| The y axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. |
| fit-min-100
| The smaller axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. |
| fit-max-100
| The larger axis will completely fill the viewport bounds, or 100% zoom, whichever is smaller. |
constraints.baseZoom
The baseZoom
property defines the base property for the camera's zoom steps. It accepts the same values as initialZoom
.
When constraints
are provided, then the actual value for the camera's zoom will be be calculated by multiplying the value from the zoomSteps
array with the value from the baseZoom
.
For example, if the baseZoom
is set to default
, then a zoom step of 2 will be 200%. However, if the baseZoom
is set to fit-x
, then a zoom step value of 2 will be twice the zoom level at which the bounds width exactly fits within the viewport.
constraints.behavior
The behavior
property defines which logic should be used when calculating the bounds position.
| Value | Description |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| 'free' | The bounds may be placed anywhere relative to the viewport. This is the default "infinite canvas" experience. |
| 'inside' | The bounds must stay entirely within the viewport. |
| 'outside' | The bounds may partially leave the viewport but must never leave it completely. |
| 'fixed' | The bounds are placed in the viewport at a fixed location according to the 'origin'
. |
| 'contain' | When the zoom is below the "fit zoom" for an axis, the bounds use the 'fixed'
behavior; when above, the bounds use the inside
behavior. |
Controlling the camera
There are several Editor
methods available for controlling the camera.
| Method | Description |
| ------------------------------- | --------------------------------------------------------------------------------------------------- |
| Editor.setCamera
| Moves the camera to the provided coordinates. |
| Editor.zoomIn
| Zooms the camera in to the nearest zoom step. See the constraints.zoomSteps
for more information. |
| Editor.zoomOut
| Zooms the camera in to the nearest zoom step. See the constraints.zoomSteps
for more information. |
| Editor.zoomToFit
| Zooms the camera in to the nearest zoom step. See the constraints.zoomSteps
for more information. |
| Editor.zoomToBounds
| Moves the camera to fit the given bounding box. |
| Editor.zoomToSelection
| Moves the camera to fit the current selection. |
| Editor.zoomToUser
| Moves the camera to center on a user's cursor. |
| Editor.resetZoom
| Resets the zoom to 100% or to the initialZoom
zoom level. |
| Editor.centerOnPoint
| Centers the camera on the given point. |
| Editor.stopCameraAnimation
| Stops any camera animation. |
Camera state
The camera may be in two states, idle
or moving
.
You can get the current camera state with Editor.getCameraState
.
Common things to do with the editor
Create a shape id
To create an id for a shape (a TLShapeId
), use the libary's createShapeId
helper.
import { createShapeId } from 'tldraw'
createShapeId() // `shape:some-random-uuid`
createShapeId('kyle') // `shape:kyle`
The id
property of any record in tldraw is "branded" with the type of that record. For shapes, that means that all shape ids are formatted as shape:{id}
. The TypeScript type of a record's id
also includes a reference to the type of the record that it belongs to. TypeScript will complain if you use a regular shape:some-id
string, but the createShapeId
helper will provide the type.
Create shapes
To create shapes, use the Editor.createShape
or Editor.createShapes
methods.
editor.createShapes([
{
id,
type: 'geo',
x: 0,
y: 0,
props: {
geo: 'rectangle',
w: 100,
h: 100,
dash: 'draw',
color: 'blue',
size: 'm',
},
},
])
A shape must be a partial of the full shape (a TLShapePartial
). All props are optional except for the type
of the shape. The shape's corresponding ShapeUtil
will provide the default props for any props not provided. The id
will be created if not provided.
Update shapes
To update shapes, use the Editor.updateShape
or Editor.updateShapes
methods.
editor.updateShapes([
{
id: shape.id, // required
type: shape.type, // required
x: 100,
y: 100,
props: {
w: 200,
},
},
])
The update must be a partial of the full shape (a TLShapePartial
). All props are optional except for the type
of the shape and its id
.
Delete shapes
To delete shapes, use the Editor.deleteShape
or Editor.deleteShapes
methods.
editor.deleteShapes([shape.id])
editor.deleteShapes([shape])
You can delete a shape using the shape's id
or the shape record itself.
Get a shape
You can get a shape with the Editor.getShape
method.
editor.getShape(myShapeId)
editor.getShape(myShape)
You can get a shape using the shape's id
or the shape record itself.
Turn on read only mode
You can use the Editor.updateInstanceState
method to turn on read only mode.
editor.updateInstanceState({ isReadonly: true })
Move the camera
You can set the camera to a specific x, y, and zoom with the Editor.setCamera
method.
editor.setCamera(0, 0, 1)
Freeze the camera
You can prevent the user from changing the camera using the Editor.setCameraOptions
method.
editor.setCameraOptions({ isLocked: true })
Turn on dark mode
You can turn on or off dark mode via the setUserPreferences
method. Note that this effects all editor instances that share the same user—even instances in other tabs.
setUserPreferences({ isDarkMode: true })
See the tldraw repository for an example of how to use tldraw's Editor API to control the editor.