[improvement] examples (#264)

* Example project, fix bugs in readonly mode

* Adds ui options
This commit is contained in:
Steve Ruiz 2021-11-11 11:37:57 +00:00 committed by GitHub
parent 2441ca0c90
commit fa38c0ef0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1566 additions and 731 deletions

180
README.md
View file

@ -98,29 +98,51 @@ Internally, the `TLDraw` component's user interface uses this API to make change
The `TLDraw` React component is the [tldraw](https://tldraw.com) editor exported as a standalone component. You can control the editor through props, or through the `TLDrawState`'s imperative API. **All props are optional.** The `TLDraw` React component is the [tldraw](https://tldraw.com) editor exported as a standalone component. You can control the editor through props, or through the `TLDrawState`'s imperative API. **All props are optional.**
| Prop | Type | Description | | Prop | Type | Description |
| --------------- | ---------------- | -------------------------------------------------------------------------------------------- | | ----------------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
| `id` | `string` | An id under which to persist the component's state. | | `id` | `string` | An id under which to persist the component's state. |
| `document` | `TLDrawDocument` | An initial [`TLDrawDocument`](#tldrawdocument) object. | | `document` | `TLDrawDocument` | An initial [`TLDrawDocument`](#tldrawdocument) object. |
| `currentPageId` | `string` | A current page id, referencing the `TLDrawDocument` object provided via the `document` prop. | | `currentPageId` | `string` | A current page id, referencing the `TLDrawDocument` object provided via the `document` prop. |
| `onMount` | `Function` | Called when the editor first mounts, receiving the current `TLDrawState`. | | `autofocus` | `boolean` | Whether the editor should immediately receive focus. Defaults to true. |
| `onPatch` | `Function` | Called when the state is updated via a patch. | | `showMenu` | `boolean` | Whether to show the menu. |
| `onCommand` | `Function` | Called when the state is updated via a command. | | `showPages` | `boolean` | Whether to show the pages menu. |
| `onPersist` | `Function` | Called when the state is persisted after an action. | | `showStyles` | `boolean` | Whether to show the styles menu. |
| `onChange` | `Function` | Called when the `TLDrawState` updates for any reason. | | `showTools` | `boolean` | Whether to show the tools. |
| `onUndo` | `Function` | Called when the `TLDrawState` updates after an undo. | | `showUI` | `boolean` | Whether to show any UI other than the canvas. |
| `onRedo` | `Function` | Called when the `TLDrawState` updates after a redo. | | `onMount` | `Function` | Called when the editor first mounts, receiving the current `TLDrawState`. |
| `onUserChange` | `Function` | Called when the user's "presence" information changes. | | `onPatch` | `Function` | Called when the state is updated via a patch. |
| `autofocus` | `boolean` | Whether the editor should immediately receive focus. Defaults to true. | | `onCommand` | `Function` | Called when the state is updated via a command. |
| `showMenu` | `boolean` | Whether to show the menu. | | `onPersist` | `Function` | Called when the state is persisted after an action. |
| `showPages` | `boolean` | Whether to show the pages menu. | | `onChange` | `Function` | Called when the `TLDrawState` updates for any reason. |
| `showStyles` | `boolean` | Whether to show the styles menu. | | `onUserChange` | `Function` | Called when the user's "presence" information changes. |
| `showTools` | `boolean` | Whether to show the tools. | | `onUndo` | `Function` | Called when the `TLDrawState` updates after an undo. |
| `showUI` | `boolean` | Whether to show any UI other than the canvas. | | `onRedo` | `Function` | Called when the `TLDrawState` updates after a redo. |
| `onSignIn` | `Function` | Called when the user selects Sign In from the menu. |
| `onSignOut` | `Function` | Called when the user selects Sign Out from the menu. |
| `onNewProject` | `Function` | Called when the user when the user creates a new project through the menu or through a keyboard shortcut. |
| `onSaveProject` | `Function` | Called when the user saves a project through the menu or through a keyboard shortcut. |
| `onSaveProjectAs` | `Function` | Called when the user saves a project as a new project through the menu or through a keyboard shortcut. |
| `onOpenProject` | `Function` | Called when the user opens new project through the menu or through a keyboard shortcut. |
> **Note**: For help with the file-related callbacks, see `useFileSystem`.
### `useFileSystem`
You can use the `useFileSystem` hook to get prepared callbacks for `onNewProject`, `onOpenProject`, `onSaveProject`, and `onSaveProjectAs`. These callbacks allow a user to save files via the [FileSystem](https://developer.mozilla.org/en-US/docs/Web/API/FileSystem) API.
```ts
import { TLDraw, useFileSystem } from '@tldraw/tldraw'
function App() {
const fileSystemEvents = useFileSystem()
return <TLDraw {...fileSystemEvents} />
}
```
### `TLDrawDocument` ### `TLDrawDocument`
A `TLDrawDocument` is an object with three properties: You can initialize or control the `<TLDraw>` component via its `document` property. A `TLDrawDocument` is an object with three properties:
- `id` - A unique ID for this document - `id` - A unique ID for this document
- `pages` - A table of `TLDrawPage` objects - `pages` - A table of `TLDrawPage` objects
@ -130,7 +152,7 @@ A `TLDrawDocument` is an object with three properties:
```ts ```ts
import { TLDrawDocument, TLDrawState } from '@tldraw/tldraw' import { TLDrawDocument, TLDrawState } from '@tldraw/tldraw'
const tldocument: TLDrawDocument = { const myDocument: TLDrawDocument = {
id: 'doc', id: 'doc',
version: TLDrawState.version, version: TLDrawState.version,
pages: { pages: {
@ -152,9 +174,13 @@ const tldocument: TLDrawDocument = {
}, },
}, },
} }
function App() {
return <TLDraw document={myDocument} />
}
``` ```
**Tip:** TLDraw is built on [@tldraw/core](https://github.com/tldraw/core). The pages and pageStates in TLDraw are just objects containing `TLPage` and `TLPageState` objects from the core library. For more about these types, check out the [@tldraw/core](https://github.com/tldraw/core) documentation. **Tip:** TLDraw is built on [@tldraw/core](https://github.com/tldraw/core). The pages and pageStates in TLDraw are objects containing `TLPage` and `TLPageState` objects from the core library. For more about these types, check out the [@tldraw/core](https://github.com/tldraw/core) documentation.
**Important:** In the `pages` object, each `TLPage` object must be keyed under its `id` property. Likewise, each `TLPageState` object must be keyed under its `id`. In addition, each `TLPageState` object must have an `id` that matches its corresponding page. **Important:** In the `pages` object, each `TLPage` object must be keyed under its `id` property. Likewise, each `TLPageState` object must be keyed under its `id`. In addition, each `TLPageState` object must have an `id` that matches its corresponding page.
@ -179,7 +205,7 @@ Your `TLPage` objects may include shapes: objects that fit one of the `TLDrawSha
| `isGenerated` | `boolean` | (optional) True if the shape is generated. | | `isGenerated` | `boolean` | (optional) True if the shape is generated. |
| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. | | `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. |
> **Important:** In order for re-ordering to work correctly, a shape's `childIndex` values _must_ start from 1, not 0. The page or parent shape's "bottom-most" child should have a `childIndex` of 1. > **Important:** In order for re-ordering to work, a shape's `childIndex` values _must_ start from 1, not 0. The page or parent shape's "bottom-most" child should have a `childIndex` of 1.
The `ShapeStyle` object is a common style API for all shapes. The `ShapeStyle` object is a common style API for all shapes.
@ -268,71 +294,81 @@ function App() {
} }
``` ```
The `TLDrawState` API is too large to document here. To view documentation for the API, build the documentation by running `yarn docs` from the root folder and open the file at `/packages/tldraw/docs/classes/TLDrawState.html` in your browser. To view the full documentation of the `TLDrawState` API, generate the project's documentation by running `yarn docs` from the root folder, then open the file at:
```
/packages/tldraw/docs/classes/TLDrawState.html
```
Here are some useful methods: Here are some useful methods:
| Method | Description | - `loadDocument`
| ----------------- | ----------- | - `select`
| `loadDocument` | | - `selectAll`
| `select` | | - `selectNone`
| `selectAll` | | - `delete`
| `selectNone` | | - `deleteAll`
| `delete` | | - `deletePage`
| `deleteAll` | | - `changePage`
| `deletePage` | | - `cut`
| `changePage` | | - `copy`
| `cut` | | - `paste`
| `copy` | | - `copyJson`
| `paste` | | - `copySvg`
| `copyJson` | | - `undo`
| `copySvg` | | - `redo`
| `undo` | | - `zoomIn`
| `redo` | | - `zoomOut`
| `zoomIn` | | - `zoomToContent`
| `zoomOut` | | - `zoomToSelection`
| `zoomToContent` | | - `zoomToFit`
| `zoomToSelection` | | - `zoomTo`
| `zoomToFit` | | - `resetZoom`
| `zoomTo` | | - `setCamera`
| `resetZoom` | | - `resetCamera`
| `setCamera` | | - `align`
| `resetCamera` | | - `distribute`
| `align` | | - `stretch`
| `distribute` | | - `nudge`
| `stretch` | | - `duplicate`
| `nudge` | | - `flipHorizontal`
| `duplicate` | | - `flipVertical`
| `flipHorizontal` | | - `rotate`
| `flipVertical` | | - `style`
| `rotate` | | - `group`
| `style` | | - `ungroup`
| `group` | | - `createShapes`
| `ungroup` | | - `updateShapes`
| `createShapes` | | - `updateDocument`
| `updateShapes` | | - `updateUsers`
| `updateDocument` | | - `removeUser`
| `updateUsers` | | - `setSetting`
| `removeUser` | | - `selectTool`
| `setSetting` | | - `cancel`
| `selectTool` | |
| `cancel` | | Check the generated docs, source or the TypeScript types for more on these and other methods.
## Local Development ## Local Development
From the root folder:
- Run `yarn` to install dependencies. - Run `yarn` to install dependencies.
- Run `yarn start` to start the development server for the package and for the example. - Run `yarn start` to start the development server for the package and for the example.
- Open `localhost:5420` to view the example project. - Open `localhost:5420` to view the example project.
**Note:** The multiplayer examples and endpoints currently require an API key from [Liveblocks](https://liveblocks.io/), however the storage services that are used in TLDraw are currently in alpha and (as of November 2021) not accessible to the general public. You won't be able to authenticate and run these parts of the project.
Other scripts:
- Run `yarn test` to execute unit tests via [Jest](https://jestjs.io). - Run `yarn test` to execute unit tests via [Jest](https://jestjs.io).
- Run `yarn docs` to build the docs via [ts-doc](https://typedoc.org/). - Run `yarn docs` to build the docs via [ts-doc](https://typedoc.org/).
## Example ## Example
See the `example` folder. See the `example` folder for examples of how to use the `<TLDraw/>` component.
## Community ## Community
@ -346,7 +382,9 @@ Want to connect with other devs? Visit the [Discord channel](https://discord.gg/
### License ### License
This project is licensed under MIT. If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok). This project is licensed under MIT.
If you're using the library in a commercial product, please consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
## Author ## Author

View file

@ -1,19 +1,13 @@
/* eslint-disable */ /* eslint-disable */
import fs from 'fs' import fs from 'fs'
import path from 'path'
import esbuild from 'esbuild' import esbuild from 'esbuild'
import dotenv from 'dotenv'
import { createRequire } from 'module' import { createRequire } from 'module'
const pkg = createRequire(import.meta.url)('../package.json') const pkg = createRequire(import.meta.url)('../package.json')
async function main() { async function main() {
if (fs.existsSync('./dist')) {
fs.rmSync('./dist', { recursive: true }, (e) => {
if (e) {
throw e
}
})
}
try { try {
esbuild.buildSync({ esbuild.buildSync({
entryPoints: ['./src/index.tsx'], entryPoints: ['./src/index.tsx'],
@ -27,15 +21,17 @@ async function main() {
tsconfig: './tsconfig.json', tsconfig: './tsconfig.json',
define: { define: {
'process.env.NODE_ENV': '"production"', 'process.env.NODE_ENV': '"production"',
'process.env.LIVEBLOCKS_PUBLIC_API_KEY': `"${process.env.LIVEBLOCKS_PUBLIC_API_KEY}"`,
}, },
metafile: false, metafile: false,
sourcemap: false, sourcemap: false,
}) })
fs.copyFile('./src/index.html', './dist/index.html', (err) => { fs.readdirSync('./src/public').forEach((file) =>
if (err) throw err fs.copyFile(path.join('./src/public', file), path.join('./dist', file), (err) => {
}) if (err) throw err
})
)
console.log(`${pkg.name}: Build completed.`) console.log(`${pkg.name}: Build completed.`)
} catch (e) { } catch (e) {
console.log(`× ${pkg.name}: Build failed due to an error.`) console.log(`× ${pkg.name}: Build failed due to an error.`)

View file

@ -1,18 +1,27 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
import fs from 'fs' import fs from 'fs'
import path from 'path'
import esbuildServe from 'esbuild-serve' import esbuildServe from 'esbuild-serve'
import dotenv from 'dotenv' import dotenv from 'dotenv'
dotenv.config() dotenv.config()
async function main() { async function main() {
if (!fs.existsSync('./dist')) { if (fs.existsSync('./dist')) {
fs.mkdirSync('./dist') fs.rmSync('./dist', { recursive: true }, (e) => {
if (e) {
throw e
}
})
} }
fs.copyFile('./src/index.html', './dist/index.html', (err) => { fs.mkdirSync('./dist')
if (err) throw err
}) fs.readdirSync('./src/public').forEach((file) =>
fs.copyFile(path.join('./src/public', file), path.join('./dist', file), (err) => {
if (err) throw err
})
)
try { try {
await esbuildServe( await esbuildServe(

30
example/src/api.tsx Normal file
View file

@ -0,0 +1,30 @@
import * as React from 'react'
import { TLDraw, TLDrawState, TLDrawShapeType, ColorStyle } from '@tldraw/tldraw'
export default function Api(): JSX.Element {
const rTLDrawState = React.useRef<TLDrawState>()
const handleMount = React.useCallback((state: TLDrawState) => {
rTLDrawState.current = state
state
.createShapes({
id: 'rect1',
type: TLDrawShapeType.Rectangle,
point: [100, 100],
size: [200, 200],
})
.selectAll()
.nudge([1, 1], true)
.duplicate()
.select('rect1')
.style({ color: ColorStyle.Blue })
.selectNone()
}, [])
return (
<div className="tldraw">
<TLDraw onMount={handleMount} />
</div>
)
}

View file

@ -2,29 +2,54 @@ import * as React from 'react'
import { Switch, Route, Link } from 'react-router-dom' import { Switch, Route, Link } from 'react-router-dom'
import Basic from './basic' import Basic from './basic'
import ReadOnly from './readonly' import ReadOnly from './readonly'
import Controlled from './controlled' import PropsControl from './props-control'
import Imperative from './imperative' import ApiControl from './api-control'
import LoadingFiles from './loading-files'
import Embedded from './embedded' import Embedded from './embedded'
import NoSizeEmbedded from './no-size-embedded' import NoSizeEmbedded from './no-size-embedded'
import { Multiplayer } from './multiplayer'
import ChangingId from './changing-id' import ChangingId from './changing-id'
import Persisted from './persisted'
import Develop from './develop'
import Api from './api'
import FileSystem from './file-system'
import UIOptions from './ui-options'
import { Multiplayer } from './multiplayer'
import './styles.css' import './styles.css'
export default function App(): JSX.Element { export default function App(): JSX.Element {
return ( return (
<main> <main>
<img className="hero" src="./card-repo.png" />
<Switch> <Switch>
<Route path="/basic">
<Develop />
</Route>
<Route path="/basic"> <Route path="/basic">
<Basic /> <Basic />
</Route> </Route>
<Route path="/ui-options">
<UIOptions />
</Route>
<Route path="/persisted">
<Persisted />
</Route>
<Route path="/loading-files">
<LoadingFiles />
</Route>
<Route path="/file-system">
<FileSystem />
</Route>
<Route path="/api">
<Api />
</Route>
<Route path="/readonly"> <Route path="/readonly">
<ReadOnly /> <ReadOnly />
</Route> </Route>
<Route path="/controlled"> <Route path="/controlled">
<Controlled /> <PropsControl />
</Route> </Route>
<Route path="/imperative"> <Route path="/imperative">
<Imperative /> <ApiControl />
</Route> </Route>
<Route path="/changing-id"> <Route path="/changing-id">
<ChangingId /> <ChangingId />
@ -39,30 +64,52 @@ export default function App(): JSX.Element {
<Multiplayer /> <Multiplayer />
</Route> </Route>
<Route path="/"> <Route path="/">
<ul> <ul className="links">
<li> <li>
<Link to="/basic">basic</Link> <Link to="/basic">Develop</Link>
</li>
<hr />
<li>
<Link to="/basic">Basic</Link>
</li> </li>
<li> <li>
<Link to="/readonly">readonly</Link> <Link to="/ui-options">UI Options</Link>
</li> </li>
<li> <li>
<Link to="/controlled">controlled</Link> <Link to="/persisted">Persisting State with an ID</Link>
</li> </li>
<li> <li>
<Link to="/imperative">imperative</Link> <Link to="/file-system">Using the File System</Link>
</li> </li>
<li> <li>
<Link to="/changing-id">changing id</Link> <Link to="/readonly">Readonly Mode</Link>
</li> </li>
<li> <li>
<Link to="/embedded">embedded</Link> <Link to="/loading-files">Loading Files</Link>
</li> </li>
<li> <li>
<Link to="/no-size-embedded">embedded (no size)</Link> <Link to="/file-system">Using the File System</Link>
</li> </li>
<li> <li>
<Link to="/multiplayer">multiplayer</Link> <Link to="/controlled">Controlled via Props</Link>
</li>
<li>
<Link to="/api">Using the TLDrawState API</Link>
</li>
<li>
<Link to="/imperative">Controlled via TLDrawState API</Link>
</li>
<li>
<Link to="/changing-id">Changing ID</Link>
</li>
<li>
<Link to="/embedded">Embedded</Link>
</li>
<li>
<Link to="/no-size-embedded">Embedded (without explicit size)</Link>
</li>
<li>
<Link to="/multiplayer">Multiplayer</Link>
</li> </li>
</ul> </ul>
</Route> </Route>

View file

@ -1,561 +0,0 @@
{
"id": "doc",
"pages": {
"page": {
"id": "page",
"name": "Page 1",
"childIndex": 1,
"shapes": {
"58343232-2843-48ff-214c-f801ba274f24": {
"id": "58343232-2843-48ff-214c-f801ba274f24",
"type": "rectangle",
"name": "Rectangle",
"parentId": "page",
"childIndex": 1,
"point": [
653.82,
158.58
],
"size": [
146.33,
213.43
],
"rotation": 0,
"style": {
"color": "Green",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"a755e702-548c-4ae3-0f62-307ced48d060": {
"id": "a755e702-548c-4ae3-0f62-307ced48d060",
"type": "ellipse",
"name": "Ellipse",
"parentId": "page",
"childIndex": 2,
"point": [
316.64,
164.11
],
"radius": [
73.7000000000001,
74.86500000000002
],
"rotation": 0,
"style": {
"color": "Blue",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"2ac5ddca-0fdd-4744-3c5b-dc63d9804993": {
"id": "2ac5ddca-0fdd-4744-3c5b-dc63d9804993",
"type": "rectangle",
"name": "Rectangle",
"parentId": "page",
"childIndex": 2,
"point": [
495.88,
466.31
],
"size": [
149.87,
134.37
],
"rotation": 0,
"style": {
"color": "Red",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"8f7c6768-8124-427f-0795-5b11b8e55cb3": {
"id": "8f7c6768-8124-427f-0795-5b11b8e55cb3",
"type": "arrow",
"name": "Arrow",
"parentId": "page",
"childIndex": 3,
"point": [
663.96,
388.01
],
"rotation": 0,
"bend": 0,
"handles": {
"start": {
"id": "start",
"index": 0,
"point": [
0,
66.018
],
"canBind": true,
"bindingId": "37f1f650-7368-41df-2489-b980efab5b7d"
},
"end": {
"id": "end",
"index": 1,
"point": [
37.531,
0
],
"canBind": true,
"bindingId": "284b2bbb-92eb-4f2b-255d-4b5a78e8a991"
},
"bend": {
"id": "bend",
"index": 2,
"point": [
18.765,
33.009
]
}
},
"decorations": {
"end": "Arrow"
},
"style": {
"color": "Black",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"10e6dade-7fab-42f8-0fc3-2091f8b5cb1f": {
"id": "10e6dade-7fab-42f8-0fc3-2091f8b5cb1f",
"type": "arrow",
"name": "Arrow",
"parentId": "page",
"childIndex": 4,
"point": [
479.98,
235.6
],
"rotation": 0,
"bend": 0,
"handles": {
"start": {
"id": "start",
"index": 0,
"point": [
157.84,
18.972
],
"canBind": true,
"bindingId": "500844a2-1dc9-4f58-0837-1a0caf446604"
},
"end": {
"id": "end",
"index": 1,
"point": [
0,
0
],
"canBind": true,
"bindingId": "a9c71649-7729-47c5-0e3c-37455ecd837c"
},
"bend": {
"id": "bend",
"index": 2,
"point": [
78.921,
9.4859
]
}
},
"decorations": {
"end": "Arrow"
},
"style": {
"color": "Black",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"0ef85012-7e75-4fdf-119f-18e615609335": {
"id": "0ef85012-7e75-4fdf-119f-18e615609335",
"type": "arrow",
"name": "Arrow",
"parentId": "page",
"childIndex": 5,
"point": [
414.98,
326.35
],
"rotation": 0,
"bend": 0,
"handles": {
"start": {
"id": "start",
"index": 0,
"point": [
0,
0
],
"canBind": true,
"bindingId": "aea8ac9a-0527-47ea-1d50-43862270759a"
},
"end": {
"id": "end",
"index": 1,
"point": [
74.457,
123.96
],
"canBind": true,
"bindingId": "be4cee5e-484d-4908-1079-a93aba785bd1"
},
"bend": {
"id": "bend",
"index": 2,
"point": [
37.228,
61.982
]
}
},
"decorations": {
"end": "Arrow"
},
"style": {
"color": "Black",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
}
},
"bindings": {
"284b2bbb-92eb-4f2b-255d-4b5a78e8a991": {
"id": "284b2bbb-92eb-4f2b-255d-4b5a78e8a991",
"type": "arrow",
"fromId": "8f7c6768-8124-427f-0795-5b11b8e55cb3",
"handleId": "end",
"toId": "58343232-2843-48ff-214c-f801ba274f24",
"point": [
0.67161,
0.5518
],
"distance": 16
},
"37f1f650-7368-41df-2489-b980efab5b7d": {
"id": "37f1f650-7368-41df-2489-b980efab5b7d",
"type": "arrow",
"fromId": "8f7c6768-8124-427f-0795-5b11b8e55cb3",
"handleId": "start",
"toId": "2ac5ddca-0fdd-4744-3c5b-dc63d9804993",
"point": [
0.68144,
0.58126
],
"distance": 18.210000000000036
},
"a9c71649-7729-47c5-0e3c-37455ecd837c": {
"id": "a9c71649-7729-47c5-0e3c-37455ecd837c",
"type": "arrow",
"fromId": "10e6dade-7fab-42f8-0fc3-2091f8b5cb1f",
"handleId": "end",
"toId": "a755e702-548c-4ae3-0f62-307ced48d060",
"point": [
0.84807,
0.4752
],
"distance": 16
},
"500844a2-1dc9-4f58-0837-1a0caf446604": {
"id": "500844a2-1dc9-4f58-0837-1a0caf446604",
"type": "arrow",
"fromId": "10e6dade-7fab-42f8-0fc3-2091f8b5cb1f",
"handleId": "start",
"toId": "58343232-2843-48ff-214c-f801ba274f24",
"point": [
0.5,
0.5
],
"distance": 16
},
"be4cee5e-484d-4908-1079-a93aba785bd1": {
"id": "be4cee5e-484d-4908-1079-a93aba785bd1",
"type": "arrow",
"fromId": "0ef85012-7e75-4fdf-119f-18e615609335",
"handleId": "end",
"toId": "2ac5ddca-0fdd-4744-3c5b-dc63d9804993",
"point": [
0.38339,
0.55439
],
"distance": 16
},
"aea8ac9a-0527-47ea-1d50-43862270759a": {
"id": "aea8ac9a-0527-47ea-1d50-43862270759a",
"type": "arrow",
"fromId": "0ef85012-7e75-4fdf-119f-18e615609335",
"handleId": "start",
"toId": "a755e702-548c-4ae3-0f62-307ced48d060",
"point": [
0.57016,
0.83242
],
"distance": 16
}
}
},
"page1": {
"id": "page1",
"shapes": {
"fa7a50a0-00d3-432c-3e97-585a9d97b6c6": {
"id": "fa7a50a0-00d3-432c-3e97-585a9d97b6c6",
"type": "rectangle",
"name": "Rectangle",
"parentId": "page1",
"childIndex": 1,
"point": [
1757.9,
912.27
],
"size": [
92.549,
78.049
],
"rotation": 0,
"radius": 0,
"style": {
"color": "Indigo",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"76a961f3-9cad-4a44-3790-74d8e46c918b": {
"id": "76a961f3-9cad-4a44-3790-74d8e46c918b",
"type": "rectangle",
"name": "Rectangle",
"parentId": "page1",
"childIndex": 2,
"point": [
2054.6,
956.4
],
"size": [
53.205,
136.52
],
"rotation": 0,
"radius": 0,
"style": {
"color": "Red",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"5e9a929d-6fa4-4176-3f60-9a0d206a986b": {
"id": "5e9a929d-6fa4-4176-3f60-9a0d206a986b",
"type": "ellipse",
"name": "Ellipse",
"parentId": "page1",
"childIndex": 3,
"point": [
1821.3,
1044.1
],
"radius": [
34.65772824556598,
39.066377761684635
],
"rotation": 0,
"style": {
"color": "Green",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"d6e6f6a8-8472-4a05-352e-3749bc75f487": {
"id": "d6e6f6a8-8472-4a05-352e-3749bc75f487",
"type": "arrow",
"name": "Arrow",
"parentId": "page1",
"childIndex": 4,
"point": [
1873.779,
968.915
],
"rotation": 0,
"bend": 0,
"handles": {
"start": {
"id": "start",
"index": 0,
"point": [
71.321,
20.225
],
"canBind": true
},
"end": {
"id": "end",
"index": 1,
"point": [
-8.5265e-14,
2.1316e-14
],
"canBind": true
},
"bend": {
"id": "bend",
"index": 2,
"point": [
35.661,
10.112
]
}
},
"decorations": {
"end": "Arrow"
},
"style": {
"color": "Black",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"ede2b8f7-2ce0-4b79-2770-a5d5b174f7d4": {
"id": "ede2b8f7-2ce0-4b79-2770-a5d5b174f7d4",
"type": "arrow",
"name": "Arrow",
"parentId": "page1",
"childIndex": 5,
"point": [
1781.2,
1003.8
],
"rotation": 0,
"bend": 0,
"handles": {
"start": {
"id": "start",
"index": 0,
"point": [
0,
0
],
"canBind": true
},
"end": {
"id": "end",
"index": 1,
"point": [
25.925,
43.226
],
"canBind": true
},
"bend": {
"id": "bend",
"index": 2,
"point": [
12.963,
21.613
]
}
},
"decorations": {
"end": "Arrow"
},
"style": {
"color": "Black",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
},
"8edca8a7-0c6d-414b-0ff0-e00c32733b46": {
"id": "8edca8a7-0c6d-414b-0ff0-e00c32733b46",
"type": "arrow",
"name": "Arrow",
"parentId": "page1",
"childIndex": 6,
"point": [
1912.9,
1065.7669999999998
],
"rotation": 0,
"bend": 0,
"handles": {
"start": {
"id": "start",
"index": 0,
"point": [
0,
12.333
],
"canBind": true
},
"end": {
"id": "end",
"index": 1,
"point": [
36.712,
8.3489e-14
],
"canBind": true
},
"bend": {
"id": "bend",
"index": 2,
"point": [
18.356,
6.1665
]
}
},
"decorations": {
"end": "Arrow"
},
"style": {
"color": "Black",
"size": "Medium",
"isFilled": true,
"dash": "Draw"
}
}
},
"bindings": {},
"name": "Page 2"
}
},
"pageStates": {
"page": {
"id": "page",
"selectedIds": [],
"camera": {
"point": [
-257,
-61
],
"zoom": 1
},
"editingId": null
},
"page1": {
"id": "page1",
"selectedIds": [
"76a961f3-9cad-4a44-3790-74d8e46c918b"
],
"camera": {
"point": [
-1620.9073790458274,
-841.1990943476605
],
"zoom": 1.5319298689588086
},
"currentParentId": "page1"
}
}
}

View file

@ -1,6 +1,10 @@
import * as React from 'react' import * as React from 'react'
import Editor from './components/editor' import { TLDraw } from '@tldraw/tldraw'
export default function Basic(): JSX.Element { export default function Basic(): JSX.Element {
return <Editor /> return (
<div className="tldraw">
<TLDraw />
</div>
)
} }

View file

@ -1,38 +0,0 @@
import * as React from 'react'
import { TLDraw, TLDrawProps, TLDrawState, useFileSystem } from '@tldraw/tldraw'
export default function Editor(props: TLDrawProps): JSX.Element {
const rTLDrawState = React.useRef<TLDrawState>()
const fileSystemEvents = useFileSystem()
const handleMount = React.useCallback((state: TLDrawState) => {
rTLDrawState.current = state
props.onMount?.(state)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.state = state
}, [])
const onSignIn = React.useCallback((state: TLDrawState) => {
// Sign in?
}, [])
const onSignOut = React.useCallback((state: TLDrawState) => {
// Sign out?
}, [])
return (
<div className="tldraw">
<TLDraw
id="tldraw1"
{...props}
onMount={handleMount}
onSignIn={onSignIn}
onSignOut={onSignOut}
{...fileSystemEvents}
autofocus
/>
</div>
)
}

41
example/src/develop.tsx Normal file
View file

@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as React from 'react'
import { TLDraw, TLDrawState, useFileSystem } from '@tldraw/tldraw'
declare const window: Window & { state: TLDrawState }
export default function Develop(): JSX.Element {
const rTLDrawState = React.useRef<TLDrawState>()
const fileSystemEvents = useFileSystem()
const handleMount = React.useCallback((state: TLDrawState) => {
window.state = state
rTLDrawState.current = state
}, [])
const handleSignOut = React.useCallback(() => {
// noop
}, [])
const handleSignIn = React.useCallback(() => {
// noop
}, [])
const handlePersist = React.useCallback(() => {
// noop
}, [])
return (
<div className="tldraw">
<TLDraw
id="develop"
{...fileSystemEvents}
onMount={handleMount}
onSignIn={handleSignIn}
onSignOut={handleSignOut}
onPersist={handlePersist}
/>
</div>
)
}

View file

@ -24,7 +24,7 @@ export default function Embedded(): JSX.Element {
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
<TLDraw id="small6" /> <TLDraw id="embedded" />
</div> </div>
</div> </div>
) )

View file

@ -0,0 +1,14 @@
import * as React from 'react'
import { TLDraw, useFileSystem } from '@tldraw/tldraw'
export default function FileSystem(): JSX.Element {
const fileSystemEvents = useFileSystem()
// Use the Menu > File to create, open, and save .tldr files.
return (
<div className="tldraw">
<TLDraw {...fileSystemEvents} />
</div>
)
}

View file

@ -0,0 +1,17 @@
import { TLDraw, TLDrawFile } from '@tldraw/tldraw'
import * as React from 'react'
export default function LoadingFiles(): JSX.Element {
const [file, setFile] = React.useState<TLDrawFile>()
React.useEffect(() => {
async function loadFile(): Promise<void> {
const file = await fetch('Example.tldr').then((response) => response.json())
setFile(file)
}
loadFile()
}, [])
return <TLDraw document={file?.document} />
}

View file

@ -1,3 +0,0 @@
export function Cursors() {
return <div>hi</div>
}

View file

@ -1,11 +1,6 @@
import { TLDraw } from '@tldraw/tldraw' import { TLDraw } from '@tldraw/tldraw'
import * as React from 'react' import * as React from 'react'
/**
* This is bugged until Radix gives me an option to control
* where menus are portaled.
*/
export default function NoSizeEmbedded(): JSX.Element { export default function NoSizeEmbedded(): JSX.Element {
return <TLDraw /> return <TLDraw />
} }

10
example/src/persisted.tsx Normal file
View file

@ -0,0 +1,10 @@
import * as React from 'react'
import { TLDraw } from '@tldraw/tldraw'
export default function Persisted(): JSX.Element {
return (
<div className="tldraw">
<TLDraw id="tldraw-persisted-id" />
</div>
)
}

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View file

@ -1,6 +1,21 @@
import { TLDraw, TLDrawFile } from '@tldraw/tldraw'
import * as React from 'react' import * as React from 'react'
import Editor from './components/editor'
export default function Basic(): JSX.Element { export default function ReadOnly(): JSX.Element {
return <Editor readOnly /> const [file, setFile] = React.useState<TLDrawFile>()
React.useEffect(() => {
async function loadFile(): Promise<void> {
const file = await fetch('Example.tldr').then((response) => response.json())
setFile(file)
}
loadFile()
}, [])
return (
<div className="tldraw">
<TLDraw readOnly document={file?.document} />
</div>
)
} }

View file

@ -7,6 +7,8 @@ body {
overscroll-behavior: none; overscroll-behavior: none;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
font-size: 1em;
font-family: Arial, Helvetica, sans-serif;
} }
.tldraw { .tldraw {
@ -18,3 +20,29 @@ body {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.hero {
width: 100%;
max-width: 720px;
}
.links {
display: grid;
width: fit-content;
list-style: none;
padding: 1em;
margin: 0px;
}
.links a {
padding: 0.5em 0.7em;
color: black;
text-decoration: none;
font-size: 1.2em;
display: block;
border-radius: 8px;
}
.links a:hover {
background-color: rgba(144, 144, 144, 0.1);
}

View file

@ -0,0 +1,17 @@
import * as React from 'react'
import { TLDraw } from '@tldraw/tldraw'
export default function UIOptions(): JSX.Element {
return (
<div className="tldraw">
<TLDraw
showUI={true}
showMenu={true}
showPages={true}
showStyles={true}
showTools={true}
showZoom={true}
/>
</div>
)
}

View file

@ -475,6 +475,7 @@ export class TLDrawState extends StateManager<TLDrawSnapshot> {
* @param bounds * @param bounds
*/ */
updateBounds = (bounds: TLBounds) => { updateBounds = (bounds: TLBounds) => {
if (this.readOnly) return
this.bounds = { ...bounds } this.bounds = { ...bounds }
if (this.session) { if (this.session) {
this.session.updateViewport(this.viewport) this.session.updateViewport(this.viewport)
@ -487,6 +488,8 @@ export class TLDrawState extends StateManager<TLDrawSnapshot> {
* @param id [string] * @param id [string]
*/ */
setEditingId = (id?: string) => { setEditingId = (id?: string) => {
if (this.readOnly) return
this.editingStartTime = Date.now() this.editingStartTime = Date.now()
this.patchState( this.patchState(
{ {
@ -640,7 +643,7 @@ export class TLDrawState extends StateManager<TLDrawSnapshot> {
* @param tool The tool to select, or "select". * @param tool The tool to select, or "select".
*/ */
selectTool = (type: ToolType): this => { selectTool = (type: ToolType): this => {
if (this.session) return this if (this.readOnly || this.session) return this
const tool = this.tools[type] const tool = this.tools[type]
@ -1145,6 +1148,7 @@ export class TLDrawState extends StateManager<TLDrawSnapshot> {
* @param pageId (optional) The new page's id. * @param pageId (optional) The new page's id.
*/ */
createPage = (id?: string): this => { createPage = (id?: string): this => {
if (this.readOnly) return this
return this.setState( return this.setState(
Commands.createPage(this.state, [-this.bounds.width / 2, -this.bounds.height / 2], id) Commands.createPage(this.state, [-this.bounds.width / 2, -this.bounds.height / 2], id)
) )
@ -1164,6 +1168,7 @@ export class TLDrawState extends StateManager<TLDrawSnapshot> {
* @param name The page's new name * @param name The page's new name
*/ */
renamePage = (pageId: string, name: string): this => { renamePage = (pageId: string, name: string): this => {
if (this.readOnly) return this
return this.setState(Commands.renamePage(this.state, pageId, name)) return this.setState(Commands.renamePage(this.state, pageId, name))
} }

View file

@ -1,8 +1,8 @@
import * as React from 'react' import * as React from 'react'
import { TLDrawState } from '@tldraw/tldraw' import { TLDrawState } from '@tldraw/tldraw'
export function useFileSystem(state: TLDrawState) { export function useFileSystem() {
const promptSaveBeforeChange = React.useCallback(async () => { const promptSaveBeforeChange = React.useCallback(async (state: TLDrawState) => {
if (state.isDirty) { if (state.isDirty) {
if (state.fileSystemHandle) { if (state.fileSystemHandle) {
if (window.confirm('Do you want to save changes to your current project?')) { if (window.confirm('Do you want to save changes to your current project?')) {
@ -14,25 +14,31 @@ export function useFileSystem(state: TLDrawState) {
} }
} }
} }
}, [state]) }, [])
const onNewProject = React.useCallback(async () => { const onNewProject = React.useCallback(
await promptSaveBeforeChange() async (state: TLDrawState) => {
state.newProject() await promptSaveBeforeChange(state)
}, [state, promptSaveBeforeChange]) state.newProject()
},
[promptSaveBeforeChange]
)
const onSaveProject = React.useCallback(() => { const onSaveProject = React.useCallback((state: TLDrawState) => {
state.saveProject() state.saveProject()
}, [state]) }, [])
const onSaveProjectAs = React.useCallback(() => { const onSaveProjectAs = React.useCallback((state: TLDrawState) => {
state.saveProjectAs() state.saveProjectAs()
}, [state]) }, [])
const onOpenProject = React.useCallback(async () => { const onOpenProject = React.useCallback(
await promptSaveBeforeChange() async (state: TLDrawState) => {
state.openProject() await promptSaveBeforeChange(state)
}, [state, promptSaveBeforeChange]) state.openProject()
},
[promptSaveBeforeChange]
)
return { return {
onNewProject, onNewProject,