Merge branch 'main' of https://github.com/tldraw/tldraw
This commit is contained in:
commit
c364581b52
254 changed files with 7974 additions and 7197 deletions
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
|
@ -17,4 +17,4 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
test-command: 'yarn test --ci --runInBand'
|
||||
test-command: 'yarn test --ci --runInBand --updateSnapshot'
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -12,3 +12,5 @@ coverage
|
|||
www/public/worker-*
|
||||
www/public/sw.js
|
||||
www/public/sw.js.map
|
||||
|
||||
.vercel
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Welcome to the TLDraw contributing guide <!-- omit in toc -->
|
||||
# Welcome to the tldraw contributing guide <!-- omit in toc -->
|
||||
|
||||
Thank you for investing your time in contributing to our project! Any contribution you make will be reflected in the @tldraw/tldraw package and at [tldraw.com](https://tldraw.com).
|
||||
|
||||
|
@ -77,4 +77,4 @@ When you're finished with the changes, create a pull request, also known as a PR
|
|||
|
||||
Congratulations :tada::tada: The GitHub team thanks you :sparkles:.
|
||||
|
||||
Once your PR is merged, your contributions will become part of the next TLDraw release, and will be visible in the [TLDraw app](https://tldraw.com).
|
||||
Once your PR is merged, your contributions will become part of the next tldraw release, and will be visible in the [tldraw app](https://tldraw.com).
|
||||
|
|
42
README.md
42
README.md
|
@ -4,13 +4,13 @@
|
|||
|
||||
# @tldraw/tldraw
|
||||
|
||||
This package contains the [TLDraw](https://tldraw.com) editor as a React component named `<TLDraw>`. You can use this package to embed the editor in any React application.
|
||||
This package contains the [tldraw](https://tldraw.com) editor as a React component named `<Tldraw>`. You can use this package to embed the editor in any React application.
|
||||
|
||||
💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
|
||||
|
||||
🙌 Questions? Join the [Discord channel](https://discord.gg/SBBEVCA4PG) or start a [discussion](https://github.com/tldraw/tldraw/discussions/new).
|
||||
|
||||
🎨 Want to build your own TLDraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core).
|
||||
🎨 Want to build your own tldraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core).
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -24,13 +24,13 @@ npm i @tldraw/tldraw
|
|||
|
||||
## Usage
|
||||
|
||||
Import the `TLDraw` React component and use it in your app.
|
||||
Import the `tldraw` React component and use it in your app.
|
||||
|
||||
```tsx
|
||||
import { TLDraw } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
return <TLDraw />
|
||||
return <Tldraw />
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -39,58 +39,58 @@ function App() {
|
|||
You can use the `id` to persist the state in a user's browser storage.
|
||||
|
||||
```tsx
|
||||
import { TLDraw } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
return <TLDraw id="myState" />
|
||||
return <Tldraw id="myState" />
|
||||
}
|
||||
```
|
||||
|
||||
### Controlling the Component through Props
|
||||
|
||||
You can control the `TLDraw` component through its props.
|
||||
You can control the `tldraw` component through its props.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawDocument } from '@tldraw/tldraw'
|
||||
import { Tldraw, TDDocument } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const myDocument: TLDrawDocument = {}
|
||||
const myDocument: TDDocument = {}
|
||||
|
||||
return <TLDraw document={document} />
|
||||
return <Tldraw document={document} />
|
||||
}
|
||||
```
|
||||
|
||||
### Controlling the Component through the TLDrawState API
|
||||
### Controlling the Component through the tldrawApp API
|
||||
|
||||
You can also control the `TLDraw` component imperatively through the `TLDrawState` API.
|
||||
You can also control the `tldraw` component imperatively through the `tldrawApp` API.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||
import { Tldraw, tldrawApp } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
||||
const handleMount = React.useCallback((state: tldrawApp) => {
|
||||
state.selectAll()
|
||||
}, [])
|
||||
|
||||
return <TLDraw onMount={handleMount} />
|
||||
return <Tldraw onMount={handleMount} />
|
||||
}
|
||||
```
|
||||
|
||||
Internally, the `TLDraw` component's user interface uses this API to make changes to the component's state. See the `TLDrawState` section of the [documentation](guides/documentation) for more on this API.
|
||||
Internally, the `tldraw` component's user interface uses this API to make changes to the component's state. See the `tldrawApp` section of the [documentation](guides/documentation) for more on this API.
|
||||
|
||||
### Responding to Changes
|
||||
|
||||
You can respond to changes and user actions using the `onChange` callback. For more specific changes, you can also use the `onPatch`, `onCommand`, or `onPersist` callbacks. See the [documentation](guides/documentation) for more.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||
import { Tldraw, tldrawApp } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const handleChange = React.useCallback((state: TLDrawState, reason: string) => {
|
||||
const handleChange = React.useCallback((state: tldrawApp, reason: string) => {
|
||||
// Do something with the change
|
||||
}, [])
|
||||
|
||||
return <TLDraw onMount={handleMount} />
|
||||
return <Tldraw onMount={handleMount} />
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -108,7 +108,7 @@ See the [development guide](/guides/development.md).
|
|||
|
||||
## Example
|
||||
|
||||
See the `example` folder for examples of how to use the `<TLDraw/>` component.
|
||||
See the `example` folder for examples of how to use the `<Tldraw/>` component.
|
||||
|
||||
## Community
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# @tldraw/tldraw-electron
|
||||
|
||||
An electron wrapper for TLDraw. Not yet published.
|
||||
An electron wrapper for tldraw. Not yet published.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ export async function createWindow() {
|
|||
minHeight: 480,
|
||||
minWidth: 600,
|
||||
titleBarStyle: 'hidden',
|
||||
title: 'TLDraw',
|
||||
title: 'Tldraw',
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
devTools: true,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import type { Message, TLApi } from 'src/types'
|
||||
import type { Message, TldrawBridgeApi } from 'src/types'
|
||||
|
||||
const api: TLApi = {
|
||||
const api: TldrawBridgeApi = {
|
||||
send: (channel: string, data: Message) => {
|
||||
ipcRenderer.send(channel, data)
|
||||
},
|
||||
|
@ -10,6 +10,6 @@ const api: TLApi = {
|
|||
},
|
||||
}
|
||||
|
||||
contextBridge?.exposeInMainWorld('TLApi', api)
|
||||
contextBridge?.exposeInMainWorld('TldrawBridgeApi', api)
|
||||
|
||||
export {}
|
||||
|
|
|
@ -1,85 +1,86 @@
|
|||
import * as React from 'react'
|
||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||
import type { IpcMainEvent, IpcMain, IpcRenderer } from 'electron'
|
||||
import type { Message, TLApi } from 'src/types'
|
||||
import { Tldraw, TldrawApp } from '@tldraw/tldraw'
|
||||
import type { Message, TldrawBridgeApi } from 'src/types'
|
||||
|
||||
declare const window: Window & { TldrawBridgeApi: TldrawBridgeApi }
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
const rTLDrawState = React.useRef<TLDrawState>()
|
||||
const rTldrawApp = React.useRef<TldrawApp>()
|
||||
|
||||
// When the editor mounts, save the state instance in a ref.
|
||||
const handleMount = React.useCallback((tldr: TLDrawState) => {
|
||||
rTLDrawState.current = tldr
|
||||
const handleMount = React.useCallback((tldr: TldrawApp) => {
|
||||
rTldrawApp.current = tldr
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
function handleEvent(message: Message) {
|
||||
const state = rTLDrawState.current
|
||||
if (!state) return
|
||||
const app = rTldrawApp.current
|
||||
if (!app) return
|
||||
|
||||
switch (message.type) {
|
||||
case 'resetZoom': {
|
||||
state.resetZoom()
|
||||
app.resetZoom()
|
||||
break
|
||||
}
|
||||
case 'zoomIn': {
|
||||
state.zoomIn()
|
||||
app.zoomIn()
|
||||
break
|
||||
}
|
||||
case 'zoomOut': {
|
||||
state.zoomOut()
|
||||
app.zoomOut()
|
||||
break
|
||||
}
|
||||
case 'zoomToFit': {
|
||||
state.zoomToFit()
|
||||
app.zoomToFit()
|
||||
break
|
||||
}
|
||||
case 'zoomToSelection': {
|
||||
state.zoomToSelection()
|
||||
app.zoomToSelection()
|
||||
break
|
||||
}
|
||||
case 'undo': {
|
||||
state.undo()
|
||||
app.undo()
|
||||
break
|
||||
}
|
||||
case 'redo': {
|
||||
state.redo()
|
||||
app.redo()
|
||||
break
|
||||
}
|
||||
case 'cut': {
|
||||
state.cut()
|
||||
app.cut()
|
||||
break
|
||||
}
|
||||
case 'copy': {
|
||||
state.copy()
|
||||
app.copy()
|
||||
break
|
||||
}
|
||||
case 'paste': {
|
||||
state.paste()
|
||||
app.paste()
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
state.delete()
|
||||
app.delete()
|
||||
break
|
||||
}
|
||||
case 'selectAll': {
|
||||
state.selectAll()
|
||||
app.selectAll()
|
||||
break
|
||||
}
|
||||
case 'selectNone': {
|
||||
state.selectNone()
|
||||
app.selectNone()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { send, on } = (window as unknown as Window & { TLApi: TLApi })['TLApi']
|
||||
const { on } = window.TldrawBridgeApi
|
||||
|
||||
on('projectMsg', handleEvent)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw id="electron" onMount={handleMount} autofocus showMenu={false} />
|
||||
<Tldraw id="electron" onMount={handleMount} autofocus showMenu={false} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
0
electron/src/renderer/decs.d.ts
vendored
0
electron/src/renderer/decs.d.ts
vendored
|
@ -13,7 +13,7 @@ export type Message =
|
|||
| { type: 'selectAll' }
|
||||
| { type: 'selectNone' }
|
||||
|
||||
export type TLApi = {
|
||||
export type TldrawBridgeApi = {
|
||||
send: (channel: string, data: Message) => void
|
||||
on: (channel: string, cb: (message: Message) => void) => void
|
||||
}
|
||||
|
|
|
@ -35,5 +35,9 @@
|
|||
"rimraf": "3.0.2",
|
||||
"typescript": "4.2.3"
|
||||
},
|
||||
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
|
||||
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6",
|
||||
"dependencies": {
|
||||
"@liveblocks/client": "^0.12.3",
|
||||
"@liveblocks/react": "^0.12.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import { ColorStyle, TLDraw, TLDrawShapeType, TLDrawState } from '@tldraw/tldraw'
|
||||
import { ColorStyle, Tldraw, TDShapeType, TldrawApp } from '@tldraw/tldraw'
|
||||
|
||||
export default function Imperative(): JSX.Element {
|
||||
const rTLDrawState = React.useRef<TLDrawState>()
|
||||
const rTldrawApp = React.useRef<TldrawApp>()
|
||||
|
||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
||||
rTLDrawState.current = state
|
||||
const handleMount = React.useCallback((app: TldrawApp) => {
|
||||
rTldrawApp.current = app
|
||||
|
||||
state.createShapes(
|
||||
app.createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
type: TDShapeType.Rectangle,
|
||||
name: 'Rectangle',
|
||||
childIndex: 1,
|
||||
point: [0, 0],
|
||||
|
@ -20,7 +20,7 @@ export default function Imperative(): JSX.Element {
|
|||
{
|
||||
id: 'rect2',
|
||||
name: 'Rectangle',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
type: TDShapeType.Rectangle,
|
||||
point: [200, 200],
|
||||
size: [100, 100],
|
||||
}
|
||||
|
@ -30,13 +30,13 @@ export default function Imperative(): JSX.Element {
|
|||
React.useEffect(() => {
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
const state = rTLDrawState.current!
|
||||
const rect1 = state.getShape('rect1')
|
||||
const app = rTldrawApp.current!
|
||||
const rect1 = app.getShape('rect1')
|
||||
|
||||
if (!rect1) {
|
||||
state.createShapes({
|
||||
app.createShapes({
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
type: TDShapeType.Rectangle,
|
||||
name: 'Rectangle',
|
||||
childIndex: 1,
|
||||
point: [0, 0],
|
||||
|
@ -47,7 +47,7 @@ export default function Imperative(): JSX.Element {
|
|||
|
||||
const color = i % 2 ? ColorStyle.Red : ColorStyle.Blue
|
||||
|
||||
state.patchShapes({
|
||||
app.patchShapes({
|
||||
id: 'rect1',
|
||||
style: {
|
||||
...rect1.style,
|
||||
|
@ -60,5 +60,5 @@ export default function Imperative(): JSX.Element {
|
|||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return <TLDraw onMount={handleMount} />
|
||||
return <Tldraw onMount={handleMount} />
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import * as React from 'react'
|
||||
import { TLDraw, TLDrawState, TLDrawShapeType, ColorStyle } from '@tldraw/tldraw'
|
||||
import { Tldraw, TldrawApp, TDShapeType, ColorStyle } from '@tldraw/tldraw'
|
||||
|
||||
declare const window: Window & { app: TldrawApp }
|
||||
|
||||
export default function Api(): JSX.Element {
|
||||
const rTLDrawState = React.useRef<TLDrawState>()
|
||||
const rTldrawApp = React.useRef<TldrawApp>()
|
||||
|
||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
||||
rTLDrawState.current = state
|
||||
const handleMount = React.useCallback((app: TldrawApp) => {
|
||||
rTldrawApp.current = app
|
||||
|
||||
state
|
||||
window.app = app
|
||||
|
||||
app
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
type: TDShapeType.Rectangle,
|
||||
point: [100, 100],
|
||||
size: [200, 200],
|
||||
})
|
||||
|
@ -24,7 +28,7 @@ export default function Api(): JSX.Element {
|
|||
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw onMount={handleMount} />
|
||||
<Tldraw onMount={handleMount} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,9 +19,8 @@ import './styles.css'
|
|||
export default function App(): JSX.Element {
|
||||
return (
|
||||
<main>
|
||||
<img className="hero" src="./card-repo.png" />
|
||||
<Switch>
|
||||
<Route path="/basic">
|
||||
<Route path="/develop">
|
||||
<Develop />
|
||||
</Route>
|
||||
<Route path="/basic">
|
||||
|
@ -64,9 +63,10 @@ export default function App(): JSX.Element {
|
|||
<Multiplayer />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<img className="hero" src="./card-repo.png" />
|
||||
<ul className="links">
|
||||
<li>
|
||||
<Link to="/basic">Develop</Link>
|
||||
<Link to="/develop">Develop</Link>
|
||||
</li>
|
||||
<hr />
|
||||
<li>
|
||||
|
@ -94,10 +94,10 @@ export default function App(): JSX.Element {
|
|||
<Link to="/controlled">Controlled via Props</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/api">Using the TLDrawState API</Link>
|
||||
<Link to="/api">Using the TldrawApp API</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/imperative">Controlled via TLDrawState API</Link>
|
||||
<Link to="/imperative">Controlled via TldrawApp API</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/changing-id">Changing ID</Link>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as React from 'react'
|
||||
import { TLDraw } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
|
||||
export default function Basic(): JSX.Element {
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw />
|
||||
<Tldraw />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import { TLDraw } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
|
||||
export default function ChangingId() {
|
||||
const [id, setId] = React.useState('example')
|
||||
|
@ -10,5 +10,5 @@ export default function ChangingId() {
|
|||
return () => clearTimeout(timeout)
|
||||
}, [])
|
||||
|
||||
return <TLDraw id={id} />
|
||||
return <Tldraw id={id} />
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import * as React from 'react'
|
||||
import { TLDraw, TLDrawState, useFileSystem } from '@tldraw/tldraw'
|
||||
import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
|
||||
|
||||
declare const window: Window & { state: TLDrawState }
|
||||
declare const window: Window & { app: TldrawApp }
|
||||
|
||||
export default function Develop(): JSX.Element {
|
||||
const rTLDrawState = React.useRef<TLDrawState>()
|
||||
const rTldrawApp = React.useRef<TldrawApp>()
|
||||
|
||||
const fileSystemEvents = useFileSystem()
|
||||
|
||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
||||
window.state = state
|
||||
rTLDrawState.current = state
|
||||
const handleMount = React.useCallback((app: TldrawApp) => {
|
||||
window.app = app
|
||||
rTldrawApp.current = app
|
||||
}, [])
|
||||
|
||||
const handleSignOut = React.useCallback(() => {
|
||||
|
@ -28,13 +28,14 @@ export default function Develop(): JSX.Element {
|
|||
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw
|
||||
<Tldraw
|
||||
id="develop"
|
||||
{...fileSystemEvents}
|
||||
onMount={handleMount}
|
||||
onSignIn={handleSignIn}
|
||||
onSignOut={handleSignOut}
|
||||
onPersist={handlePersist}
|
||||
showSponsorLink={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TLDraw } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import * as React from 'react'
|
||||
|
||||
export default function Embedded(): JSX.Element {
|
||||
|
@ -13,7 +13,7 @@ export default function Embedded(): JSX.Element {
|
|||
marginBottom: '32px',
|
||||
}}
|
||||
>
|
||||
<TLDraw id="small5" />
|
||||
<Tldraw id="small5" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -24,7 +24,7 @@ export default function Embedded(): JSX.Element {
|
|||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<TLDraw id="embedded" />
|
||||
<Tldraw id="embedded" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import { TLDraw, useFileSystem } from '@tldraw/tldraw'
|
||||
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
||||
|
||||
export default function FileSystem(): JSX.Element {
|
||||
const fileSystemEvents = useFileSystem()
|
||||
|
@ -8,7 +8,7 @@ export default function FileSystem(): JSX.Element {
|
|||
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw {...fileSystemEvents} />
|
||||
<Tldraw {...fileSystemEvents} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { TLDraw, TLDrawFile } from '@tldraw/tldraw'
|
||||
import { Tldraw, TDFile } from '@tldraw/tldraw'
|
||||
import * as React from 'react'
|
||||
|
||||
export default function LoadingFiles(): JSX.Element {
|
||||
const [file, setFile] = React.useState<TLDrawFile>()
|
||||
const [file, setFile] = React.useState<TDFile>()
|
||||
|
||||
React.useEffect(() => {
|
||||
async function loadFile(): Promise<void> {
|
||||
|
@ -13,5 +13,5 @@ export default function LoadingFiles(): JSX.Element {
|
|||
loadFile()
|
||||
}, [])
|
||||
|
||||
return <TLDraw document={file?.document} />
|
||||
return <Tldraw document={file?.document} />
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser } from '@tldraw/tldraw'
|
||||
import { Tldraw, TldrawApp, TDDocument, TDUser } from '@tldraw/tldraw'
|
||||
import { createClient, Presence } from '@liveblocks/client'
|
||||
import { LiveblocksProvider, RoomProvider, useErrorListener, useObject } from '@liveblocks/react'
|
||||
import { Utils } from '@tldraw/core'
|
||||
|
||||
interface TLDrawUserPresence extends Presence {
|
||||
user: TLDrawUser
|
||||
declare const window: Window & { app: TldrawApp }
|
||||
|
||||
interface TDUserPresence extends Presence {
|
||||
user: TDUser
|
||||
}
|
||||
|
||||
const client = createClient({
|
||||
|
@ -20,37 +22,34 @@ export function Multiplayer() {
|
|||
return (
|
||||
<LiveblocksProvider client={client}>
|
||||
<RoomProvider id={roomId}>
|
||||
<TLDrawWrapper />
|
||||
<TldrawWrapper />
|
||||
</RoomProvider>
|
||||
</LiveblocksProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TLDrawWrapper() {
|
||||
function TldrawWrapper() {
|
||||
const [docId] = React.useState(() => Utils.uniqueId())
|
||||
|
||||
const [error, setError] = React.useState<Error>()
|
||||
|
||||
const [state, setstate] = React.useState<TLDrawState>()
|
||||
const [app, setApp] = React.useState<TldrawApp>()
|
||||
|
||||
useErrorListener((err) => setError(err))
|
||||
|
||||
const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', {
|
||||
const doc = useObject<{ uuid: string; document: TDDocument }>('doc', {
|
||||
uuid: docId,
|
||||
document: {
|
||||
...TLDrawState.defaultDocument,
|
||||
...TldrawApp.defaultDocument,
|
||||
id: roomId,
|
||||
},
|
||||
})
|
||||
|
||||
// Put the state into the window, for debugging.
|
||||
const handleMount = React.useCallback(
|
||||
(state: TLDrawState) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.state = state
|
||||
state.loadRoom(roomId)
|
||||
setstate(state)
|
||||
(app: TldrawApp) => {
|
||||
window.app = app
|
||||
setApp(app)
|
||||
},
|
||||
[roomId]
|
||||
)
|
||||
|
@ -60,17 +59,11 @@ function TLDrawWrapper() {
|
|||
|
||||
if (!room) return
|
||||
if (!doc) return
|
||||
if (!state) return
|
||||
if (!state.state.room) return
|
||||
|
||||
// Update the user's presence with the user from state
|
||||
const { users, userId } = state.state.room
|
||||
|
||||
room.updatePresence({ id: userId, user: users[userId] })
|
||||
if (!app) return
|
||||
|
||||
// Subscribe to presence changes; when others change, update the state
|
||||
room.subscribe<TLDrawUserPresence>('others', (others) => {
|
||||
state.updateUsers(
|
||||
room.subscribe<TDUserPresence>('others', (others) => {
|
||||
app.updateUsers(
|
||||
others
|
||||
.toArray()
|
||||
.filter((other) => other.presence)
|
||||
|
@ -81,23 +74,23 @@ function TLDrawWrapper() {
|
|||
|
||||
room.subscribe('event', (event) => {
|
||||
if (event.event?.name === 'exit') {
|
||||
state.removeUser(event.event.userId)
|
||||
app.removeUser(event.event.userId)
|
||||
}
|
||||
})
|
||||
|
||||
function handleDocumentUpdates() {
|
||||
if (!doc) return
|
||||
if (!state) return
|
||||
if (!state.state.room) return
|
||||
if (!app) return
|
||||
if (!app.room) return
|
||||
|
||||
const docObject = doc.toObject()
|
||||
|
||||
// Only merge the change if it caused by someone else
|
||||
if (docObject.uuid !== docId) {
|
||||
state.mergeDocument(docObject.document)
|
||||
app.mergeDocument(docObject.document)
|
||||
} else {
|
||||
state.updateUsers(
|
||||
Object.values(state.state.room.users).map((user) => {
|
||||
app.updateUsers(
|
||||
Object.values(app.room.users).map((user) => {
|
||||
return {
|
||||
...user,
|
||||
selectedIds: user.selectedIds,
|
||||
|
@ -108,8 +101,8 @@ function TLDrawWrapper() {
|
|||
}
|
||||
|
||||
function handleExit() {
|
||||
if (!(state && state.state.room)) return
|
||||
room?.broadcastEvent({ name: 'exit', userId: state.state.room.userId })
|
||||
if (!(app && app.room)) return
|
||||
room?.broadcastEvent({ name: 'exit', userId: app.room.userId })
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', handleExit)
|
||||
|
@ -121,26 +114,33 @@ function TLDrawWrapper() {
|
|||
const newDocument = doc.toObject().document
|
||||
|
||||
if (newDocument) {
|
||||
state.loadDocument(newDocument)
|
||||
app.loadDocument(newDocument)
|
||||
app.loadRoom(roomId)
|
||||
|
||||
// Update the user's presence with the user from state
|
||||
if (app.state.room) {
|
||||
const { users, userId } = app.state.room
|
||||
room.updatePresence({ id: userId, user: users[userId] })
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleExit)
|
||||
doc.unsubscribe(handleDocumentUpdates)
|
||||
}
|
||||
}, [doc, docId, state])
|
||||
}, [doc, docId, app])
|
||||
|
||||
const handlePersist = React.useCallback(
|
||||
(state: TLDrawState) => {
|
||||
doc?.update({ uuid: docId, document: state.document })
|
||||
(app: TldrawApp) => {
|
||||
doc?.update({ uuid: docId, document: app.document })
|
||||
},
|
||||
[docId, doc]
|
||||
)
|
||||
|
||||
const handleUserChange = React.useCallback(
|
||||
(state: TLDrawState, user: TLDrawUser) => {
|
||||
(app: TldrawApp, user: TDUser) => {
|
||||
const room = client.getRoom(roomId)
|
||||
room?.updatePresence({ id: state.state.room?.userId, user })
|
||||
room?.updatePresence({ id: app.room?.userId, user })
|
||||
},
|
||||
[client]
|
||||
)
|
||||
|
@ -151,7 +151,7 @@ function TLDrawWrapper() {
|
|||
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw
|
||||
<Tldraw
|
||||
onMount={handleMount}
|
||||
onPersist={handlePersist}
|
||||
onUserChange={handleUserChange}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { TLDraw } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import * as React from 'react'
|
||||
|
||||
export default function NoSizeEmbedded(): JSX.Element {
|
||||
return <TLDraw />
|
||||
return <Tldraw />
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as React from 'react'
|
||||
import { TLDraw } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
|
||||
export default function Persisted(): JSX.Element {
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw id="tldraw-persisted-id" />
|
||||
<Tldraw id="Tldraw-persisted-id" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
TLDraw,
|
||||
Tldraw,
|
||||
ColorStyle,
|
||||
DashStyle,
|
||||
SizeStyle,
|
||||
TLDrawDocument,
|
||||
TLDrawShapeType,
|
||||
TLDrawState,
|
||||
TDDocument,
|
||||
TDShapeType,
|
||||
TldrawApp,
|
||||
} from '@tldraw/tldraw'
|
||||
|
||||
export default function Controlled() {
|
||||
const rDocument = React.useRef<TLDrawDocument>({
|
||||
const rDocument = React.useRef<TDDocument>({
|
||||
name: 'New Document',
|
||||
version: TLDrawState.version,
|
||||
version: TldrawApp.version,
|
||||
id: 'doc',
|
||||
pages: {
|
||||
page1: {
|
||||
|
@ -20,7 +20,7 @@ export default function Controlled() {
|
|||
shapes: {
|
||||
rect1: {
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
type: TDShapeType.Rectangle,
|
||||
parentId: 'page1',
|
||||
name: 'Rectangle',
|
||||
childIndex: 1,
|
||||
|
@ -34,7 +34,7 @@ export default function Controlled() {
|
|||
},
|
||||
rect2: {
|
||||
id: 'rect2',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
type: TDShapeType.Rectangle,
|
||||
parentId: 'page1',
|
||||
name: 'Rectangle',
|
||||
childIndex: 1,
|
||||
|
@ -62,7 +62,7 @@ export default function Controlled() {
|
|||
},
|
||||
})
|
||||
|
||||
const [doc, setDoc] = React.useState<TLDrawDocument>(rDocument.current)
|
||||
const [doc, setDoc] = React.useState<TDDocument>(rDocument.current)
|
||||
|
||||
React.useEffect(() => {
|
||||
let i = 0
|
||||
|
@ -105,5 +105,5 @@ export default function Controlled() {
|
|||
rDocument.current = state.document
|
||||
}, [])
|
||||
|
||||
return <TLDraw document={doc} onChange={handleChange} />
|
||||
return <Tldraw document={doc} onChange={handleChange} />
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { TLDraw, TLDrawFile } from '@tldraw/tldraw'
|
||||
import { Tldraw, TDFile } from '@tldraw/tldraw'
|
||||
import * as React from 'react'
|
||||
|
||||
export default function ReadOnly(): JSX.Element {
|
||||
const [file, setFile] = React.useState<TLDrawFile>()
|
||||
const [file, setFile] = React.useState<TDFile>()
|
||||
|
||||
React.useEffect(() => {
|
||||
async function loadFile(): Promise<void> {
|
||||
|
@ -15,7 +15,7 @@ export default function ReadOnly(): JSX.Element {
|
|||
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw readOnly document={file?.document} />
|
||||
<Tldraw readOnly document={file?.document} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as React from 'react'
|
||||
import { TLDraw } from '@tldraw/tldraw'
|
||||
import { Tldraw } from '@tldraw/tldraw'
|
||||
|
||||
export default function UIOptions(): JSX.Element {
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<TLDraw
|
||||
<Tldraw
|
||||
showUI={true}
|
||||
showMenu={true}
|
||||
showPages={true}
|
||||
|
|
|
@ -8,7 +8,7 @@ From the root folder:
|
|||
|
||||
- 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.
|
||||
**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:
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Introduction
|
||||
|
||||
This file contains the documentatin for the `<TLDraw>` component as well as the data model that the component accepts.
|
||||
This file contains the documentatin for the `<Tldraw>` component as well as the data model that the component accepts.
|
||||
|
||||
In addition to the docs written below, this project also includes **generated documentation**. To view the generated docs:
|
||||
|
||||
|
@ -10,38 +10,39 @@ In addition to the docs written below, this project also includes **generated do
|
|||
2. Open the file at:
|
||||
|
||||
```
|
||||
/packages/tldraw/docs/classes/TLDrawState.html
|
||||
/packages/tldraw/docs/classes/TldrawApp.html
|
||||
```
|
||||
|
||||
## `TLDraw`
|
||||
## `tldraw`
|
||||
|
||||
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 `TldrawApp`'s imperative API. **All props are optional.**
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ----------------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | `string` | An id under which to persist the component's state. |
|
||||
| `document` | `TLDrawDocument` | An initial [`TLDrawDocument`](#tldrawdocument) object. |
|
||||
| `currentPageId` | `string` | A current page id, referencing the `TLDrawDocument` object provided via the `document` prop. |
|
||||
| `autofocus` | `boolean` | Whether the editor should immediately receive focus. Defaults to true. |
|
||||
| `showMenu` | `boolean` | Whether to show the menu. |
|
||||
| `showPages` | `boolean` | Whether to show the pages menu. |
|
||||
| `showStyles` | `boolean` | Whether to show the styles menu. |
|
||||
| `showTools` | `boolean` | Whether to show the tools. |
|
||||
| `showUI` | `boolean` | Whether to show any UI other than the canvas. |
|
||||
| `onMount` | `Function` | Called when the editor first mounts, receiving the current `TLDrawState`. |
|
||||
| `onPatch` | `Function` | Called when the state is updated via a patch. |
|
||||
| `onCommand` | `Function` | Called when the state is updated via a command. |
|
||||
| `onPersist` | `Function` | Called when the state is persisted after an action. |
|
||||
| `onChange` | `Function` | Called when the `TLDrawState` updates for any reason. |
|
||||
| `onUserChange` | `Function` | Called when the user's "presence" information changes. |
|
||||
| `onUndo` | `Function` | Called when the `TLDrawState` updates after an undo. |
|
||||
| `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. |
|
||||
| Prop | Type | Description |
|
||||
| ----------------- | ------------ | --------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | `string` | An id under which to persist the component's state. |
|
||||
| `document` | `TDDocument` | An initial [`TDDocument`](#TDDocument) object. |
|
||||
| `currentPageId` | `string` | A current page id, referencing the `TDDocument` object provided via the `document` prop. |
|
||||
| `autofocus` | `boolean` | Whether the editor should immediately receive focus. Defaults to true. |
|
||||
| `showMenu` | `boolean` | Whether to show the menu. |
|
||||
| `showPages` | `boolean` | Whether to show the pages menu. |
|
||||
| `showStyles` | `boolean` | Whether to show the styles menu. |
|
||||
| `showTools` | `boolean` | Whether to show the tools. |
|
||||
| `showUI` | `boolean` | Whether to show any UI other than the canvas. |
|
||||
| `showSponsorLink` | `boolean` | Whether to show a sponsor link. |
|
||||
| `onMount` | `Function` | Called when the editor first mounts, receiving the current `TldrawApp`. |
|
||||
| `onPatch` | `Function` | Called when the state is updated via a patch. |
|
||||
| `onCommand` | `Function` | Called when the state is updated via a command. |
|
||||
| `onPersist` | `Function` | Called when the state is persisted after an action. |
|
||||
| `onChange` | `Function` | Called when the `TldrawApp` updates for any reason. |
|
||||
| `onUserChange` | `Function` | Called when the user's "presence" information changes. |
|
||||
| `onUndo` | `Function` | Called when the `TldrawApp` updates after an undo. |
|
||||
| `onRedo` | `Function` | Called when the `TldrawApp` 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`.
|
||||
|
||||
|
@ -50,30 +51,30 @@ The `TLDraw` React component is the [tldraw](https://tldraw.com) editor exported
|
|||
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'
|
||||
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const fileSystemEvents = useFileSystem()
|
||||
|
||||
return <TLDraw {...fileSystemEvents} />
|
||||
return <Tldraw {...fileSystemEvents} />
|
||||
}
|
||||
```
|
||||
|
||||
## `TLDrawDocument`
|
||||
## `TDDocument`
|
||||
|
||||
You can initialize or control the `<TLDraw>` component via its `document` property. A `TLDrawDocument` is an object with three properties:
|
||||
You can initialize or control the `<Tldraw>` component via its `document` property. A `TDDocument` is an object with three properties:
|
||||
|
||||
- `id` - A unique ID for this document
|
||||
- `pages` - A table of `TLDrawPage` objects
|
||||
- `pages` - A table of `TDPage` objects
|
||||
- `pageStates` - A table of `TLPageState` objects
|
||||
- `version` - The document's version, used internally for migrations.
|
||||
|
||||
```ts
|
||||
import { TLDrawDocument, TLDrawState } from '@tldraw/tldraw'
|
||||
import { TDDocument, TldrawApp } from '@tldraw/tldraw'
|
||||
|
||||
const myDocument: TLDrawDocument = {
|
||||
const myDocument: TDDocument = {
|
||||
id: 'doc',
|
||||
version: TLDrawState.version,
|
||||
version: TldrawApp.version,
|
||||
pages: {
|
||||
page1: {
|
||||
id: 'page1',
|
||||
|
@ -95,34 +96,34 @@ const myDocument: TLDrawDocument = {
|
|||
}
|
||||
|
||||
function App() {
|
||||
return <TLDraw document={myDocument} />
|
||||
return <Tldraw document={myDocument} />
|
||||
}
|
||||
```
|
||||
|
||||
**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.
|
||||
**Tip:** The pages and pageStates in tldraw are objects containing `TLPage` and `TLPageState` objects from the [@tldraw/core](https://github.com/tldraw/core) library.
|
||||
|
||||
**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.
|
||||
|
||||
## Shapes
|
||||
|
||||
Your `TLPage` objects may include shapes: objects that fit one of the `TLDrawShape` interfaces listed below. All `TLDrawShapes` extends a common interface:
|
||||
Your `TLPage` objects may include shapes: objects that fit one of the `TldrawShape` interfaces listed below. All `TldrawShapes` extends a common interface:
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------------- | ---------------- | --------------------------------------------------------------- |
|
||||
| `id` | `string` | A unique ID for the shape. |
|
||||
| `name` | `string` | The shape's name. |
|
||||
| `type` | `string` | The shape's type. |
|
||||
| `parentId` | `string` | The ID of the shape's parent (a shape or its page). |
|
||||
| `childIndex` | `number` | The shape's order within its parent's children, indexed from 1. |
|
||||
| `point` | `number[]` | The `[x, y]` position of the shape. |
|
||||
| `rotation` | `number[]` | (optional) The shape's rotation in radians. |
|
||||
| `children` | `string[]` | (optional) The shape's child shape ids. |
|
||||
| `handles` | `TLDrawHandle{}` | (optional) A table of `TLHandle` objects. |
|
||||
| `isLocked` | `boolean` | (optional) True if the shape is locked. |
|
||||
| `isHidden` | `boolean` | (optional) True if the shape is hidden. |
|
||||
| `isEditing` | `boolean` | (optional) True if the shape is currently editing. |
|
||||
| `isGenerated` | `boolean` | (optional) True if the shape is generated. |
|
||||
| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. |
|
||||
| Property | Type | Description |
|
||||
| --------------------- | ------------ | --------------------------------------------------------------- |
|
||||
| `id` | `string` | A unique ID for the shape. |
|
||||
| `name` | `string` | The shape's name. |
|
||||
| `type` | `string` | The shape's type. |
|
||||
| `parentId` | `string` | The ID of the shape's parent (a shape or its page). |
|
||||
| `childIndex` | `number` | The shape's order within its parent's children, indexed from 1. |
|
||||
| `point` | `number[]` | The `[x, y]` position of the shape. |
|
||||
| `rotation` | `number[]` | (optional) The shape's rotation in radians. |
|
||||
| `children` | `string[]` | (optional) The shape's child shape ids. |
|
||||
| `handles` | `TDHandle{}` | (optional) A table of `TLHandle` objects. |
|
||||
| `isLocked` | `boolean` | (optional) True if the shape is locked. |
|
||||
| `isHidden` | `boolean` | (optional) True if the shape is hidden. |
|
||||
| `isEditing` | `boolean` | (optional) True if the shape is currently editing. |
|
||||
| `isGenerated` | `boolean` | (optional) True if the shape is generated. |
|
||||
| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. |
|
||||
|
||||
> **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.
|
||||
|
||||
|
@ -197,26 +198,26 @@ A binding is a connection **from** one shape and **to** another shape. At the mo
|
|||
| `distance` | `number` | The distance from the bound point. |
|
||||
| `point` | `number[]` | A normalized point representing the bound point. |
|
||||
|
||||
## `TLDrawState` API
|
||||
## `TldrawApp` API
|
||||
|
||||
You can change the `TLDraw` component's state through an imperative API called `TLDrawState`. To access this API, use the `onMount` callback, or any of the component's callback props, like `onPersist`.
|
||||
You can change the `tldraw` component's state through an imperative API called `TldrawApp`. To access this API, use the `onMount` callback, or any of the component's callback props, like `onPersist`.
|
||||
|
||||
```tsx
|
||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
||||
import { Tldraw, TldrawApp } from '@tldraw/tldraw'
|
||||
|
||||
function App() {
|
||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
||||
const handleMount = React.useCallback((state: TldrawApp) => {
|
||||
state.selectAll()
|
||||
}, [])
|
||||
|
||||
return <TLDraw onMount={handleMount} />
|
||||
return <Tldraw onMount={handleMount} />
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
To view the full documentation of the `TldrawApp` 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
|
||||
/packages/tldraw/docs/classes/TldrawApp.html
|
||||
```
|
||||
|
||||
Here are some useful methods:
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"author": "@steveruizok",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tldraw/tldraw.git"
|
||||
"url": "https://github.com/tldraw/tldraw.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
|
|
|
@ -1,3 +1,29 @@
|
|||
## 1.0.0
|
||||
|
||||
Breaking
|
||||
|
||||
- Renames many props, components (e.g. `TLDrawState` to `TLDrawApp`)
|
||||
|
||||
Improvements
|
||||
|
||||
- Updates UI in toolbars, menus.
|
||||
- Simplifies state and context.
|
||||
- Adds and updtes tests.
|
||||
- Renames TLDraw to tldraw throughout the app and documentation.
|
||||
- Renames TLDrawState to TldrawApp, state to app.
|
||||
- Improves action menu
|
||||
- Improves dark colors
|
||||
- Consolidates style menu
|
||||
- Fixes performance bug with menus
|
||||
- Fixes text formatting in Text shapes
|
||||
|
||||
New
|
||||
|
||||
- Adds shape menu
|
||||
- Adds eraser tool (click or drag to erase)
|
||||
- Adds `darkMode` prop for controlling dark mode UI.
|
||||
- Double-click a tool icon to toggle "tool lock". This will prevent the app from returning to the select tool after creating a shape.
|
||||
|
||||
## 0.1.17
|
||||
|
||||
- Fixes "shifting" bug with drawing tool. Finally!
|
||||
|
@ -61,7 +87,7 @@
|
|||
|
||||
- Extracted into its own repository (`tldraw/core`) and open sourced! :clap:
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Updated with latest `@tldraw/core`, updated ShapeUtils API.
|
||||
|
||||
|
@ -71,31 +97,31 @@
|
|||
|
||||
- Major change to ShapeUtils API.
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Rewrite utils with new API.
|
||||
|
||||
## 0.0.126
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Swap behavior of command and alt keys in arrow shapes.
|
||||
|
||||
## 0.0.125
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Bug fixes.
|
||||
|
||||
## 0.0.124
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Fix typings.
|
||||
|
||||
## 0.0.123
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Adds bound shape controls.
|
||||
- Drag a bound shape control to translate shapes in that direction.
|
||||
|
@ -104,7 +130,7 @@
|
|||
|
||||
## 0.0.122
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Adds snapping for transforming shapes.
|
||||
|
||||
|
@ -114,20 +140,20 @@
|
|||
|
||||
- Adds `snapLines`.
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Adds shape snapping while translating. Hold Command/Control to disable while dragging.
|
||||
|
||||
## 0.0.120
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Improves rectangle rendering.
|
||||
- Improves zoom to fit and zoom to selection.
|
||||
|
||||
## 0.0.119
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Fixes bug with bound arrows after undo.
|
||||
|
||||
|
@ -137,7 +163,7 @@
|
|||
|
||||
- Improves multiplayer features.
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Fixes bugs in text, arrows, stickies.
|
||||
- Adds start binding for new arrows.
|
||||
|
@ -151,7 +177,7 @@
|
|||
|
||||
- Improves rendering on Safari.
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Improves rendering on Safari.
|
||||
- Minor bug fixes around selection.
|
||||
|
@ -164,32 +190,32 @@
|
|||
- Adds [side cloning](https://github.com/tldraw/tldraw/pull/149).
|
||||
- Improves rendering.
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Adds sticky note [side cloning](https://github.com/tldraw/tldraw/pull/149).
|
||||
|
||||
## 0.0.114
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Improves fills for filled shapes.
|
||||
|
||||
## 0.0.113
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Improves grouping and ungrouping.
|
||||
|
||||
## 0.0.112
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Fixes centering on embedded TLDraw components.
|
||||
- Fixes centering on embedded tldraw components.
|
||||
- Removes expensive calls to window.
|
||||
|
||||
## 0.0.111
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Adjust stroke widths and sizes.
|
||||
- Fixes a bug on very small dashed shapes.
|
||||
|
@ -200,7 +226,7 @@
|
|||
|
||||
- Adds `user` and `users` props (optional) for multiplayer cursors. This feature is very lightly implemented.
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Adds multiplayer support.
|
||||
- Adds `showMenu` and `showPages` props.
|
||||
|
@ -208,7 +234,7 @@
|
|||
|
||||
## 0.0.109
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Bumps perfect-freehand
|
||||
- Fixes dots for small sized draw shapes
|
||||
|
@ -217,6 +243,6 @@
|
|||
|
||||
- Adds CHANGELOG. Only 108 releases late!
|
||||
|
||||
### TLDraw
|
||||
### tldraw
|
||||
|
||||
- Fixes a bug with bounding boxes on arrows.
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
# @tldraw/tldraw
|
||||
|
||||
This package contains the [TLDraw](https://tldraw.com) editor as a React component named `<TLDraw>`. You can use this package to embed the editor in any React application.
|
||||
This package contains the [tldraw](https://tldraw.com) editor as a React component named `<Tldraw>`. You can use this package to embed the editor in any React application.
|
||||
|
||||
🎨 Want to build your own TLDraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core).
|
||||
🎨 Want to build your own tldraw-ish app instead? Try [@tldraw/core](https://github.com/tldraw/core).
|
||||
|
||||
💕 Love this library? Consider [becoming a sponsor](https://github.com/sponsors/steveruizok?frequency=recurring&sponsor=steveruizok).
|
||||
|
||||
For documentation, see the [TLDraw](https://github.com/tldraw) repository.
|
||||
For documentation, see the [tldraw](https://github.com/tldraw) repository.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"author": "@steveruizok",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tldraw/tldraw.git"
|
||||
"url": "https://github.com/tldraw/tldraw.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
@ -59,4 +59,4 @@
|
|||
"tsconfig-replace-paths": "^0.0.5"
|
||||
},
|
||||
"gitHead": "083b36e167b6911927a6b58cbbb830b11b33f00a"
|
||||
}
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
import * as React from 'react'
|
||||
import { render, waitFor } from '@testing-library/react'
|
||||
import { TLDraw } from './TLDraw'
|
||||
import { Tldraw } from './Tldraw'
|
||||
|
||||
describe('tldraw', () => {
|
||||
describe('Tldraw', () => {
|
||||
test('mounts component and calls onMount', async () => {
|
||||
const onMount = jest.fn()
|
||||
render(<TLDraw onMount={onMount} />)
|
||||
render(<Tldraw onMount={onMount} />)
|
||||
await waitFor(onMount)
|
||||
})
|
||||
|
||||
test('mounts component and calls onMount when id is present', async () => {
|
||||
const onMount = jest.fn()
|
||||
render(<TLDraw id="someId" onMount={onMount} />)
|
||||
render(<Tldraw id="someId" onMount={onMount} />)
|
||||
await waitFor(onMount)
|
||||
})
|
||||
})
|
|
@ -2,15 +2,9 @@ import * as React from 'react'
|
|||
import { IdProvider } from '@radix-ui/react-id'
|
||||
import { Renderer } from '@tldraw/core'
|
||||
import { styled, dark } from '~styles'
|
||||
import { TLDrawSnapshot, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types'
|
||||
import { TLDrawCallbacks, TLDrawState } from '~state'
|
||||
import {
|
||||
TLDrawContext,
|
||||
TLDrawContextType,
|
||||
useStylesheet,
|
||||
useKeyboardShortcuts,
|
||||
useTLDrawContext,
|
||||
} from '~hooks'
|
||||
import { TDDocument, TDStatus, TDUser } from '~types'
|
||||
import { TldrawApp, TDCallbacks } from '~state'
|
||||
import { TldrawContext, useStylesheet, useKeyboardShortcuts, useTldrawApp } from '~hooks'
|
||||
import { shapeUtils } from '~state/shapes'
|
||||
import { ToolsPanel } from '~components/ToolsPanel'
|
||||
import { TopPanel } from '~components/TopPanel'
|
||||
|
@ -18,29 +12,7 @@ import { TLDR } from '~state/TLDR'
|
|||
import { ContextMenu } from '~components/ContextMenu'
|
||||
import { FocusButton } from '~components/FocusButton/FocusButton'
|
||||
|
||||
// Selectors
|
||||
const isInSelectSelector = (s: TLDrawSnapshot) => s.appState.activeTool === 'select'
|
||||
|
||||
const isHideBoundsShapeSelector = (s: TLDrawSnapshot) => {
|
||||
const { shapes } = s.document.pages[s.appState.currentPageId]
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return (
|
||||
selectedIds.length === 1 &&
|
||||
selectedIds.every((id) => TLDR.getShapeUtils(shapes[id].type).hideBounds)
|
||||
)
|
||||
}
|
||||
|
||||
const pageSelector = (s: TLDrawSnapshot) => s.document.pages[s.appState.currentPageId]
|
||||
|
||||
const snapLinesSelector = (s: TLDrawSnapshot) => s.appState.snapLines
|
||||
|
||||
const usersSelector = (s: TLDrawSnapshot) => s.room?.users
|
||||
|
||||
const pageStateSelector = (s: TLDrawSnapshot) => s.document.pageStates[s.appState.currentPageId]
|
||||
|
||||
const settingsSelector = (s: TLDrawSnapshot) => s.settings
|
||||
|
||||
export interface TLDrawProps extends TLDrawCallbacks {
|
||||
export interface TldrawProps extends TDCallbacks {
|
||||
/**
|
||||
* (optional) If provided, the component will load / persist state under this key.
|
||||
*/
|
||||
|
@ -49,7 +21,7 @@ export interface TLDrawProps extends TLDrawCallbacks {
|
|||
/**
|
||||
* (optional) The document to load or update from.
|
||||
*/
|
||||
document?: TLDrawDocument
|
||||
document?: TDDocument
|
||||
|
||||
/**
|
||||
* (optional) The current page id.
|
||||
|
@ -86,6 +58,11 @@ export interface TLDrawProps extends TLDrawCallbacks {
|
|||
*/
|
||||
showTools?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Whether to show a sponsor link for Tldraw.
|
||||
*/
|
||||
showSponsorLink?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Whether to show the UI.
|
||||
*/
|
||||
|
@ -96,69 +73,81 @@ export interface TLDrawProps extends TLDrawCallbacks {
|
|||
*/
|
||||
readOnly?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Whether to to show the app's dark mode UI.
|
||||
*/
|
||||
darkMode?: boolean
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the component mounts.
|
||||
*/
|
||||
onMount?: (state: TLDrawState) => void
|
||||
onMount?: (state: TldrawApp) => void
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the user creates a new project through the menu or through a keyboard shortcut.
|
||||
*/
|
||||
onNewProject?: (state: TLDrawState, e?: KeyboardEvent) => void
|
||||
onNewProject?: (state: TldrawApp, e?: KeyboardEvent) => void
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the user saves a project through the menu or through a keyboard shortcut.
|
||||
*/
|
||||
onSaveProject?: (state: TLDrawState, e?: KeyboardEvent) => void
|
||||
onSaveProject?: (state: TldrawApp, e?: KeyboardEvent) => void
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the user saves a project as a new project through the menu or through a keyboard shortcut.
|
||||
*/
|
||||
onSaveProjectAs?: (state: TLDrawState, e?: KeyboardEvent) => void
|
||||
onSaveProjectAs?: (state: TldrawApp, e?: KeyboardEvent) => void
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the user opens new project through the menu or through a keyboard shortcut.
|
||||
*/
|
||||
onOpenProject?: (state: TLDrawState, e?: KeyboardEvent) => void
|
||||
onOpenProject?: (state: TldrawApp, e?: KeyboardEvent) => void
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the user signs in via the menu.
|
||||
*/
|
||||
onSignIn?: (state: TLDrawState) => void
|
||||
onSignIn?: (state: TldrawApp) => void
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the user signs out via the menu.
|
||||
*/
|
||||
onSignOut?: (state: TLDrawState) => void
|
||||
onSignOut?: (state: TldrawApp) => void
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the user creates a new project.
|
||||
*/
|
||||
onUserChange?: (state: TLDrawState, user: TLDrawUser) => void
|
||||
onUserChange?: (state: TldrawApp, user: TDUser) => void
|
||||
/**
|
||||
* (optional) A callback to run when the component's state changes.
|
||||
*/
|
||||
onChange?: (state: TLDrawState, reason?: string) => void
|
||||
onChange?: (state: TldrawApp, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is patched.
|
||||
*/
|
||||
onPatch?: (state: TLDrawState, reason?: string) => void
|
||||
onPatch?: (state: TldrawApp, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is changed with a command.
|
||||
*/
|
||||
onCommand?: (state: TLDrawState, reason?: string) => void
|
||||
onCommand?: (state: TldrawApp, reason?: string) => void
|
||||
/**
|
||||
* (optional) A callback to run when the state is persisted.
|
||||
*/
|
||||
onPersist?: (state: TLDrawState) => void
|
||||
onPersist?: (state: TldrawApp) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user undos.
|
||||
*/
|
||||
onUndo?: (state: TLDrawState) => void
|
||||
onUndo?: (state: TldrawApp) => void
|
||||
/**
|
||||
* (optional) A callback to run when the user redos.
|
||||
*/
|
||||
onRedo?: (state: TLDrawState) => void
|
||||
onRedo?: (state: TldrawApp) => void
|
||||
}
|
||||
|
||||
export function TLDraw({
|
||||
export function Tldraw({
|
||||
id,
|
||||
document,
|
||||
currentPageId,
|
||||
darkMode = false,
|
||||
autofocus = true,
|
||||
showMenu = true,
|
||||
showPages = true,
|
||||
|
@ -167,6 +156,7 @@ export function TLDraw({
|
|||
showStyles = true,
|
||||
showUI = true,
|
||||
readOnly = false,
|
||||
showSponsorLink = false,
|
||||
onMount,
|
||||
onChange,
|
||||
onUserChange,
|
||||
|
@ -181,12 +171,13 @@ export function TLDraw({
|
|||
onPersist,
|
||||
onPatch,
|
||||
onCommand,
|
||||
}: TLDrawProps) {
|
||||
}: TldrawProps) {
|
||||
const [sId, setSId] = React.useState(id)
|
||||
|
||||
const [state, setState] = React.useState(
|
||||
// Create a new app when the component mounts.
|
||||
const [app, setApp] = React.useState(
|
||||
() =>
|
||||
new TLDrawState(id, {
|
||||
new TldrawApp(id, {
|
||||
onMount,
|
||||
onChange,
|
||||
onUserChange,
|
||||
|
@ -204,15 +195,11 @@ export function TLDraw({
|
|||
})
|
||||
)
|
||||
|
||||
const [context, setContext] = React.useState<TLDrawContextType>(() => ({
|
||||
state,
|
||||
useSelector: state.useStore,
|
||||
}))
|
||||
|
||||
// Create a new app if the `id` prop changes.
|
||||
React.useEffect(() => {
|
||||
if (id === sId) return
|
||||
|
||||
const newState = new TLDrawState(id, {
|
||||
const newApp = new TldrawApp(id, {
|
||||
onMount,
|
||||
onChange,
|
||||
onUserChange,
|
||||
|
@ -231,31 +218,42 @@ export function TLDraw({
|
|||
|
||||
setSId(id)
|
||||
|
||||
setContext((ctx) => ({
|
||||
...ctx,
|
||||
state: newState,
|
||||
useSelector: newState.useStore,
|
||||
}))
|
||||
|
||||
setState(newState)
|
||||
setApp(newApp)
|
||||
}, [sId, id])
|
||||
|
||||
React.useEffect(() => {
|
||||
state.readOnly = readOnly
|
||||
}, [state, readOnly])
|
||||
|
||||
// Update the document if the `document` prop changes but the ids,
|
||||
// are the same, or else load a new document if the ids are different.
|
||||
React.useEffect(() => {
|
||||
if (!document) return
|
||||
|
||||
if (document.id === state.document.id) {
|
||||
state.updateDocument(document)
|
||||
if (document.id === app.document.id) {
|
||||
app.updateDocument(document)
|
||||
} else {
|
||||
state.loadDocument(document)
|
||||
app.loadDocument(document)
|
||||
}
|
||||
}, [document, state])
|
||||
}, [document, app])
|
||||
|
||||
// Change the page when the `currentPageId` prop changes
|
||||
React.useEffect(() => {
|
||||
state.callbacks = {
|
||||
if (!currentPageId) return
|
||||
app.changePage(currentPageId)
|
||||
}, [currentPageId, app])
|
||||
|
||||
// Toggle the app's readOnly mode when the `readOnly` prop changes
|
||||
React.useEffect(() => {
|
||||
app.readOnly = readOnly
|
||||
}, [app, readOnly])
|
||||
|
||||
// Toggle the app's readOnly mode when the `readOnly` prop changes
|
||||
React.useEffect(() => {
|
||||
if (darkMode && !app.settings.isDarkMode) {
|
||||
// app.toggleDarkMode()
|
||||
}
|
||||
}, [app, darkMode])
|
||||
|
||||
// Update the app's callbacks when any callback changes.
|
||||
React.useEffect(() => {
|
||||
app.callbacks = {
|
||||
onMount,
|
||||
onChange,
|
||||
onUserChange,
|
||||
|
@ -272,7 +270,7 @@ export function TLDraw({
|
|||
onPersist,
|
||||
}
|
||||
}, [
|
||||
state,
|
||||
app,
|
||||
onMount,
|
||||
onChange,
|
||||
onUserChange,
|
||||
|
@ -291,12 +289,11 @@ export function TLDraw({
|
|||
|
||||
// Use the `key` to ensure that new selector hooks are made when the id changes
|
||||
return (
|
||||
<TLDrawContext.Provider value={context}>
|
||||
<TldrawContext.Provider value={app}>
|
||||
<IdProvider>
|
||||
<InnerTLDraw
|
||||
key={sId || 'tldraw'}
|
||||
<InnerTldraw
|
||||
key={sId || 'Tldraw'}
|
||||
id={sId}
|
||||
currentPageId={currentPageId}
|
||||
autofocus={autofocus}
|
||||
showPages={showPages}
|
||||
showMenu={showMenu}
|
||||
|
@ -304,16 +301,16 @@ export function TLDraw({
|
|||
showZoom={showZoom}
|
||||
showTools={showTools}
|
||||
showUI={showUI}
|
||||
showSponsorLink={showSponsorLink}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</IdProvider>
|
||||
</TLDrawContext.Provider>
|
||||
</TldrawContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
interface InnerTLDrawProps {
|
||||
interface InnerTldrawProps {
|
||||
id?: string
|
||||
currentPageId?: string
|
||||
autofocus: boolean
|
||||
showPages: boolean
|
||||
showMenu: boolean
|
||||
|
@ -321,44 +318,49 @@ interface InnerTLDrawProps {
|
|||
showStyles: boolean
|
||||
showUI: boolean
|
||||
showTools: boolean
|
||||
showSponsorLink: boolean
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
const InnerTLDraw = React.memo(function InnerTLDraw({
|
||||
const InnerTldraw = React.memo(function InnerTldraw({
|
||||
id,
|
||||
currentPageId,
|
||||
autofocus,
|
||||
showPages,
|
||||
showMenu,
|
||||
showZoom,
|
||||
showStyles,
|
||||
showTools,
|
||||
showSponsorLink,
|
||||
readOnly,
|
||||
showUI,
|
||||
}: InnerTLDrawProps) {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
}: InnerTldrawProps) {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const rWrapper = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const page = useSelector(pageSelector)
|
||||
const state = app.useStore()
|
||||
|
||||
const pageState = useSelector(pageStateSelector)
|
||||
const { document, settings, appState, room } = state
|
||||
|
||||
const snapLines = useSelector(snapLinesSelector)
|
||||
const isSelecting = state.appState.activeTool === 'select'
|
||||
|
||||
const users = useSelector(usersSelector)
|
||||
const page = document.pages[appState.currentPageId]
|
||||
const pageState = document.pageStates[page.id]
|
||||
const { selectedIds } = state.document.pageStates[page.id]
|
||||
|
||||
const settings = useSelector(settingsSelector)
|
||||
const isHideBoundsShape =
|
||||
pageState.selectedIds.length === 1 &&
|
||||
TLDR.getShapeUtil(page.shapes[selectedIds[0]].type).hideBounds
|
||||
|
||||
const isSelecting = useSelector(isInSelectSelector)
|
||||
const isHideResizeHandlesShape =
|
||||
selectedIds.length === 1 &&
|
||||
TLDR.getShapeUtil(page.shapes[selectedIds[0]].type).hideResizeHandles
|
||||
|
||||
const isHideBoundsShape = useSelector(isHideBoundsShapeSelector)
|
||||
|
||||
const isInSession = state.session !== undefined
|
||||
const isInSession = app.session !== undefined
|
||||
|
||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||
const hideBounds =
|
||||
(isInSession && state.session?.constructor.name !== 'BrushSession') ||
|
||||
(isInSession && app.session?.constructor.name !== 'BrushSession') ||
|
||||
!isSelecting ||
|
||||
isHideBoundsShape ||
|
||||
!!pageState.editingId
|
||||
|
@ -368,10 +370,12 @@ const InnerTLDraw = React.memo(function InnerTLDraw({
|
|||
|
||||
// Hide indicators when not using the select tool, or when in session
|
||||
const hideIndicators =
|
||||
(isInSession && state.appState.status !== TLDrawStatus.Brushing) || !isSelecting
|
||||
(isInSession && state.appState.status !== TDStatus.Brushing) || !isSelecting
|
||||
|
||||
// Custom rendering meta, with dark mode for shapes
|
||||
const meta = React.useMemo(() => ({ isDarkMode: settings.isDarkMode }), [settings.isDarkMode])
|
||||
const meta = React.useMemo(() => {
|
||||
return { isDarkMode: settings.isDarkMode }
|
||||
}, [settings.isDarkMode])
|
||||
|
||||
// Custom theme, based on darkmode
|
||||
const theme = React.useMemo(() => {
|
||||
|
@ -381,7 +385,7 @@ const InnerTLDraw = React.memo(function InnerTLDraw({
|
|||
brushStroke: 'rgba(180, 180, 180, .25)',
|
||||
selected: 'rgba(38, 150, 255, 1.000)',
|
||||
selectFill: 'rgba(38, 150, 255, 0.05)',
|
||||
background: '#343d45',
|
||||
background: '#212529',
|
||||
foreground: '#49555f',
|
||||
}
|
||||
}
|
||||
|
@ -389,11 +393,6 @@ const InnerTLDraw = React.memo(function InnerTLDraw({
|
|||
return {}
|
||||
}, [settings.isDarkMode])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentPageId) return
|
||||
state.changePage(currentPageId)
|
||||
}, [currentPageId, state])
|
||||
|
||||
// When the context menu is blurred, close the menu by sending pointer events
|
||||
// to the context menu's ref. This is a hack around the fact that certain shapes
|
||||
// stop event propagation, which causes the menu to stay open even when blurred.
|
||||
|
@ -415,72 +414,73 @@ const InnerTLDraw = React.memo(function InnerTLDraw({
|
|||
shapeUtils={shapeUtils}
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
snapLines={snapLines}
|
||||
users={users}
|
||||
userId={state.state.room?.userId}
|
||||
snapLines={appState.snapLines}
|
||||
users={room?.users}
|
||||
userId={room?.userId}
|
||||
theme={theme}
|
||||
meta={meta}
|
||||
hideBounds={hideBounds}
|
||||
hideHandles={hideHandles}
|
||||
hideResizeHandles={isHideResizeHandlesShape}
|
||||
hideIndicators={hideIndicators}
|
||||
hideBindingHandles={!settings.showBindingHandles}
|
||||
hideCloneHandles={!settings.showCloneHandles}
|
||||
hideRotateHandles={!settings.showRotateHandles}
|
||||
onPinchStart={state.onPinchStart}
|
||||
onPinchEnd={state.onPinchEnd}
|
||||
onPinch={state.onPinch}
|
||||
onPan={state.onPan}
|
||||
onZoom={state.onZoom}
|
||||
onPointerDown={state.onPointerDown}
|
||||
onPointerMove={state.onPointerMove}
|
||||
onPointerUp={state.onPointerUp}
|
||||
onPointCanvas={state.onPointCanvas}
|
||||
onDoubleClickCanvas={state.onDoubleClickCanvas}
|
||||
onRightPointCanvas={state.onRightPointCanvas}
|
||||
onDragCanvas={state.onDragCanvas}
|
||||
onReleaseCanvas={state.onReleaseCanvas}
|
||||
onPointShape={state.onPointShape}
|
||||
onDoubleClickShape={state.onDoubleClickShape}
|
||||
onRightPointShape={state.onRightPointShape}
|
||||
onDragShape={state.onDragShape}
|
||||
onHoverShape={state.onHoverShape}
|
||||
onUnhoverShape={state.onUnhoverShape}
|
||||
onReleaseShape={state.onReleaseShape}
|
||||
onPointBounds={state.onPointBounds}
|
||||
onDoubleClickBounds={state.onDoubleClickBounds}
|
||||
onRightPointBounds={state.onRightPointBounds}
|
||||
onDragBounds={state.onDragBounds}
|
||||
onHoverBounds={state.onHoverBounds}
|
||||
onUnhoverBounds={state.onUnhoverBounds}
|
||||
onReleaseBounds={state.onReleaseBounds}
|
||||
onPointBoundsHandle={state.onPointBoundsHandle}
|
||||
onDoubleClickBoundsHandle={state.onDoubleClickBoundsHandle}
|
||||
onRightPointBoundsHandle={state.onRightPointBoundsHandle}
|
||||
onDragBoundsHandle={state.onDragBoundsHandle}
|
||||
onHoverBoundsHandle={state.onHoverBoundsHandle}
|
||||
onUnhoverBoundsHandle={state.onUnhoverBoundsHandle}
|
||||
onReleaseBoundsHandle={state.onReleaseBoundsHandle}
|
||||
onPointHandle={state.onPointHandle}
|
||||
onDoubleClickHandle={state.onDoubleClickHandle}
|
||||
onRightPointHandle={state.onRightPointHandle}
|
||||
onDragHandle={state.onDragHandle}
|
||||
onHoverHandle={state.onHoverHandle}
|
||||
onUnhoverHandle={state.onUnhoverHandle}
|
||||
onReleaseHandle={state.onReleaseHandle}
|
||||
onError={state.onError}
|
||||
onRenderCountChange={state.onRenderCountChange}
|
||||
onShapeChange={state.onShapeChange}
|
||||
onShapeBlur={state.onShapeBlur}
|
||||
onShapeClone={state.onShapeClone}
|
||||
onBoundsChange={state.updateBounds}
|
||||
onKeyDown={state.onKeyDown}
|
||||
onKeyUp={state.onKeyUp}
|
||||
onPinchStart={app.onPinchStart}
|
||||
onPinchEnd={app.onPinchEnd}
|
||||
onPinch={app.onPinch}
|
||||
onPan={app.onPan}
|
||||
onZoom={app.onZoom}
|
||||
onPointerDown={app.onPointerDown}
|
||||
onPointerMove={app.onPointerMove}
|
||||
onPointerUp={app.onPointerUp}
|
||||
onPointCanvas={app.onPointCanvas}
|
||||
onDoubleClickCanvas={app.onDoubleClickCanvas}
|
||||
onRightPointCanvas={app.onRightPointCanvas}
|
||||
onDragCanvas={app.onDragCanvas}
|
||||
onReleaseCanvas={app.onReleaseCanvas}
|
||||
onPointShape={app.onPointShape}
|
||||
onDoubleClickShape={app.onDoubleClickShape}
|
||||
onRightPointShape={app.onRightPointShape}
|
||||
onDragShape={app.onDragShape}
|
||||
onHoverShape={app.onHoverShape}
|
||||
onUnhoverShape={app.onUnhoverShape}
|
||||
onReleaseShape={app.onReleaseShape}
|
||||
onPointBounds={app.onPointBounds}
|
||||
onDoubleClickBounds={app.onDoubleClickBounds}
|
||||
onRightPointBounds={app.onRightPointBounds}
|
||||
onDragBounds={app.onDragBounds}
|
||||
onHoverBounds={app.onHoverBounds}
|
||||
onUnhoverBounds={app.onUnhoverBounds}
|
||||
onReleaseBounds={app.onReleaseBounds}
|
||||
onPointBoundsHandle={app.onPointBoundsHandle}
|
||||
onDoubleClickBoundsHandle={app.onDoubleClickBoundsHandle}
|
||||
onRightPointBoundsHandle={app.onRightPointBoundsHandle}
|
||||
onDragBoundsHandle={app.onDragBoundsHandle}
|
||||
onHoverBoundsHandle={app.onHoverBoundsHandle}
|
||||
onUnhoverBoundsHandle={app.onUnhoverBoundsHandle}
|
||||
onReleaseBoundsHandle={app.onReleaseBoundsHandle}
|
||||
onPointHandle={app.onPointHandle}
|
||||
onDoubleClickHandle={app.onDoubleClickHandle}
|
||||
onRightPointHandle={app.onRightPointHandle}
|
||||
onDragHandle={app.onDragHandle}
|
||||
onHoverHandle={app.onHoverHandle}
|
||||
onUnhoverHandle={app.onUnhoverHandle}
|
||||
onReleaseHandle={app.onReleaseHandle}
|
||||
onError={app.onError}
|
||||
onRenderCountChange={app.onRenderCountChange}
|
||||
onShapeChange={app.onShapeChange}
|
||||
onShapeBlur={app.onShapeBlur}
|
||||
onShapeClone={app.onShapeClone}
|
||||
onBoundsChange={app.updateBounds}
|
||||
onKeyDown={app.onKeyDown}
|
||||
onKeyUp={app.onKeyUp}
|
||||
/>
|
||||
</ContextMenu>
|
||||
{showUI && (
|
||||
<StyledUI>
|
||||
{settings.isFocusMode ? (
|
||||
<FocusButton onSelect={state.toggleFocusMode} />
|
||||
<FocusButton onSelect={app.toggleFocusMode} />
|
||||
) : (
|
||||
<>
|
||||
<TopPanel
|
||||
|
@ -489,6 +489,7 @@ const InnerTLDraw = React.memo(function InnerTLDraw({
|
|||
showMenu={showMenu}
|
||||
showStyles={showStyles}
|
||||
showZoom={showZoom}
|
||||
showSponsorLink={showSponsorLink}
|
||||
/>
|
||||
<StyledSpacer />
|
||||
{showTools && !readOnly && <ToolsPanel />}
|
||||
|
@ -539,6 +540,13 @@ const StyledLayout = styled('div', {
|
|||
width: '100%',
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
'& input, textarea, button, select, label, button': {
|
||||
webkitTouchCallout: 'none',
|
||||
webkitUserSelect: 'none',
|
||||
'-webkit-tap-highlight-color': 'transparent',
|
||||
'tap-highlight-color': 'transparent',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledUI = styled('div', {
|
|
@ -2,9 +2,9 @@ import * as React from 'react'
|
|||
import { ContextMenuItem } from '@radix-ui/react-context-menu'
|
||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||
|
||||
export const CMRowButton = ({ onSelect, ...rest }: RowButtonProps) => {
|
||||
export const CMRowButton = ({ ...rest }: RowButtonProps) => {
|
||||
return (
|
||||
<ContextMenuItem asChild onSelect={onSelect}>
|
||||
<ContextMenuItem asChild>
|
||||
<RowButton {...rest} />
|
||||
</ContextMenuItem>
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from 'react'
|
||||
import { styled } from '~styles'
|
||||
import * as RadixContextMenu from '@radix-ui/react-context-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { TLDrawSnapshot, AlignType, DistributeType, StretchType } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { TDSnapshot, AlignType, DistributeType, StretchType } from '~types'
|
||||
import {
|
||||
AlignBottomIcon,
|
||||
AlignCenterHorizontallyIcon,
|
||||
|
@ -21,21 +21,21 @@ import { CMTriggerButton } from './CMTriggerButton'
|
|||
import { Divider } from '~components/Divider'
|
||||
import { MenuContent } from '~components/MenuContent'
|
||||
|
||||
const has1SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
||||
const has1SelectedIdsSelector = (s: TDSnapshot) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
|
||||
}
|
||||
const has2SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
||||
const has2SelectedIdsSelector = (s: TDSnapshot) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1
|
||||
}
|
||||
const has3SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
||||
const has3SelectedIdsSelector = (s: TDSnapshot) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
|
||||
}
|
||||
|
||||
const isDebugModeSelector = (s: TLDrawSnapshot) => {
|
||||
const isDebugModeSelector = (s: TDSnapshot) => {
|
||||
return s.settings.isDebugMode
|
||||
}
|
||||
|
||||
const hasGroupSelectedSelector = (s: TLDrawSnapshot) => {
|
||||
const hasGroupSelectedSelector = (s: TDSnapshot) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
|
||||
(id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined
|
||||
)
|
||||
|
@ -49,77 +49,81 @@ interface ContextMenuProps {
|
|||
}
|
||||
|
||||
export const ContextMenu = ({ onBlur, children }: ContextMenuProps): JSX.Element => {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const hasSelection = useSelector(has1SelectedIdsSelector)
|
||||
const hasTwoOrMore = useSelector(has2SelectedIdsSelector)
|
||||
const hasThreeOrMore = useSelector(has3SelectedIdsSelector)
|
||||
const isDebugMode = useSelector(isDebugModeSelector)
|
||||
const hasGroupSelected = useSelector(hasGroupSelectedSelector)
|
||||
const app = useTldrawApp()
|
||||
const hasSelection = app.useStore(has1SelectedIdsSelector)
|
||||
const hasTwoOrMore = app.useStore(has2SelectedIdsSelector)
|
||||
const hasThreeOrMore = app.useStore(has3SelectedIdsSelector)
|
||||
const isDebugMode = app.useStore(isDebugModeSelector)
|
||||
const hasGroupSelected = app.useStore(hasGroupSelectedSelector)
|
||||
|
||||
const rContent = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleFlipHorizontal = React.useCallback(() => {
|
||||
state.flipHorizontal()
|
||||
}, [state])
|
||||
app.flipHorizontal()
|
||||
}, [app])
|
||||
|
||||
const handleFlipVertical = React.useCallback(() => {
|
||||
state.flipVertical()
|
||||
}, [state])
|
||||
app.flipVertical()
|
||||
}, [app])
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
state.duplicate()
|
||||
}, [state])
|
||||
app.duplicate()
|
||||
}, [app])
|
||||
|
||||
const handleLock = React.useCallback(() => {
|
||||
app.toggleLocked()
|
||||
}, [app])
|
||||
|
||||
const handleGroup = React.useCallback(() => {
|
||||
state.group()
|
||||
}, [state])
|
||||
app.group()
|
||||
}, [app])
|
||||
|
||||
const handleMoveToBack = React.useCallback(() => {
|
||||
state.moveToBack()
|
||||
}, [state])
|
||||
app.moveToBack()
|
||||
}, [app])
|
||||
|
||||
const handleMoveBackward = React.useCallback(() => {
|
||||
state.moveBackward()
|
||||
}, [state])
|
||||
app.moveBackward()
|
||||
}, [app])
|
||||
|
||||
const handleMoveForward = React.useCallback(() => {
|
||||
state.moveForward()
|
||||
}, [state])
|
||||
app.moveForward()
|
||||
}, [app])
|
||||
|
||||
const handleMoveToFront = React.useCallback(() => {
|
||||
state.moveToFront()
|
||||
}, [state])
|
||||
app.moveToFront()
|
||||
}, [app])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
state.delete()
|
||||
}, [state])
|
||||
app.delete()
|
||||
}, [app])
|
||||
|
||||
const handleCopyJson = React.useCallback(() => {
|
||||
state.copyJson()
|
||||
}, [state])
|
||||
app.copyJson()
|
||||
}, [app])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
state.copy()
|
||||
}, [state])
|
||||
app.copy()
|
||||
}, [app])
|
||||
|
||||
const handlePaste = React.useCallback(() => {
|
||||
state.paste()
|
||||
}, [state])
|
||||
app.paste()
|
||||
}, [app])
|
||||
|
||||
const handleCopySvg = React.useCallback(() => {
|
||||
state.copySvg()
|
||||
}, [state])
|
||||
app.copySvg()
|
||||
}, [app])
|
||||
|
||||
const handleUndo = React.useCallback(() => {
|
||||
state.undo()
|
||||
}, [state])
|
||||
app.undo()
|
||||
}, [app])
|
||||
|
||||
const handleRedo = React.useCallback(() => {
|
||||
state.redo()
|
||||
}, [state])
|
||||
app.redo()
|
||||
}, [app])
|
||||
|
||||
return (
|
||||
<RadixContextMenu.Root>
|
||||
<RadixContextMenu.Root dir="ltr">
|
||||
<RadixContextMenu.Trigger dir="ltr">{children}</RadixContextMenu.Trigger>
|
||||
<RadixContextMenu.Content
|
||||
dir="ltr"
|
||||
|
@ -132,38 +136,41 @@ export const ContextMenu = ({ onBlur, children }: ContextMenuProps): JSX.Element
|
|||
<MenuContent>
|
||||
{hasSelection ? (
|
||||
<>
|
||||
<CMRowButton onSelect={handleFlipHorizontal} kbd="⇧H">
|
||||
<CMRowButton onClick={handleDuplicate} kbd="#D">
|
||||
Duplicate
|
||||
</CMRowButton>
|
||||
<CMRowButton onClick={handleFlipHorizontal} kbd="⇧H">
|
||||
Flip Horizontal
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleFlipVertical} kbd="⇧V">
|
||||
<CMRowButton onClick={handleFlipVertical} kbd="⇧V">
|
||||
Flip Vertical
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleDuplicate} kbd="#D">
|
||||
Duplicate
|
||||
<CMRowButton onClick={handleLock} kbd="#⇧L">
|
||||
Lock / Unlock
|
||||
</CMRowButton>
|
||||
{(hasTwoOrMore || hasGroupSelected) && <Divider />}
|
||||
{hasTwoOrMore && (
|
||||
<CMRowButton onSelect={handleGroup} kbd="#G">
|
||||
<CMRowButton onClick={handleGroup} kbd="#G">
|
||||
Group
|
||||
</CMRowButton>
|
||||
)}
|
||||
{hasGroupSelected && (
|
||||
<CMRowButton onSelect={handleGroup} kbd="#⇧G">
|
||||
<CMRowButton onClick={handleGroup} kbd="#⇧G">
|
||||
Ungroup
|
||||
</CMRowButton>
|
||||
)}
|
||||
<Divider />
|
||||
<ContextMenuSubMenu label="Move">
|
||||
<CMRowButton onSelect={handleMoveToFront} kbd="⇧]">
|
||||
<CMRowButton onClick={handleMoveToFront} kbd="⇧]">
|
||||
To Front
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleMoveForward} kbd="]">
|
||||
<CMRowButton onClick={handleMoveForward} kbd="]">
|
||||
Forward
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleMoveBackward} kbd="[">
|
||||
<CMRowButton onClick={handleMoveBackward} kbd="[">
|
||||
Backward
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleMoveToBack} kbd="⇧[">
|
||||
<CMRowButton onClick={handleMoveToBack} kbd="⇧[">
|
||||
To Back
|
||||
</CMRowButton>
|
||||
</ContextMenuSubMenu>
|
||||
|
@ -175,30 +182,30 @@ export const ContextMenu = ({ onBlur, children }: ContextMenuProps): JSX.Element
|
|||
/>
|
||||
)}
|
||||
<Divider />
|
||||
<CMRowButton onSelect={handleCopy} kbd="#C">
|
||||
<CMRowButton onClick={handleCopy} kbd="#C">
|
||||
Copy
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleCopySvg} kbd="⇧#C">
|
||||
<CMRowButton onClick={handleCopySvg} kbd="⇧#C">
|
||||
Copy as SVG
|
||||
</CMRowButton>
|
||||
{isDebugMode && <CMRowButton onSelect={handleCopyJson}>Copy as JSON</CMRowButton>}
|
||||
<CMRowButton onSelect={handlePaste} kbd="#V">
|
||||
{isDebugMode && <CMRowButton onClick={handleCopyJson}>Copy as JSON</CMRowButton>}
|
||||
<CMRowButton onClick={handlePaste} kbd="#V">
|
||||
Paste
|
||||
</CMRowButton>
|
||||
<Divider />
|
||||
<CMRowButton onSelect={handleDelete} kbd="⌫">
|
||||
<CMRowButton onClick={handleDelete} kbd="⌫">
|
||||
Delete
|
||||
</CMRowButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CMRowButton onSelect={handlePaste} kbd="#V">
|
||||
<CMRowButton onClick={handlePaste} kbd="#V">
|
||||
Paste
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleUndo} kbd="#Z">
|
||||
<CMRowButton onClick={handleUndo} kbd="#Z">
|
||||
Undo
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleRedo} kbd="#⇧Z">
|
||||
<CMRowButton onClick={handleRedo} kbd="#⇧Z">
|
||||
Redo
|
||||
</CMRowButton>
|
||||
</>
|
||||
|
@ -215,84 +222,84 @@ function AlignDistributeSubMenu({
|
|||
hasTwoOrMore: boolean
|
||||
hasThreeOrMore: boolean
|
||||
}) {
|
||||
const { state } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const alignTop = React.useCallback(() => {
|
||||
state.align(AlignType.Top)
|
||||
}, [state])
|
||||
app.align(AlignType.Top)
|
||||
}, [app])
|
||||
|
||||
const alignCenterVertical = React.useCallback(() => {
|
||||
state.align(AlignType.CenterVertical)
|
||||
}, [state])
|
||||
app.align(AlignType.CenterVertical)
|
||||
}, [app])
|
||||
|
||||
const alignBottom = React.useCallback(() => {
|
||||
state.align(AlignType.Bottom)
|
||||
}, [state])
|
||||
app.align(AlignType.Bottom)
|
||||
}, [app])
|
||||
|
||||
const stretchVertically = React.useCallback(() => {
|
||||
state.stretch(StretchType.Vertical)
|
||||
}, [state])
|
||||
app.stretch(StretchType.Vertical)
|
||||
}, [app])
|
||||
|
||||
const distributeVertically = React.useCallback(() => {
|
||||
state.distribute(DistributeType.Vertical)
|
||||
}, [state])
|
||||
app.distribute(DistributeType.Vertical)
|
||||
}, [app])
|
||||
|
||||
const alignLeft = React.useCallback(() => {
|
||||
state.align(AlignType.Left)
|
||||
}, [state])
|
||||
app.align(AlignType.Left)
|
||||
}, [app])
|
||||
|
||||
const alignCenterHorizontal = React.useCallback(() => {
|
||||
state.align(AlignType.CenterHorizontal)
|
||||
}, [state])
|
||||
app.align(AlignType.CenterHorizontal)
|
||||
}, [app])
|
||||
|
||||
const alignRight = React.useCallback(() => {
|
||||
state.align(AlignType.Right)
|
||||
}, [state])
|
||||
app.align(AlignType.Right)
|
||||
}, [app])
|
||||
|
||||
const stretchHorizontally = React.useCallback(() => {
|
||||
state.stretch(StretchType.Horizontal)
|
||||
}, [state])
|
||||
app.stretch(StretchType.Horizontal)
|
||||
}, [app])
|
||||
|
||||
const distributeHorizontally = React.useCallback(() => {
|
||||
state.distribute(DistributeType.Horizontal)
|
||||
}, [state])
|
||||
app.distribute(DistributeType.Horizontal)
|
||||
}, [app])
|
||||
|
||||
return (
|
||||
<RadixContextMenu.Root>
|
||||
<RadixContextMenu.Root dir="ltr">
|
||||
<CMTriggerButton isSubmenu>Align / Distribute</CMTriggerButton>
|
||||
<RadixContextMenu.Content asChild sideOffset={2} alignOffset={-2}>
|
||||
<StyledGridContent selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}>
|
||||
<CMIconButton onSelect={alignLeft}>
|
||||
<CMIconButton onClick={alignLeft}>
|
||||
<AlignLeftIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={alignCenterHorizontal}>
|
||||
<CMIconButton onClick={alignCenterHorizontal}>
|
||||
<AlignCenterHorizontallyIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={alignRight}>
|
||||
<CMIconButton onClick={alignRight}>
|
||||
<AlignRightIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={stretchHorizontally}>
|
||||
<CMIconButton onClick={stretchHorizontally}>
|
||||
<StretchHorizontallyIcon />
|
||||
</CMIconButton>
|
||||
{hasThreeOrMore && (
|
||||
<CMIconButton onSelect={distributeHorizontally}>
|
||||
<CMIconButton onClick={distributeHorizontally}>
|
||||
<SpaceEvenlyHorizontallyIcon />
|
||||
</CMIconButton>
|
||||
)}
|
||||
<CMIconButton onSelect={alignTop}>
|
||||
<CMIconButton onClick={alignTop}>
|
||||
<AlignTopIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={alignCenterVertical}>
|
||||
<CMIconButton onClick={alignCenterVertical}>
|
||||
<AlignCenterVerticallyIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={alignBottom}>
|
||||
<CMIconButton onClick={alignBottom}>
|
||||
<AlignBottomIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={stretchVertically}>
|
||||
<CMIconButton onClick={stretchVertically}>
|
||||
<StretchVerticallyIcon />
|
||||
</CMIconButton>
|
||||
{hasThreeOrMore && (
|
||||
<CMIconButton onSelect={distributeVertically}>
|
||||
<CMIconButton onClick={distributeVertically}>
|
||||
<SpaceEvenlyVerticallyIcon />
|
||||
</CMIconButton>
|
||||
)}
|
||||
|
@ -319,13 +326,13 @@ const StyledGridContent = styled(MenuContent, {
|
|||
|
||||
/* ------------------ Move to Page ------------------ */
|
||||
|
||||
const currentPageIdSelector = (s: TLDrawSnapshot) => s.appState.currentPageId
|
||||
const documentPagesSelector = (s: TLDrawSnapshot) => s.document.pages
|
||||
const currentPageIdSelector = (s: TDSnapshot) => s.appState.currentPageId
|
||||
const documentPagesSelector = (s: TDSnapshot) => s.document.pages
|
||||
|
||||
function MoveToPageMenu(): JSX.Element | null {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const currentPageId = useSelector(currentPageIdSelector)
|
||||
const documentPages = useSelector(documentPagesSelector)
|
||||
const app = useTldrawApp()
|
||||
const currentPageId = app.useStore(currentPageIdSelector)
|
||||
const documentPages = app.useStore(documentPagesSelector)
|
||||
|
||||
const sorted = Object.values(documentPages)
|
||||
.sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||
|
@ -342,7 +349,7 @@ function MoveToPageMenu(): JSX.Element | null {
|
|||
<CMRowButton
|
||||
key={id}
|
||||
disabled={id === currentPageId}
|
||||
onSelect={() => state.moveToPage(id)}
|
||||
onClick={() => app.moveToPage(id)}
|
||||
>
|
||||
{name || `Page ${i}`}
|
||||
</CMRowButton>
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import * as React from 'react'
|
||||
import { CheckboxItem } from '@radix-ui/react-dropdown-menu'
|
||||
import { RowButton } from '~components/RowButton'
|
||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||
import { preventEvent } from '~components/preventEvent'
|
||||
|
||||
interface DMCheckboxItemProps {
|
||||
checked: boolean
|
||||
disabled?: boolean
|
||||
onCheckedChange: (isChecked: boolean) => void
|
||||
children: React.ReactNode
|
||||
variant?: RowButtonProps['variant']
|
||||
kbd?: string
|
||||
}
|
||||
|
||||
export function DMCheckboxItem({
|
||||
checked,
|
||||
disabled = false,
|
||||
variant,
|
||||
onCheckedChange,
|
||||
kbd,
|
||||
children,
|
||||
|
@ -20,12 +23,13 @@ export function DMCheckboxItem({
|
|||
return (
|
||||
<CheckboxItem
|
||||
dir="ltr"
|
||||
onSelect={preventEvent}
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
asChild
|
||||
>
|
||||
<RowButton kbd={kbd} hasIndicator>
|
||||
<RowButton kbd={kbd} variant={variant} hasIndicator>
|
||||
{children}
|
||||
</RowButton>
|
||||
</CheckboxItem>
|
||||
|
|
|
@ -2,18 +2,29 @@ import * as React from 'react'
|
|||
import { Content } from '@radix-ui/react-dropdown-menu'
|
||||
import { styled } from '~styles/stitches.config'
|
||||
import { MenuContent } from '~components/MenuContent'
|
||||
import { stopPropagation } from '~components/stopPropagation'
|
||||
|
||||
export interface DMContentProps {
|
||||
variant?: 'grid' | 'menu' | 'horizontal'
|
||||
variant?: 'menu' | 'horizontal'
|
||||
align?: 'start' | 'center' | 'end'
|
||||
sideOffset?: number
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const preventDefault = (e: Event) => e.stopPropagation()
|
||||
|
||||
export function DMContent({ children, align, variant }: DMContentProps): JSX.Element {
|
||||
export function DMContent({
|
||||
sideOffset = 8,
|
||||
children,
|
||||
align,
|
||||
variant,
|
||||
}: DMContentProps): JSX.Element {
|
||||
return (
|
||||
<Content sideOffset={8} dir="ltr" asChild align={align} onEscapeKeyDown={preventDefault}>
|
||||
<Content
|
||||
dir="ltr"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
onEscapeKeyDown={stopPropagation}
|
||||
asChild
|
||||
>
|
||||
<StyledContent variant={variant}>{children}</StyledContent>
|
||||
</Content>
|
||||
)
|
||||
|
@ -25,11 +36,6 @@ export const StyledContent = styled(MenuContent, {
|
|||
minWidth: 0,
|
||||
variants: {
|
||||
variant: {
|
||||
grid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, auto)',
|
||||
gap: 0,
|
||||
},
|
||||
horizontal: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
|
|
|
@ -2,7 +2,10 @@ import * as React from 'react'
|
|||
import { Item } from '@radix-ui/react-dropdown-menu'
|
||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||
|
||||
export function DMItem({ onSelect, ...rest }: RowButtonProps): JSX.Element {
|
||||
export function DMItem({
|
||||
onSelect,
|
||||
...rest
|
||||
}: RowButtonProps & { onSelect?: (event: Event) => void }): JSX.Element {
|
||||
return (
|
||||
<Item dir="ltr" asChild onSelect={onSelect}>
|
||||
<RowButton {...rest} />
|
||||
|
|
|
@ -16,11 +16,32 @@ export const DMRadioItem = styled(RadioItem, {
|
|||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
|
||||
'&:focus': {
|
||||
backgroundColor: '$hover',
|
||||
variants: {
|
||||
isActive: {
|
||||
true: {
|
||||
backgroundColor: '$selected',
|
||||
color: '$panel',
|
||||
},
|
||||
false: {},
|
||||
},
|
||||
bp: {
|
||||
mobile: {},
|
||||
small: {},
|
||||
},
|
||||
},
|
||||
|
||||
'&:hover:not(:disabled)': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
isActive: false,
|
||||
bp: 'small',
|
||||
css: {
|
||||
'&:focus': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
'&:hover:not(:disabled)': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import * as React from 'react'
|
||||
import { Trigger } from '@radix-ui/react-dropdown-menu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { ToolButton, ToolButtonProps } from '~components/ToolButton'
|
||||
|
||||
interface DMTriggerIconProps {
|
||||
interface DMTriggerIconProps extends ToolButtonProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DMTriggerIcon({ children }: DMTriggerIconProps) {
|
||||
export function DMTriggerIcon({ children, ...rest }: DMTriggerIconProps) {
|
||||
return (
|
||||
<Trigger asChild>
|
||||
<ToolButton>{children}</ToolButton>
|
||||
<ToolButton {...rest}>{children}</ToolButton>
|
||||
</Trigger>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ const StyledButtonContainer = styled('div', {
|
|||
backgroundColor: 'transparent',
|
||||
|
||||
'& svg': {
|
||||
color: '$muted',
|
||||
color: '$text',
|
||||
},
|
||||
|
||||
'&:hover svg': {
|
||||
|
|
|
@ -33,8 +33,9 @@ export const StyledKbd = styled('kbd', {
|
|||
textAlign: 'center',
|
||||
fontSize: '$0',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
color: '$text',
|
||||
background: 'none',
|
||||
fontWeight: 400,
|
||||
gap: '$1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
@ -51,7 +52,7 @@ export const StyledKbd = styled('kbd', {
|
|||
variant: {
|
||||
tooltip: {
|
||||
'& > span': {
|
||||
color: '$tooltipText',
|
||||
color: '$tooltipContrast',
|
||||
background: '$overlayContrast',
|
||||
boxShadow: '$key',
|
||||
width: '20px',
|
||||
|
|
|
@ -6,17 +6,28 @@ export const Panel = styled('div', {
|
|||
flexDirection: 'row',
|
||||
boxShadow: '$panel',
|
||||
padding: '$2',
|
||||
border: '1px solid $panelContrast',
|
||||
gap: 0,
|
||||
variants: {
|
||||
side: {
|
||||
center: {
|
||||
borderTopLeftRadius: '$4',
|
||||
borderTopRightRadius: '$4',
|
||||
borderRadius: '$4',
|
||||
},
|
||||
left: {
|
||||
padding: 0,
|
||||
borderTop: 0,
|
||||
borderLeft: 0,
|
||||
borderTopRightRadius: '$1',
|
||||
borderBottomRightRadius: '$3',
|
||||
borderBottomLeftRadius: '$1',
|
||||
},
|
||||
right: {
|
||||
padding: 0,
|
||||
borderTop: 0,
|
||||
borderRight: 0,
|
||||
borderTopLeftRadius: '$1',
|
||||
borderBottomLeftRadius: '$3',
|
||||
borderBottomRightRadius: '$1',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -7,10 +7,12 @@ import { SmallIcon } from '~components/SmallIcon'
|
|||
import { styled } from '~styles'
|
||||
|
||||
export interface RowButtonProps {
|
||||
onSelect?: () => void
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
kbd?: string
|
||||
variant?: 'wide'
|
||||
isSponsor?: boolean
|
||||
isActive?: boolean
|
||||
isWarning?: boolean
|
||||
hasIndicator?: boolean
|
||||
|
@ -20,12 +22,14 @@ export interface RowButtonProps {
|
|||
export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(
|
||||
(
|
||||
{
|
||||
onSelect,
|
||||
onClick,
|
||||
isActive = false,
|
||||
isWarning = false,
|
||||
hasIndicator = false,
|
||||
hasArrow = false,
|
||||
disabled = false,
|
||||
isSponsor = false,
|
||||
variant,
|
||||
kbd,
|
||||
children,
|
||||
...rest
|
||||
|
@ -38,8 +42,10 @@ export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(
|
|||
bp={breakpoints}
|
||||
isWarning={isWarning}
|
||||
isActive={isActive}
|
||||
isSponsor={isSponsor}
|
||||
disabled={disabled}
|
||||
onPointerDown={onSelect}
|
||||
onClick={onClick}
|
||||
variant={variant}
|
||||
{...rest}
|
||||
>
|
||||
<StyledRowButtonInner>
|
||||
|
@ -66,13 +72,10 @@ export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(
|
|||
const StyledRowButtonInner = styled('div', {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
color: '$text',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
fontSize: '$1',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '$2',
|
||||
display: 'flex',
|
||||
gap: '$1',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: '0 $3',
|
||||
|
@ -95,6 +98,10 @@ export const StyledRowButton = styled('button', {
|
|||
cursor: 'pointer',
|
||||
height: '32px',
|
||||
outline: 'none',
|
||||
color: '$text',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
fontSize: '$1',
|
||||
borderRadius: 4,
|
||||
userSelect: 'none',
|
||||
margin: 0,
|
||||
|
@ -112,17 +119,33 @@ export const StyledRowButton = styled('button', {
|
|||
backgroundColor: '$hover',
|
||||
},
|
||||
|
||||
'& a': {
|
||||
textDecoration: 'none',
|
||||
color: '$text',
|
||||
},
|
||||
|
||||
variants: {
|
||||
bp: {
|
||||
mobile: {},
|
||||
small: {},
|
||||
},
|
||||
variant: {
|
||||
wide: {
|
||||
gridColumn: '1 / span 4',
|
||||
},
|
||||
},
|
||||
size: {
|
||||
icon: {
|
||||
padding: '4px ',
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
isSponsor: {
|
||||
true: {
|
||||
color: '#eb30a2',
|
||||
},
|
||||
false: {},
|
||||
},
|
||||
isWarning: {
|
||||
true: {
|
||||
color: '$warn',
|
||||
|
@ -132,7 +155,29 @@ export const StyledRowButton = styled('button', {
|
|||
true: {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
false: {
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
isActive: false,
|
||||
isSponsor: true,
|
||||
bp: 'small',
|
||||
css: {
|
||||
[`&:hover:not(:disabled) ${StyledRowButtonInner}`]: {
|
||||
backgroundColor: '$sponsorContrast',
|
||||
border: '1px solid $panel',
|
||||
'& *[data-shy="true"]': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
isSponsor: false,
|
||||
bp: 'small',
|
||||
css: {
|
||||
[`&:hover:not(:disabled) ${StyledRowButtonInner}`]: {
|
||||
backgroundColor: '$hover',
|
||||
border: '1px solid $panel',
|
||||
|
@ -142,5 +187,5 @@ export const StyledRowButton = styled('button', {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -12,7 +12,7 @@ export const SmallIcon = styled('div', {
|
|||
border: 'none',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
color: '$text',
|
||||
color: 'currentColor',
|
||||
|
||||
'& svg': {
|
||||
height: 16,
|
||||
|
|
|
@ -1,32 +1,52 @@
|
|||
import * as React from 'react'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { styled } from '~styles'
|
||||
|
||||
export interface ToolButtonProps {
|
||||
onClick?: () => void
|
||||
onSelect?: () => void
|
||||
onDoubleClick?: () => void
|
||||
disabled?: boolean
|
||||
isActive?: boolean
|
||||
isSponsor?: boolean
|
||||
isToolLocked?: boolean
|
||||
variant?: 'icon' | 'text' | 'circle' | 'primary'
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
|
||||
({ onSelect, onClick, onDoubleClick, isActive = false, variant, children, ...rest }, ref) => {
|
||||
(
|
||||
{
|
||||
onSelect,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
variant,
|
||||
children,
|
||||
isToolLocked = false,
|
||||
disabled = false,
|
||||
isActive = false,
|
||||
isSponsor = false,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<StyledToolButton
|
||||
ref={ref}
|
||||
isActive={isActive}
|
||||
isSponsor={isSponsor}
|
||||
variant={variant}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
onPointerDown={onSelect}
|
||||
onDoubleClick={onDoubleClick}
|
||||
bp={breakpoints}
|
||||
{...rest}
|
||||
>
|
||||
<StyledToolButtonInner>{children}</StyledToolButtonInner>
|
||||
{isToolLocked && <ToolLockIndicator />}
|
||||
</StyledToolButton>
|
||||
)
|
||||
}
|
||||
|
@ -36,20 +56,30 @@ export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
|
|||
|
||||
interface ToolButtonWithTooltipProps extends ToolButtonProps {
|
||||
label: string
|
||||
isLocked?: boolean
|
||||
kbd?: string
|
||||
}
|
||||
|
||||
export function ToolButtonWithTooltip({ label, kbd, ...rest }: ToolButtonWithTooltipProps) {
|
||||
const { state } = useTLDrawContext()
|
||||
export function ToolButtonWithTooltip({
|
||||
label,
|
||||
kbd,
|
||||
isLocked,
|
||||
...rest
|
||||
}: ToolButtonWithTooltipProps) {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const handleDoubleClick = React.useCallback(() => {
|
||||
console.log('double clicking')
|
||||
state.toggleToolLock()
|
||||
app.toggleToolLock()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Tooltip label={label[0].toUpperCase() + label.slice(1)} kbd={kbd}>
|
||||
<ToolButton {...rest} variant="primary" onDoubleClick={handleDoubleClick} />
|
||||
<ToolButton
|
||||
{...rest}
|
||||
variant="primary"
|
||||
isToolLocked={isLocked && rest.isActive}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
@ -112,6 +142,7 @@ export const StyledToolButton = styled('button', {
|
|||
circle: {
|
||||
padding: '$2',
|
||||
[`& ${StyledToolButtonInner}`]: {
|
||||
border: '1px solid $panelContrast',
|
||||
borderRadius: '100%',
|
||||
boxShadow: '$panel',
|
||||
},
|
||||
|
@ -121,23 +152,17 @@ export const StyledToolButton = styled('button', {
|
|||
},
|
||||
},
|
||||
},
|
||||
isActive: {
|
||||
isSponsor: {
|
||||
true: {
|
||||
[`${StyledToolButtonInner}`]: {
|
||||
backgroundColor: '$selected',
|
||||
color: '$panelActive',
|
||||
},
|
||||
},
|
||||
false: {
|
||||
[`&:hover:not(:disabled) ${StyledToolButtonInner}`]: {
|
||||
backgroundColor: '$hover',
|
||||
border: '1px solid $panel',
|
||||
},
|
||||
[`&:focus:not(:disabled) ${StyledToolButtonInner}`]: {
|
||||
backgroundColor: '$hover',
|
||||
backgroundColor: '$sponsorContrast',
|
||||
},
|
||||
},
|
||||
},
|
||||
isActive: {
|
||||
true: {},
|
||||
false: {},
|
||||
},
|
||||
bp: {
|
||||
mobile: {},
|
||||
small: {},
|
||||
|
@ -168,5 +193,40 @@ export const StyledToolButton = styled('button', {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
isActive: true,
|
||||
isSponsor: false,
|
||||
css: {
|
||||
[`${StyledToolButtonInner}`]: {
|
||||
backgroundColor: '$selected',
|
||||
color: '$selectedContrast',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
isActive: false,
|
||||
isSponsor: false,
|
||||
bp: 'small',
|
||||
css: {
|
||||
[`&:hover:not(:disabled) ${StyledToolButtonInner}`]: {
|
||||
backgroundColor: '$hover',
|
||||
border: '1px solid $panel',
|
||||
},
|
||||
[`&:focus:not(:disabled) ${StyledToolButtonInner}`]: {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const ToolLockIndicator = styled('div', {
|
||||
position: 'absolute',
|
||||
width: 10,
|
||||
height: 10,
|
||||
backgroundColor: '$selected',
|
||||
borderRadius: '100%',
|
||||
bottom: -2,
|
||||
border: '2px solid $panel',
|
||||
zIndex: 100,
|
||||
})
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as React from 'react'
|
||||
import { Tooltip } from '~components/Tooltip/Tooltip'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { styled } from '~styles'
|
||||
import { AlignType, TLDrawSnapshot, DistributeType, StretchType } from '~types'
|
||||
import { AlignType, TDSnapshot, DistributeType, StretchType } from '~types'
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
|
@ -30,25 +30,24 @@ import {
|
|||
import { DMContent } from '~components/DropdownMenu'
|
||||
import { Divider } from '~components/Divider'
|
||||
import { TrashIcon } from '~components/icons'
|
||||
import { IconButton } from '~components/IconButton'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
const selectedShapesCountSelector = (s: TLDrawSnapshot) =>
|
||||
const selectedShapesCountSelector = (s: TDSnapshot) =>
|
||||
s.document.pageStates[s.appState.currentPageId].selectedIds.length
|
||||
|
||||
const isAllLockedSelector = (s: TLDrawSnapshot) => {
|
||||
const isAllLockedSelector = (s: TDSnapshot) => {
|
||||
const page = s.document.pages[s.appState.currentPageId]
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.every((id) => page.shapes[id].isLocked)
|
||||
}
|
||||
|
||||
const isAllAspectLockedSelector = (s: TLDrawSnapshot) => {
|
||||
const isAllAspectLockedSelector = (s: TDSnapshot) => {
|
||||
const page = s.document.pages[s.appState.currentPageId]
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
|
||||
}
|
||||
|
||||
const isAllGroupedSelector = (s: TLDrawSnapshot) => {
|
||||
const isAllGroupedSelector = (s: TDSnapshot) => {
|
||||
const page = s.document.pages[s.appState.currentPageId]
|
||||
const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map(
|
||||
(id) => page.shapes[id]
|
||||
|
@ -62,110 +61,110 @@ const isAllGroupedSelector = (s: TLDrawSnapshot) => {
|
|||
)
|
||||
}
|
||||
|
||||
const hasSelectionClickor = (s: TLDrawSnapshot) => {
|
||||
const hasSelectionClickor = (s: TDSnapshot) => {
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.length > 0
|
||||
}
|
||||
|
||||
const hasMultipleSelectionClickor = (s: TLDrawSnapshot) => {
|
||||
const hasMultipleSelectionClickor = (s: TDSnapshot) => {
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.length > 1
|
||||
}
|
||||
|
||||
export function ActionButton(): JSX.Element {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const isAllLocked = useSelector(isAllLockedSelector)
|
||||
const isAllLocked = app.useStore(isAllLockedSelector)
|
||||
|
||||
const isAllAspectLocked = useSelector(isAllAspectLockedSelector)
|
||||
const isAllAspectLocked = app.useStore(isAllAspectLockedSelector)
|
||||
|
||||
const isAllGrouped = useSelector(isAllGroupedSelector)
|
||||
const isAllGrouped = app.useStore(isAllGroupedSelector)
|
||||
|
||||
const hasSelection = useSelector(hasSelectionClickor)
|
||||
const hasSelection = app.useStore(hasSelectionClickor)
|
||||
|
||||
const hasMultipleSelection = useSelector(hasMultipleSelectionClickor)
|
||||
const hasMultipleSelection = app.useStore(hasMultipleSelectionClickor)
|
||||
|
||||
const handleRotate = React.useCallback(() => {
|
||||
state.rotate()
|
||||
}, [state])
|
||||
app.rotate()
|
||||
}, [app])
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
state.duplicate()
|
||||
}, [state])
|
||||
app.duplicate()
|
||||
}, [app])
|
||||
|
||||
const handleToggleLocked = React.useCallback(() => {
|
||||
state.toggleLocked()
|
||||
}, [state])
|
||||
app.toggleLocked()
|
||||
}, [app])
|
||||
|
||||
const handleToggleAspectRatio = React.useCallback(() => {
|
||||
state.toggleAspectRatioLocked()
|
||||
}, [state])
|
||||
app.toggleAspectRatioLocked()
|
||||
}, [app])
|
||||
|
||||
const handleGroup = React.useCallback(() => {
|
||||
state.group()
|
||||
}, [state])
|
||||
app.group()
|
||||
}, [app])
|
||||
|
||||
const handleMoveToBack = React.useCallback(() => {
|
||||
state.moveToBack()
|
||||
}, [state])
|
||||
app.moveToBack()
|
||||
}, [app])
|
||||
|
||||
const handleMoveBackward = React.useCallback(() => {
|
||||
state.moveBackward()
|
||||
}, [state])
|
||||
app.moveBackward()
|
||||
}, [app])
|
||||
|
||||
const handleMoveForward = React.useCallback(() => {
|
||||
state.moveForward()
|
||||
}, [state])
|
||||
app.moveForward()
|
||||
}, [app])
|
||||
|
||||
const handleMoveToFront = React.useCallback(() => {
|
||||
state.moveToFront()
|
||||
}, [state])
|
||||
app.moveToFront()
|
||||
}, [app])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
state.delete()
|
||||
}, [state])
|
||||
app.delete()
|
||||
}, [app])
|
||||
|
||||
const alignTop = React.useCallback(() => {
|
||||
state.align(AlignType.Top)
|
||||
}, [state])
|
||||
app.align(AlignType.Top)
|
||||
}, [app])
|
||||
|
||||
const alignCenterVertical = React.useCallback(() => {
|
||||
state.align(AlignType.CenterVertical)
|
||||
}, [state])
|
||||
app.align(AlignType.CenterVertical)
|
||||
}, [app])
|
||||
|
||||
const alignBottom = React.useCallback(() => {
|
||||
state.align(AlignType.Bottom)
|
||||
}, [state])
|
||||
app.align(AlignType.Bottom)
|
||||
}, [app])
|
||||
|
||||
const stretchVertically = React.useCallback(() => {
|
||||
state.stretch(StretchType.Vertical)
|
||||
}, [state])
|
||||
app.stretch(StretchType.Vertical)
|
||||
}, [app])
|
||||
|
||||
const distributeVertically = React.useCallback(() => {
|
||||
state.distribute(DistributeType.Vertical)
|
||||
}, [state])
|
||||
app.distribute(DistributeType.Vertical)
|
||||
}, [app])
|
||||
|
||||
const alignLeft = React.useCallback(() => {
|
||||
state.align(AlignType.Left)
|
||||
}, [state])
|
||||
app.align(AlignType.Left)
|
||||
}, [app])
|
||||
|
||||
const alignCenterHorizontal = React.useCallback(() => {
|
||||
state.align(AlignType.CenterHorizontal)
|
||||
}, [state])
|
||||
app.align(AlignType.CenterHorizontal)
|
||||
}, [app])
|
||||
|
||||
const alignRight = React.useCallback(() => {
|
||||
state.align(AlignType.Right)
|
||||
}, [state])
|
||||
app.align(AlignType.Right)
|
||||
}, [app])
|
||||
|
||||
const stretchHorizontally = React.useCallback(() => {
|
||||
state.stretch(StretchType.Horizontal)
|
||||
}, [state])
|
||||
app.stretch(StretchType.Horizontal)
|
||||
}, [app])
|
||||
|
||||
const distributeHorizontally = React.useCallback(() => {
|
||||
state.distribute(DistributeType.Horizontal)
|
||||
}, [state])
|
||||
app.distribute(DistributeType.Horizontal)
|
||||
}, [app])
|
||||
|
||||
const selectedShapesCount = useSelector(selectedShapesCountSelector)
|
||||
const selectedShapesCount = app.useStore(selectedShapesCountSelector)
|
||||
|
||||
const hasTwoOrMore = selectedShapesCount > 1
|
||||
const hasThreeOrMore = selectedShapesCount > 2
|
||||
|
@ -177,99 +176,99 @@ export function ActionButton(): JSX.Element {
|
|||
<DotsHorizontalIcon />
|
||||
</ToolButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DMContent>
|
||||
<DMContent sideOffset={16}>
|
||||
<>
|
||||
<ButtonsRow>
|
||||
<IconButton disabled={!hasSelection} onClick={handleDuplicate}>
|
||||
<ToolButton variant="icon" disabled={!hasSelection} onClick={handleDuplicate}>
|
||||
<Tooltip label="Duplicate" kbd={`#D`}>
|
||||
<CopyIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onClick={handleRotate}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasSelection} onClick={handleRotate}>
|
||||
<Tooltip label="Rotate">
|
||||
<RotateCounterClockwiseIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onClick={handleToggleLocked}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasSelection} onClick={handleToggleLocked}>
|
||||
<Tooltip label="Toogle Locked" kbd={`#L`}>
|
||||
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon opacity={0.4} />}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onClick={handleToggleAspectRatio}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasSelection} onClick={handleToggleAspectRatio}>
|
||||
<Tooltip label="Toogle Aspect Ratio Lock">
|
||||
<AspectRatioIcon opacity={isAllAspectLocked ? 1 : 0.4} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
</ToolButton>
|
||||
<ToolButton
|
||||
disabled={!hasSelection || (!isAllGrouped && !hasMultipleSelection)}
|
||||
onClick={handleGroup}
|
||||
>
|
||||
<Tooltip label="Group" kbd={`#G`}>
|
||||
<GroupIcon opacity={isAllGrouped ? 1 : 0.4} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ToolButton>
|
||||
</ButtonsRow>
|
||||
<ButtonsRow>
|
||||
<IconButton disabled={!hasSelection} onClick={handleMoveToBack}>
|
||||
<ToolButton disabled={!hasSelection} onClick={handleMoveToBack}>
|
||||
<Tooltip label="Move to Back" kbd={`#⇧[`}>
|
||||
<PinBottomIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onClick={handleMoveBackward}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasSelection} onClick={handleMoveBackward}>
|
||||
<Tooltip label="Move Backward" kbd={`#[`}>
|
||||
<ArrowDownIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onClick={handleMoveForward}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasSelection} onClick={handleMoveForward}>
|
||||
<Tooltip label="Move Forward" kbd={`#]`}>
|
||||
<ArrowUpIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onClick={handleMoveToFront}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasSelection} onClick={handleMoveToFront}>
|
||||
<Tooltip label="More to Front" kbd={`#⇧]`}>
|
||||
<PinTopIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onClick={handleDelete}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasSelection} onClick={handleDelete}>
|
||||
<Tooltip label="Delete" kbd="⌫">
|
||||
<TrashIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ToolButton>
|
||||
</ButtonsRow>
|
||||
<Divider />
|
||||
<ButtonsRow>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignLeft}>
|
||||
<ToolButton disabled={!hasTwoOrMore} onClick={alignLeft}>
|
||||
<AlignLeftIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
|
||||
<AlignCenterHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignRight}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasTwoOrMore} onClick={alignRight}>
|
||||
<AlignRightIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
|
||||
<StretchHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
|
||||
<SpaceEvenlyHorizontallyIcon />
|
||||
</IconButton>
|
||||
</ToolButton>
|
||||
</ButtonsRow>
|
||||
<ButtonsRow>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignTop}>
|
||||
<ToolButton disabled={!hasTwoOrMore} onClick={alignTop}>
|
||||
<AlignTopIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}>
|
||||
<AlignCenterVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={alignBottom}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasTwoOrMore} onClick={alignBottom}>
|
||||
<AlignBottomIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onClick={stretchVertically}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasTwoOrMore} onClick={stretchVertically}>
|
||||
<StretchVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasThreeOrMore} onClick={distributeVertically}>
|
||||
</ToolButton>
|
||||
<ToolButton disabled={!hasThreeOrMore} onClick={distributeVertically}>
|
||||
<SpaceEvenlyVerticallyIcon />
|
||||
</IconButton>
|
||||
</ToolButton>
|
||||
</ButtonsRow>
|
||||
</>
|
||||
</DMContent>
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import * as React from 'react'
|
||||
import { styled } from '~styles'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { RowButton } from '~components/RowButton'
|
||||
import { MenuContent } from '~components/MenuContent'
|
||||
|
||||
const isEmptyCanvasSelector = (s: TLDrawSnapshot) =>
|
||||
const isEmptyCanvasSelector = (s: TDSnapshot) =>
|
||||
Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
|
||||
s.appState.isEmptyCanvas
|
||||
|
||||
export const BackToContent = React.memo(function BackToContent() {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const isEmptyCanvas = useSelector(isEmptyCanvasSelector)
|
||||
const isEmptyCanvas = app.useStore(isEmptyCanvasSelector)
|
||||
|
||||
if (!isEmptyCanvas) return null
|
||||
|
||||
return (
|
||||
<BackToContentContainer>
|
||||
<RowButton onSelect={state.zoomToContent}>Back to content</RowButton>
|
||||
<RowButton onClick={app.zoomToContent}>Back to content</RowButton>
|
||||
</BackToContentContainer>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import * as React from 'react'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { TrashIcon } from '~components/icons'
|
||||
|
||||
export function DeleteButton(): JSX.Element {
|
||||
const { state } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
state.delete()
|
||||
}, [state])
|
||||
app.delete()
|
||||
}, [app])
|
||||
|
||||
return (
|
||||
<Tooltip label="Delete" kbd="7">
|
||||
<Tooltip label="Delete" kbd="⌫">
|
||||
<ToolButton variant="circle" onSelect={handleDelete}>
|
||||
<TrashIcon />
|
||||
</ToolButton>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import * as React from 'react'
|
||||
import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import type { TDSnapshot } from '~types'
|
||||
|
||||
const isToolLockedSelector = (s: TLDrawSnapshot) => s.appState.isToolLocked
|
||||
const isToolLockedSelector = (s: TDSnapshot) => s.appState.isToolLocked
|
||||
|
||||
export function LockButton(): JSX.Element {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const isToolLocked = useSelector(isToolLockedSelector)
|
||||
const isToolLocked = app.useStore(isToolLockedSelector)
|
||||
|
||||
return (
|
||||
<Tooltip label="Lock Tool" kbd="7">
|
||||
<ToolButton variant="circle" isActive={isToolLocked} onSelect={state.toggleToolLock}>
|
||||
<ToolButton variant="circle" isActive={isToolLocked} onSelect={app.toggleToolLock}>
|
||||
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
||||
</ToolButton>
|
||||
</Tooltip>
|
||||
|
|
76
packages/tldraw/src/components/ToolsPanel/PenMenu.tsx
Normal file
76
packages/tldraw/src/components/ToolsPanel/PenMenu.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { Panel } from '~components/Panel'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { TDShapeType, TDToolType } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { Pencil1Icon } from '@radix-ui/react-icons'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
|
||||
interface ShapesMenuProps {
|
||||
activeTool: TDToolType
|
||||
}
|
||||
|
||||
type PenShape = TDShapeType.Draw
|
||||
const penShapes: PenShape[] = [TDShapeType.Draw]
|
||||
const penShapeIcons = {
|
||||
[TDShapeType.Draw]: <Pencil1Icon />,
|
||||
}
|
||||
|
||||
export const PenMenu = React.memo(function PenMenu({ activeTool }: ShapesMenuProps) {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = React.useState<PenShape>(TDShapeType.Draw)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (penShapes.includes(activeTool as PenShape) && lastActiveTool !== activeTool) {
|
||||
setLastActiveTool(activeTool as PenShape)
|
||||
}
|
||||
}, [activeTool])
|
||||
|
||||
const selectShapeTool = React.useCallback(() => {
|
||||
app.selectTool(lastActiveTool)
|
||||
}, [activeTool, app])
|
||||
|
||||
const handleDoubleClick = React.useCallback(() => {
|
||||
app.toggleToolLock()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DropdownMenu.Trigger dir="ltr" asChild>
|
||||
<ToolButton
|
||||
variant="primary"
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onClick={selectShapeTool}
|
||||
isActive={penShapes.includes(activeTool as PenShape)}
|
||||
>
|
||||
{penShapeIcons[lastActiveTool]}
|
||||
</ToolButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content asChild dir="ltr" side="top" sideOffset={12}>
|
||||
<Panel side="center">
|
||||
{penShapes.map((shape, i) => (
|
||||
<Tooltip
|
||||
key={shape}
|
||||
label={shape[0].toUpperCase() + shape.slice(1)}
|
||||
kbd={(1 + i).toString()}
|
||||
>
|
||||
<DropdownMenu.Item asChild>
|
||||
<ToolButton
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
app.selectTool(shape)
|
||||
setLastActiveTool(shape)
|
||||
}}
|
||||
>
|
||||
{penShapeIcons[shape]}
|
||||
</ToolButton>
|
||||
</DropdownMenu.Item>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Panel>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
|
@ -1,52 +1,51 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
ArrowTopRightIcon,
|
||||
CircleIcon,
|
||||
CursorArrowIcon,
|
||||
Pencil1Icon,
|
||||
Pencil2Icon,
|
||||
SquareIcon,
|
||||
TextIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { TLDrawSnapshot, TLDrawShapeType } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { TDSnapshot, TDShapeType } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { ToolButtonWithTooltip } from '~components/ToolButton'
|
||||
import { Panel } from '~components/Panel'
|
||||
import { ShapesMenu } from './ShapesMenu'
|
||||
import { EraserIcon } from '~components/icons'
|
||||
|
||||
const activeToolSelector = (s: TLDrawSnapshot) => s.appState.activeTool
|
||||
const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool
|
||||
const toolLockedSelector = (s: TDSnapshot) => s.appState.isToolLocked
|
||||
|
||||
export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const activeTool = useSelector(activeToolSelector)
|
||||
const activeTool = app.useStore(activeToolSelector)
|
||||
|
||||
const isToolLocked = app.useStore(toolLockedSelector)
|
||||
|
||||
const selectSelectTool = React.useCallback(() => {
|
||||
state.selectTool('select')
|
||||
}, [state])
|
||||
app.selectTool('select')
|
||||
}, [app])
|
||||
|
||||
const selectEraseTool = React.useCallback(() => {
|
||||
app.selectTool('erase')
|
||||
}, [app])
|
||||
|
||||
const selectDrawTool = React.useCallback(() => {
|
||||
state.selectTool(TLDrawShapeType.Draw)
|
||||
}, [state])
|
||||
|
||||
const selectRectangleTool = React.useCallback(() => {
|
||||
state.selectTool(TLDrawShapeType.Rectangle)
|
||||
}, [state])
|
||||
|
||||
const selectEllipseTool = React.useCallback(() => {
|
||||
state.selectTool(TLDrawShapeType.Ellipse)
|
||||
}, [state])
|
||||
app.selectTool(TDShapeType.Draw)
|
||||
}, [app])
|
||||
|
||||
const selectArrowTool = React.useCallback(() => {
|
||||
state.selectTool(TLDrawShapeType.Arrow)
|
||||
}, [state])
|
||||
app.selectTool(TDShapeType.Arrow)
|
||||
}, [app])
|
||||
|
||||
const selectTextTool = React.useCallback(() => {
|
||||
state.selectTool(TLDrawShapeType.Text)
|
||||
}, [state])
|
||||
app.selectTool(TDShapeType.Text)
|
||||
}, [app])
|
||||
|
||||
const selectStickyTool = React.useCallback(() => {
|
||||
state.selectTool(TLDrawShapeType.Sticky)
|
||||
}, [state])
|
||||
app.selectTool(TDShapeType.Sticky)
|
||||
}, [app])
|
||||
|
||||
return (
|
||||
<Panel side="center">
|
||||
|
@ -60,49 +59,43 @@ export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
|
|||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'2'}
|
||||
label={TLDrawShapeType.Draw}
|
||||
label={TDShapeType.Draw}
|
||||
onClick={selectDrawTool}
|
||||
isActive={activeTool === TLDrawShapeType.Draw}
|
||||
isActive={activeTool === TDShapeType.Draw}
|
||||
>
|
||||
<Pencil1Icon />
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'3'}
|
||||
label={TLDrawShapeType.Rectangle}
|
||||
onClick={selectRectangleTool}
|
||||
isActive={activeTool === TLDrawShapeType.Rectangle}
|
||||
label={'eraser'}
|
||||
onClick={selectEraseTool}
|
||||
isActive={activeTool === 'erase'}
|
||||
>
|
||||
<SquareIcon />
|
||||
<EraserIcon />
|
||||
</ToolButtonWithTooltip>
|
||||
<ShapesMenu activeTool={activeTool} isToolLocked={isToolLocked} />
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'4'}
|
||||
label={TLDrawShapeType.Ellipse}
|
||||
onClick={selectEllipseTool}
|
||||
isActive={activeTool === TLDrawShapeType.Ellipse}
|
||||
>
|
||||
<CircleIcon />
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'5'}
|
||||
label={TLDrawShapeType.Arrow}
|
||||
kbd={'6'}
|
||||
label={TDShapeType.Arrow}
|
||||
onClick={selectArrowTool}
|
||||
isActive={activeTool === TLDrawShapeType.Arrow}
|
||||
isLocked={isToolLocked}
|
||||
isActive={activeTool === TDShapeType.Arrow}
|
||||
>
|
||||
<ArrowTopRightIcon />
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'6'}
|
||||
label={TLDrawShapeType.Text}
|
||||
kbd={'7'}
|
||||
label={TDShapeType.Text}
|
||||
onClick={selectTextTool}
|
||||
isActive={activeTool === TLDrawShapeType.Text}
|
||||
isActive={activeTool === TDShapeType.Text}
|
||||
>
|
||||
<TextIcon />
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'7'}
|
||||
label={TLDrawShapeType.Sticky}
|
||||
kbd={'8'}
|
||||
label={TDShapeType.Sticky}
|
||||
onClick={selectStickyTool}
|
||||
isActive={activeTool === TLDrawShapeType.Sticky}
|
||||
isActive={activeTool === TDShapeType.Sticky}
|
||||
>
|
||||
<Pencil2Icon />
|
||||
</ToolButtonWithTooltip>
|
||||
|
|
83
packages/tldraw/src/components/ToolsPanel/ShapesMenu.tsx
Normal file
83
packages/tldraw/src/components/ToolsPanel/ShapesMenu.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { Panel } from '~components/Panel'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { TDShapeType, TDToolType } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { SquareIcon, CircleIcon } from '@radix-ui/react-icons'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
|
||||
interface ShapesMenuProps {
|
||||
activeTool: TDToolType
|
||||
isToolLocked: boolean
|
||||
}
|
||||
|
||||
type ShapeShape = TDShapeType.Rectangle | TDShapeType.Ellipse
|
||||
const shapeShapes: ShapeShape[] = [TDShapeType.Rectangle, TDShapeType.Ellipse]
|
||||
const shapeShapeIcons = {
|
||||
[TDShapeType.Rectangle]: <SquareIcon />,
|
||||
[TDShapeType.Ellipse]: <CircleIcon />,
|
||||
}
|
||||
|
||||
export const ShapesMenu = React.memo(function ShapesMenu({
|
||||
activeTool,
|
||||
isToolLocked,
|
||||
}: ShapesMenuProps) {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = React.useState<ShapeShape>(TDShapeType.Rectangle)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shapeShapes.includes(activeTool as ShapeShape) && lastActiveTool !== activeTool) {
|
||||
setLastActiveTool(activeTool as ShapeShape)
|
||||
}
|
||||
}, [activeTool])
|
||||
|
||||
const selectShapeTool = React.useCallback(() => {
|
||||
app.selectTool(lastActiveTool)
|
||||
}, [activeTool, app])
|
||||
|
||||
const handleDoubleClick = React.useCallback(() => {
|
||||
app.toggleToolLock()
|
||||
}, [app])
|
||||
|
||||
const isActive = shapeShapes.includes(activeTool as ShapeShape)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr" onOpenChange={selectShapeTool}>
|
||||
<DropdownMenu.Trigger dir="ltr" asChild>
|
||||
<ToolButton
|
||||
variant="primary"
|
||||
onDoubleClick={handleDoubleClick}
|
||||
isToolLocked={isActive && isToolLocked}
|
||||
isActive={isActive}
|
||||
>
|
||||
{shapeShapeIcons[lastActiveTool]}
|
||||
</ToolButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content asChild dir="ltr" side="top" sideOffset={12}>
|
||||
<Panel side="center">
|
||||
{shapeShapes.map((shape, i) => (
|
||||
<Tooltip
|
||||
key={shape}
|
||||
label={shape[0].toUpperCase() + shape.slice(1)}
|
||||
kbd={(4 + i).toString()}
|
||||
>
|
||||
<DropdownMenu.Item asChild>
|
||||
<ToolButton
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
app.selectTool(shape)
|
||||
setLastActiveTool(shape)
|
||||
}}
|
||||
>
|
||||
{shapeShapeIcons[shape]}
|
||||
</ToolButton>
|
||||
</DropdownMenu.Item>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Panel>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
|
@ -1,16 +1,16 @@
|
|||
import * as React from 'react'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { styled } from '~styles'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
|
||||
const statusSelector = (s: TLDrawSnapshot) => s.appState.status
|
||||
const activeToolSelector = (s: TLDrawSnapshot) => s.appState.activeTool
|
||||
const statusSelector = (s: TDSnapshot) => s.appState.status
|
||||
const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool
|
||||
|
||||
export function StatusBar(): JSX.Element | null {
|
||||
const { useSelector } = useTLDrawContext()
|
||||
const status = useSelector(statusSelector)
|
||||
const activeTool = useSelector(activeToolSelector)
|
||||
const app = useTldrawApp()
|
||||
const status = app.useStore(statusSelector)
|
||||
const activeTool = app.useStore(activeToolSelector)
|
||||
|
||||
return (
|
||||
<StyledStatusBar bp={breakpoints}>
|
||||
|
@ -24,7 +24,7 @@ export function StatusBar(): JSX.Element | null {
|
|||
const StyledStatusBar = styled('div', {
|
||||
height: 40,
|
||||
userSelect: 'none',
|
||||
borderTop: '1px solid $border',
|
||||
borderTop: '1px solid $panelContrast',
|
||||
gridArea: 'status',
|
||||
display: 'flex',
|
||||
color: '$text',
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import * as React from 'react'
|
||||
import { styled } from '~styles'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { StatusBar } from './StatusBar'
|
||||
import { BackToContent } from './BackToContent'
|
||||
import { PrimaryTools } from './PrimaryTools'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { LockButton } from './LockButton'
|
||||
import { DeleteButton } from './DeleteButton'
|
||||
|
||||
const isDebugModeSelector = (s: TLDrawSnapshot) => s.settings.isDebugMode
|
||||
const isDebugModeSelector = (s: TDSnapshot) => s.settings.isDebugMode
|
||||
|
||||
export const ToolsPanel = React.memo(function ToolsPanel(): JSX.Element {
|
||||
const { useSelector } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const isDebugMode = useSelector(isDebugModeSelector)
|
||||
const isDebugMode = app.useStore(isDebugModeSelector)
|
||||
|
||||
return (
|
||||
<StyledToolsPanelContainer>
|
||||
|
@ -48,6 +47,7 @@ const StyledToolsPanelContainer = styled('div', {
|
|||
gridTemplateRows: 'auto auto',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0',
|
||||
gap: '$4',
|
||||
zIndex: 200,
|
||||
pointerEvents: 'none',
|
||||
'& > div > *': {
|
||||
|
|
|
@ -22,10 +22,10 @@ export function Tooltip({
|
|||
}: TooltipProps): JSX.Element {
|
||||
return (
|
||||
<RadixTooltip.Root>
|
||||
<RadixTooltip.Trigger asChild={true}>
|
||||
<RadixTooltip.Trigger dir="ltr" asChild={true}>
|
||||
<span>{children}</span>
|
||||
</RadixTooltip.Trigger>
|
||||
<StyledContent side={side} sideOffset={8}>
|
||||
<StyledContent dir="ltr" side={side} sideOffset={8}>
|
||||
{label}
|
||||
{kbdProp ? <Kbd variant="tooltip">{kbdProp}</Kbd> : null}
|
||||
<StyledArrow />
|
||||
|
@ -38,8 +38,8 @@ const StyledContent = styled(RadixTooltip.Content, {
|
|||
borderRadius: 3,
|
||||
padding: '$3 $3 $3 $3',
|
||||
fontSize: '$1',
|
||||
backgroundColor: '$tooltipBg',
|
||||
color: '$tooltipText',
|
||||
backgroundColor: '$tooltip',
|
||||
color: '$tooltipContrast',
|
||||
boxShadow: '$3',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
@ -48,6 +48,6 @@ const StyledContent = styled(RadixTooltip.Content, {
|
|||
})
|
||||
|
||||
const StyledArrow = styled(RadixTooltip.Arrow, {
|
||||
fill: '$tooltipBg',
|
||||
fill: '$tooltip',
|
||||
margin: '0 8px',
|
||||
})
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { strokes } from '~state/shapes/shape-styles'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { BoxIcon, CircleIcon } from '~components/icons'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import type { TLDrawSnapshot, ColorStyle } from '~types'
|
||||
|
||||
const selectColor = (s: TLDrawSnapshot) => s.appState.selectedStyle.color
|
||||
const preventEvent = (e: Event) => e.preventDefault()
|
||||
const themeSelector = (data: TLDrawSnapshot) => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||
|
||||
export const ColorMenu = React.memo(function ColorMenu(): JSX.Element {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const theme = useSelector(themeSelector)
|
||||
const color = useSelector(selectColor)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon>
|
||||
<CircleIcon size={16} fill={strokes[theme][color]} stroke={strokes[theme][color]} />
|
||||
</DMTriggerIcon>
|
||||
<DMContent variant="grid">
|
||||
{Object.keys(strokes[theme]).map((colorStyle: string) => (
|
||||
<DropdownMenu.Item key={colorStyle} onSelect={preventEvent} asChild>
|
||||
<ToolButton
|
||||
variant="icon"
|
||||
isActive={color === colorStyle}
|
||||
onClick={() => state.style({ color: colorStyle as ColorStyle })}
|
||||
>
|
||||
<BoxIcon
|
||||
fill={strokes[theme][colorStyle as ColorStyle]}
|
||||
stroke={strokes[theme][colorStyle as ColorStyle]}
|
||||
/>
|
||||
</ToolButton>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
|
@ -1,103 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { DashStyle, TLDrawSnapshot } from '~types'
|
||||
import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { DashDashedIcon, DashDottedIcon, DashDrawIcon, DashSolidIcon } from '~components/icons'
|
||||
|
||||
const dashes = {
|
||||
[DashStyle.Draw]: <DashDrawIcon />,
|
||||
[DashStyle.Solid]: <DashSolidIcon />,
|
||||
[DashStyle.Dashed]: <DashDashedIcon />,
|
||||
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||
}
|
||||
|
||||
const selectDash = (s: TLDrawSnapshot) => s.appState.selectedStyle.dash
|
||||
|
||||
const preventEvent = (e: Event) => e.preventDefault()
|
||||
|
||||
export const DashMenu = React.memo(function DashMenu(): JSX.Element {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const dash = useSelector(selectDash)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon>{dashes[dash]}</DMTriggerIcon>
|
||||
<DMContent variant="horizontal">
|
||||
{Object.values(DashStyle).map((dashStyle) => (
|
||||
<DropdownMenu.Item key={dashStyle} onSelect={preventEvent} asChild>
|
||||
<ToolButton
|
||||
variant="icon"
|
||||
isActive={dash === dashStyle}
|
||||
onClick={() => state.style({ dash: dashStyle as DashStyle })}
|
||||
>
|
||||
{dashes[dashStyle as DashStyle]}
|
||||
</ToolButton>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
||||
|
||||
// function DashSolidIcon(): JSX.Element {
|
||||
// return (
|
||||
// <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
// <circle cx={12} cy={12} r={8} fill="none" strokeWidth={2} strokeLinecap="round" />
|
||||
// </svg>
|
||||
// )
|
||||
// }
|
||||
|
||||
// function DashDashedIcon(): JSX.Element {
|
||||
// return (
|
||||
// <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
// <circle
|
||||
// cx={12}
|
||||
// cy={12}
|
||||
// r={8}
|
||||
// fill="none"
|
||||
// strokeWidth={2.5}
|
||||
// strokeLinecap="round"
|
||||
// strokeDasharray={50.26548 * 0.1}
|
||||
// />
|
||||
// </svg>
|
||||
// )
|
||||
// }
|
||||
|
||||
// const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
|
||||
|
||||
// function DashDottedIcon(): JSX.Element {
|
||||
// return (
|
||||
// <svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
// <circle
|
||||
// cx={12}
|
||||
// cy={12}
|
||||
// r={8}
|
||||
// fill="none"
|
||||
// strokeWidth={2.5}
|
||||
// strokeLinecap="round"
|
||||
// strokeDasharray={dottedDasharray}
|
||||
// />
|
||||
// </svg>
|
||||
// )
|
||||
// }
|
||||
|
||||
// function DashDrawIcon(): JSX.Element {
|
||||
// return (
|
||||
// <svg
|
||||
// width="24"
|
||||
// height="24"
|
||||
// viewBox="1 1.5 21 22"
|
||||
// fill="currentColor"
|
||||
// stroke="currentColor"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// >
|
||||
// <path
|
||||
// d="M10.0162 19.2768C10.0162 19.2768 9.90679 19.2517 9.6879 19.2017C9.46275 19.1454 9.12816 19.0422 8.68413 18.8921C8.23384 18.7358 7.81482 18.545 7.42707 18.3199C7.03307 18.101 6.62343 17.7883 6.19816 17.3818C5.77289 16.9753 5.33511 16.3718 4.88482 15.5713C4.43453 14.7645 4.1531 13.8545 4.04053 12.8414C3.92795 11.822 4.04991 10.8464 4.40639 9.91451C4.76286 8.98266 5.39452 8.10084 6.30135 7.26906C7.21444 6.44353 8.29325 5.83377 9.5378 5.43976C10.7823 5.05202 11.833 4.92068 12.6898 5.04576C13.5466 5.16459 14.3878 5.43664 15.2133 5.86191C16.0388 6.28718 16.7768 6.8688 17.4272 7.60678C18.0714 8.34475 18.5404 9.21406 18.8344 10.2147C19.1283 11.2153 19.1721 12.2598 18.9657 13.348C18.7593 14.4299 18.2872 15.4337 17.5492 16.3593C16.8112 17.2849 15.9263 18.0072 14.8944 18.5263C13.8624 19.0391 12.9056 19.3174 12.0238 19.3612C11.142 19.405 10.2101 19.2705 9.22823 18.9578C8.24635 18.6451 7.35828 18.151 6.56402 17.4756C5.77601 16.8002 6.08871 16.8658 7.50212 17.6726C8.90927 18.4731 10.1444 18.8484 11.2076 18.7983C12.2645 18.7545 13.2965 18.4825 14.3034 17.9822C15.3102 17.4819 16.1264 16.8221 16.7518 16.0028C17.3772 15.1835 17.7681 14.3111 17.9244 13.3855C18.0808 12.4599 18.0401 11.5781 17.8025 10.74C17.5586 9.902 17.1739 9.15464 16.6486 8.49797C16.1233 7.8413 15.2289 7.27844 13.9656 6.80939C12.7086 6.34034 11.4203 6.20901 10.1007 6.41539C8.78732 6.61552 7.69599 7.06893 6.82669 7.77564C5.96363 8.48859 5.34761 9.26409 4.97863 10.1021C4.60964 10.9402 4.45329 11.8376 4.50958 12.7945C4.56586 13.7513 4.79101 14.6238 5.18501 15.4118C5.57276 16.1998 5.96363 16.8002 6.35764 17.2129C6.75164 17.6257 7.13313 17.9509 7.50212 18.1886C7.87736 18.4325 8.28074 18.642 8.71227 18.8171C9.15005 18.9922 9.47839 19.111 9.69728 19.1736C9.91617 19.2361 10.0256 19.2705 10.0256 19.2768H10.0162Z"
|
||||
// strokeWidth="2"
|
||||
// />
|
||||
// </svg>
|
||||
// )
|
||||
// }
|
|
@ -1,30 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { BoxIcon, IsFilledIcon } from '~components/icons'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
const isFilledSelector = (s: TLDrawSnapshot) => s.appState.selectedStyle.isFilled
|
||||
|
||||
export const FillCheckbox = React.memo(function FillCheckbox(): JSX.Element {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const isFilled = useSelector(isFilledSelector)
|
||||
|
||||
const handleIsFilledChange = React.useCallback(
|
||||
(isFilled: boolean) => state.style({ isFilled }),
|
||||
[state]
|
||||
)
|
||||
|
||||
return (
|
||||
<Checkbox.Root dir="ltr" asChild checked={isFilled} onCheckedChange={handleIsFilledChange}>
|
||||
<ToolButton variant="icon">
|
||||
<BoxIcon />
|
||||
<Checkbox.Indicator>
|
||||
<IsFilledIcon />
|
||||
</Checkbox.Indicator>
|
||||
</ToolButton>
|
||||
</Checkbox.Root>
|
||||
)
|
||||
})
|
3
packages/tldraw/src/components/TopPanel/HelpMenu.tsx
Normal file
3
packages/tldraw/src/components/TopPanel/HelpMenu.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function HelpMenu() {
|
||||
return <div />
|
||||
}
|
|
@ -1,90 +1,93 @@
|
|||
import * as React from 'react'
|
||||
import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
|
||||
import { ExitIcon, GitHubLogoIcon, HamburgerMenuIcon, TwitterLogoIcon } from '@radix-ui/react-icons'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import { DMItem, DMContent, DMDivider, DMSubMenu, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { SmallIcon } from '~components/SmallIcon'
|
||||
import { useFileSystemHandlers } from '~hooks'
|
||||
import { HeartIcon } from '~components/icons/HeartIcon'
|
||||
import { preventEvent } from '~components/preventEvent'
|
||||
|
||||
interface MenuProps {
|
||||
showSponsorLink: boolean
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
||||
const { state } = useTLDrawContext()
|
||||
export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: MenuProps) {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
||||
|
||||
const handleSignIn = React.useCallback(() => {
|
||||
state.callbacks.onSignIn?.(state)
|
||||
}, [state])
|
||||
app.callbacks.onSignIn?.(app)
|
||||
}, [app])
|
||||
|
||||
const handleSignOut = React.useCallback(() => {
|
||||
state.callbacks.onSignOut?.(state)
|
||||
}, [state])
|
||||
app.callbacks.onSignOut?.(app)
|
||||
}, [app])
|
||||
|
||||
const handleCut = React.useCallback(() => {
|
||||
state.cut()
|
||||
}, [state])
|
||||
app.cut()
|
||||
}, [app])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
state.copy()
|
||||
}, [state])
|
||||
app.copy()
|
||||
}, [app])
|
||||
|
||||
const handlePaste = React.useCallback(() => {
|
||||
state.paste()
|
||||
}, [state])
|
||||
app.paste()
|
||||
}, [app])
|
||||
|
||||
const handleCopySvg = React.useCallback(() => {
|
||||
state.copySvg()
|
||||
}, [state])
|
||||
app.copySvg()
|
||||
}, [app])
|
||||
|
||||
const handleCopyJson = React.useCallback(() => {
|
||||
state.copyJson()
|
||||
}, [state])
|
||||
app.copyJson()
|
||||
}, [app])
|
||||
|
||||
const handleSelectAll = React.useCallback(() => {
|
||||
state.selectAll()
|
||||
}, [state])
|
||||
app.selectAll()
|
||||
}, [app])
|
||||
|
||||
const handleselectNone = React.useCallback(() => {
|
||||
state.selectNone()
|
||||
}, [state])
|
||||
app.selectNone()
|
||||
}, [app])
|
||||
|
||||
const showFileMenu =
|
||||
state.callbacks.onNewProject ||
|
||||
state.callbacks.onOpenProject ||
|
||||
state.callbacks.onSaveProject ||
|
||||
state.callbacks.onSaveProjectAs
|
||||
app.callbacks.onNewProject ||
|
||||
app.callbacks.onOpenProject ||
|
||||
app.callbacks.onSaveProject ||
|
||||
app.callbacks.onSaveProjectAs
|
||||
|
||||
const showSignInOutMenu = state.callbacks.onSignIn || state.callbacks.onSignOut
|
||||
const showSignInOutMenu = app.callbacks.onSignIn || app.callbacks.onSignOut || showSponsorLink
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DMTriggerIcon>
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon isSponsor={showSponsorLink}>
|
||||
<HamburgerMenuIcon />
|
||||
</DMTriggerIcon>
|
||||
<DMContent variant="menu">
|
||||
{showFileMenu && (
|
||||
<DMSubMenu label="File...">
|
||||
{state.callbacks.onNewProject && (
|
||||
<DMItem onSelect={onNewProject} kbd="#N">
|
||||
{app.callbacks.onNewProject && (
|
||||
<DMItem onClick={onNewProject} kbd="#N">
|
||||
New Project
|
||||
</DMItem>
|
||||
)}
|
||||
{state.callbacks.onOpenProject && (
|
||||
<DMItem onSelect={onOpenProject} kbd="#O">
|
||||
{app.callbacks.onOpenProject && (
|
||||
<DMItem onClick={onOpenProject} kbd="#O">
|
||||
Open...
|
||||
</DMItem>
|
||||
)}
|
||||
{state.callbacks.onSaveProject && (
|
||||
<DMItem onSelect={onSaveProject} kbd="#S">
|
||||
{app.callbacks.onSaveProject && (
|
||||
<DMItem onClick={onSaveProject} kbd="#S">
|
||||
Save
|
||||
</DMItem>
|
||||
)}
|
||||
{state.callbacks.onSaveProjectAs && (
|
||||
<DMItem onSelect={onSaveProjectAs} kbd="⇧#S">
|
||||
{app.callbacks.onSaveProjectAs && (
|
||||
<DMItem onClick={onSaveProjectAs} kbd="⇧#S">
|
||||
Save As...
|
||||
</DMItem>
|
||||
)}
|
||||
|
@ -93,42 +96,76 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
|||
{!readOnly && (
|
||||
<>
|
||||
<DMSubMenu label="Edit...">
|
||||
<DMItem onSelect={state.undo} kbd="#Z">
|
||||
<DMItem onSelect={preventEvent} onClick={app.undo} kbd="#Z">
|
||||
Undo
|
||||
</DMItem>
|
||||
<DMItem onSelect={state.redo} kbd="#⇧Z">
|
||||
<DMItem onSelect={preventEvent} onClick={app.redo} kbd="#⇧Z">
|
||||
Redo
|
||||
</DMItem>
|
||||
<DMDivider dir="ltr" />
|
||||
<DMItem onSelect={handleCut} kbd="#X">
|
||||
<DMItem onSelect={preventEvent} onClick={handleCut} kbd="#X">
|
||||
Cut
|
||||
</DMItem>
|
||||
<DMItem onSelect={handleCopy} kbd="#C">
|
||||
<DMItem onSelect={preventEvent} onClick={handleCopy} kbd="#C">
|
||||
Copy
|
||||
</DMItem>
|
||||
<DMItem onSelect={handlePaste} kbd="#V">
|
||||
<DMItem onSelect={preventEvent} onClick={handlePaste} kbd="#V">
|
||||
Paste
|
||||
</DMItem>
|
||||
<DMDivider dir="ltr" />
|
||||
<DMItem onSelect={handleCopySvg} kbd="#⇧C">
|
||||
<DMItem onSelect={preventEvent} onClick={handleCopySvg} kbd="#⇧C">
|
||||
Copy as SVG
|
||||
</DMItem>
|
||||
<DMItem onSelect={handleCopyJson}>Copy as JSON</DMItem>
|
||||
<DMItem onSelect={preventEvent} onClick={handleCopyJson}>
|
||||
Copy as JSON
|
||||
</DMItem>
|
||||
<DMDivider dir="ltr" />
|
||||
<DMItem onSelect={handleSelectAll} kbd="#A">
|
||||
<DMItem onSelect={preventEvent} onClick={handleSelectAll} kbd="#A">
|
||||
Select All
|
||||
</DMItem>
|
||||
<DMItem onSelect={handleselectNone}>Select None</DMItem>
|
||||
<DMItem onSelect={preventEvent} onClick={handleselectNone}>
|
||||
Select None
|
||||
</DMItem>
|
||||
</DMSubMenu>
|
||||
<DMDivider dir="ltr" />
|
||||
</>
|
||||
)}
|
||||
<a href="https://tldraw.com/r">
|
||||
<DMItem>Create a Multiplayer Room</DMItem>
|
||||
</a>
|
||||
<DMDivider dir="ltr" />
|
||||
<PreferencesMenu />
|
||||
<DMDivider dir="ltr" />
|
||||
<a href="https://github.com/Tldraw/Tldraw" target="_blank" rel="nofollow">
|
||||
<DMItem>
|
||||
Github
|
||||
<SmallIcon>
|
||||
<GitHubLogoIcon />
|
||||
</SmallIcon>
|
||||
</DMItem>
|
||||
</a>
|
||||
<a href="https://twitter.com/Tldraw" target="_blank" rel="nofollow">
|
||||
<DMItem>
|
||||
Twitter
|
||||
<SmallIcon>
|
||||
<TwitterLogoIcon />
|
||||
</SmallIcon>
|
||||
</DMItem>
|
||||
</a>
|
||||
{showSponsorLink && (
|
||||
<a href="https://github.com/sponsors/steveruizok" target="_blank" rel="nofollow">
|
||||
<DMItem isSponsor>
|
||||
Become a Sponsor{' '}
|
||||
<SmallIcon>
|
||||
<HeartIcon />
|
||||
</SmallIcon>
|
||||
</DMItem>
|
||||
</a>
|
||||
)}
|
||||
{showSignInOutMenu && (
|
||||
<>
|
||||
<DMDivider dir="ltr" />{' '}
|
||||
{state.callbacks.onSignIn && <DMItem onSelect={handleSignIn}>Sign In</DMItem>}
|
||||
{state.callbacks.onSignOut && (
|
||||
{app.callbacks.onSignIn && <DMItem onSelect={handleSignIn}>Sign In</DMItem>}
|
||||
{app.callbacks.onSignOut && (
|
||||
<DMItem onSelect={handleSignOut}>
|
||||
Sign Out
|
||||
<SmallIcon>
|
||||
|
|
82
packages/tldraw/src/components/TopPanel/MultiplayerMenu.tsx
Normal file
82
packages/tldraw/src/components/TopPanel/MultiplayerMenu.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import * as React from 'react'
|
||||
import { CheckIcon, ClipboardIcon } from '@radix-ui/react-icons'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { DMItem, DMContent, DMDivider, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { SmallIcon } from '~components/SmallIcon'
|
||||
import { MultiplayerIcon } from '~components/icons'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
const roomSelector = (state: TDSnapshot) => state.room
|
||||
|
||||
export const MultiplayerMenu = React.memo(function MultiplayerMenu() {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const room = app.useStore(roomSelector)
|
||||
|
||||
const [copied, setCopied] = React.useState(false)
|
||||
|
||||
const handleCopySelect = React.useCallback(() => {
|
||||
setCopied(true)
|
||||
TLDR.copyStringToClipboard(window.location.href)
|
||||
setTimeout(() => setCopied(false), 1200)
|
||||
}, [])
|
||||
|
||||
const handleCreateMultiplayerRoom = React.useCallback(async () => {
|
||||
if (app.isDirty) {
|
||||
if (app.fileSystemHandle) {
|
||||
if (window.confirm('Do you want to save changes to your current project?')) {
|
||||
await app.saveProject()
|
||||
}
|
||||
} else {
|
||||
if (window.confirm('Do you want to save your current project?')) {
|
||||
await app.saveProject()
|
||||
}
|
||||
}
|
||||
} else if (!app.fileSystemHandle) {
|
||||
if (window.confirm('Do you want to save your current project?')) {
|
||||
await app.saveProject()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleCopyToMultiplayerRoom = React.useCallback(async () => {
|
||||
const myHeaders = new Headers({
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
|
||||
const res = await fetch('http://tldraw.com/api/create-multiplayer-room', {
|
||||
headers: myHeaders,
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
body: JSON.stringify(app.document),
|
||||
}).then((res) => res.json())
|
||||
|
||||
window.location.href = `http://tldraw.com/r/${res.roomId}`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon>
|
||||
<MultiplayerIcon />
|
||||
</DMTriggerIcon>
|
||||
<DMContent variant="menu" align="start">
|
||||
<DMItem onClick={handleCreateMultiplayerRoom}>
|
||||
<a href="https://tldraw.com/r">Create a Multiplayer Room</a>
|
||||
</DMItem>
|
||||
<DMItem onClick={handleCopyToMultiplayerRoom}>Copy to Multiplayer Room</DMItem>
|
||||
{room && (
|
||||
<>
|
||||
<DMDivider />
|
||||
<DMItem onClick={handleCopySelect}>
|
||||
Copy Invite Link<SmallIcon>{copied ? <CheckIcon /> : <ClipboardIcon />}</SmallIcon>
|
||||
</DMItem>
|
||||
</>
|
||||
)}
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
|
@ -3,23 +3,22 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
|||
import { PlusIcon, CheckIcon } from '@radix-ui/react-icons'
|
||||
import { PageOptionsDialog } from './PageOptionsDialog'
|
||||
import { styled } from '~styles'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { DMContent, DMDivider } from '~components/DropdownMenu'
|
||||
import { SmallIcon } from '~components/SmallIcon'
|
||||
import { RowButton } from '~components/RowButton'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
const sortedSelector = (s: TLDrawSnapshot) =>
|
||||
const sortedSelector = (s: TDSnapshot) =>
|
||||
Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||
|
||||
const currentPageNameSelector = (s: TLDrawSnapshot) =>
|
||||
s.document.pages[s.appState.currentPageId].name
|
||||
const currentPageNameSelector = (s: TDSnapshot) => s.document.pages[s.appState.currentPageId].name
|
||||
|
||||
const currentPageIdSelector = (s: TLDrawSnapshot) => s.document.pages[s.appState.currentPageId].id
|
||||
const currentPageIdSelector = (s: TDSnapshot) => s.document.pages[s.appState.currentPageId].id
|
||||
|
||||
export function PageMenu(): JSX.Element {
|
||||
const { useSelector } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const rIsOpen = React.useRef(false)
|
||||
|
||||
|
@ -43,7 +42,7 @@ export function PageMenu(): JSX.Element {
|
|||
},
|
||||
[setIsOpen]
|
||||
)
|
||||
const currentPageName = useSelector(currentPageNameSelector)
|
||||
const currentPageName = app.useStore(currentPageNameSelector)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr" open={isOpen} onOpenChange={handleOpenChange}>
|
||||
|
@ -58,27 +57,27 @@ export function PageMenu(): JSX.Element {
|
|||
}
|
||||
|
||||
function PageMenuContent({ onClose }: { onClose: () => void }) {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const sortedPages = useSelector(sortedSelector)
|
||||
const sortedPages = app.useStore(sortedSelector)
|
||||
|
||||
const currentPageId = useSelector(currentPageIdSelector)
|
||||
const currentPageId = app.useStore(currentPageIdSelector)
|
||||
|
||||
const handleCreatePage = React.useCallback(() => {
|
||||
state.createPage()
|
||||
}, [state])
|
||||
app.createPage()
|
||||
}, [app])
|
||||
|
||||
const handleChangePage = React.useCallback(
|
||||
(id: string) => {
|
||||
onClose()
|
||||
state.changePage(id)
|
||||
app.changePage(id)
|
||||
},
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.RadioGroup value={currentPageId} onValueChange={handleChangePage}>
|
||||
<DropdownMenu.RadioGroup dir="ltr" value={currentPageId} onValueChange={handleChangePage}>
|
||||
{sortedPages.map((page) => (
|
||||
<ButtonWithOptions key={page.id}>
|
||||
<DropdownMenu.RadioItem
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from 'react'
|
||||
import * as Dialog from '@radix-ui/react-alert-dialog'
|
||||
import { MixerVerticalIcon } from '@radix-ui/react-icons'
|
||||
import type { TLDrawSnapshot, TLDrawPage } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { TDSnapshot, TDPage } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||
import { styled } from '~styles'
|
||||
import { Divider } from '~components/Divider'
|
||||
|
@ -10,36 +10,36 @@ import { IconButton } from '~components/IconButton/IconButton'
|
|||
import { SmallIcon } from '~components/SmallIcon'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
|
||||
const canDeleteSelector = (s: TLDrawSnapshot) => {
|
||||
const canDeleteSelector = (s: TDSnapshot) => {
|
||||
return Object.keys(s.document.pages).length > 1
|
||||
}
|
||||
|
||||
interface PageOptionsDialogProps {
|
||||
page: TLDrawPage
|
||||
page: TDPage
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
const canDelete = useSelector(canDeleteSelector)
|
||||
const canDelete = app.useStore(canDeleteSelector)
|
||||
|
||||
const rInput = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
state.duplicatePage(page.id)
|
||||
app.duplicatePage(page.id)
|
||||
onClose?.()
|
||||
}, [state])
|
||||
}, [app])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
if (window.confirm(`Are you sure you want to delete this page?`)) {
|
||||
state.deletePage(page.id)
|
||||
app.deletePage(page.id)
|
||||
onClose?.()
|
||||
}
|
||||
}, [state])
|
||||
}, [app])
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(isOpen: boolean) => {
|
||||
|
@ -50,7 +50,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr
|
|||
return
|
||||
}
|
||||
},
|
||||
[state, name]
|
||||
[app]
|
||||
)
|
||||
|
||||
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||
|
@ -60,7 +60,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr
|
|||
// TODO: Replace with text input
|
||||
function handleRename() {
|
||||
const nextName = window.prompt('New name:', page.name)
|
||||
state.renamePage(page.id, nextName || page.name || 'Page')
|
||||
app.renamePage(page.id, nextName || page.name || 'Page')
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -82,7 +82,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr
|
|||
</IconButton>
|
||||
</Dialog.Trigger>
|
||||
<StyledDialogOverlay />
|
||||
<StyledDialogContent onKeyDown={stopPropagation} onKeyUp={stopPropagation}>
|
||||
<StyledDialogContent dir="ltr" onKeyDown={stopPropagation} onKeyUp={stopPropagation}>
|
||||
<DialogAction onSelect={handleRename}>Rename</DialogAction>
|
||||
<DialogAction onSelect={handleDuplicate}>Duplicate</DialogAction>
|
||||
<DialogAction disabled={!canDelete} onSelect={handleDelete}>
|
||||
|
@ -112,7 +112,6 @@ export const StyledDialogContent = styled(Dialog.Content, {
|
|||
marginTop: '-5vh',
|
||||
pointerEvents: 'all',
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panelBorder',
|
||||
padding: '$0',
|
||||
borderRadius: '$2',
|
||||
font: '$ui',
|
||||
|
@ -132,9 +131,9 @@ export const StyledDialogOverlay = styled(Dialog.Overlay, {
|
|||
height: '100%',
|
||||
})
|
||||
|
||||
function DialogAction({ onSelect, ...rest }: RowButtonProps) {
|
||||
function DialogAction({ ...rest }: RowButtonProps & { onSelect: (e: Event) => void }) {
|
||||
return (
|
||||
<Dialog.Action asChild onClick={onSelect}>
|
||||
<Dialog.Action asChild>
|
||||
<RowButton {...rest} />
|
||||
</Dialog.Action>
|
||||
)
|
||||
|
|
|
@ -1,42 +1,42 @@
|
|||
import * as React from 'react'
|
||||
import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/DropdownMenu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import type { TDSnapshot } from '~types'
|
||||
|
||||
const settingsSelector = (s: TLDrawSnapshot) => s.settings
|
||||
const settingsSelector = (s: TDSnapshot) => s.settings
|
||||
|
||||
export function PreferencesMenu() {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const settings = useSelector(settingsSelector)
|
||||
const settings = app.useStore(settingsSelector)
|
||||
|
||||
const toggleDebugMode = React.useCallback(() => {
|
||||
state.setSetting('isDebugMode', (v) => !v)
|
||||
}, [state])
|
||||
app.setSetting('isDebugMode', (v) => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleDarkMode = React.useCallback(() => {
|
||||
state.setSetting('isDarkMode', (v) => !v)
|
||||
}, [state])
|
||||
app.setSetting('isDarkMode', (v) => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleFocusMode = React.useCallback(() => {
|
||||
state.setSetting('isFocusMode', (v) => !v)
|
||||
}, [state])
|
||||
app.setSetting('isFocusMode', (v) => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleRotateHandle = React.useCallback(() => {
|
||||
state.setSetting('showRotateHandles', (v) => !v)
|
||||
}, [state])
|
||||
app.setSetting('showRotateHandles', (v) => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleBoundShapesHandle = React.useCallback(() => {
|
||||
state.setSetting('showBindingHandles', (v) => !v)
|
||||
}, [state])
|
||||
app.setSetting('showBindingHandles', (v) => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleisSnapping = React.useCallback(() => {
|
||||
state.setSetting('isSnapping', (v) => !v)
|
||||
}, [state])
|
||||
app.setSetting('isSnapping', (v) => !v)
|
||||
}, [app])
|
||||
|
||||
const toggleCloneControls = React.useCallback(() => {
|
||||
state.setSetting('showCloneHandles', (v) => !v)
|
||||
}, [state])
|
||||
app.setSetting('showCloneHandles', (v) => !v)
|
||||
}, [app])
|
||||
|
||||
return (
|
||||
<DMSubMenu label="Preferences">
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { TLDrawSnapshot, SizeStyle } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { SizeSmallIcon, SizeMediumIcon, SizeLargeIcon } from '~components/icons'
|
||||
|
||||
const sizes = {
|
||||
[SizeStyle.Small]: <SizeSmallIcon />,
|
||||
[SizeStyle.Medium]: <SizeMediumIcon />,
|
||||
[SizeStyle.Large]: <SizeLargeIcon />,
|
||||
}
|
||||
|
||||
const selectSize = (s: TLDrawSnapshot) => s.appState.selectedStyle.size
|
||||
|
||||
const preventEvent = (e: Event) => e.preventDefault()
|
||||
|
||||
export const SizeMenu = React.memo(function SizeMenu(): JSX.Element {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
|
||||
const size = useSelector(selectSize)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon>{sizes[size as SizeStyle]}</DMTriggerIcon>
|
||||
<DMContent variant="horizontal">
|
||||
{Object.values(SizeStyle).map((sizeStyle: string) => (
|
||||
<DropdownMenu.Item key={sizeStyle} onSelect={preventEvent} asChild>
|
||||
<ToolButton
|
||||
isActive={size === sizeStyle}
|
||||
variant="icon"
|
||||
onClick={() => state.style({ size: sizeStyle as SizeStyle })}
|
||||
>
|
||||
{sizes[sizeStyle as SizeStyle]}
|
||||
</ToolButton>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
209
packages/tldraw/src/components/TopPanel/StyleMenu.tsx
Normal file
209
packages/tldraw/src/components/TopPanel/StyleMenu.tsx
Normal file
|
@ -0,0 +1,209 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { strokes, fills } from '~state/shapes/shared/shape-styles'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { DMCheckboxItem, DMContent, DMRadioItem, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import {
|
||||
CircleIcon,
|
||||
DashDashedIcon,
|
||||
DashDottedIcon,
|
||||
DashDrawIcon,
|
||||
DashSolidIcon,
|
||||
SizeLargeIcon,
|
||||
SizeMediumIcon,
|
||||
SizeSmallIcon,
|
||||
} from '~components/icons'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { TDSnapshot, ColorStyle, DashStyle, SizeStyle } from '~types'
|
||||
import { styled } from '~styles'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
import { Divider } from '~components/Divider'
|
||||
import { preventEvent } from '~components/preventEvent'
|
||||
|
||||
const selectedStyleSelector = (s: TDSnapshot) => s.appState.selectedStyle
|
||||
|
||||
const dashes = {
|
||||
[DashStyle.Draw]: <DashDrawIcon />,
|
||||
[DashStyle.Solid]: <DashSolidIcon />,
|
||||
[DashStyle.Dashed]: <DashDashedIcon />,
|
||||
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
[SizeStyle.Small]: <SizeSmallIcon />,
|
||||
[SizeStyle.Medium]: <SizeMediumIcon />,
|
||||
[SizeStyle.Large]: <SizeLargeIcon />,
|
||||
}
|
||||
|
||||
const themeSelector = (data: TDSnapshot) => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||
|
||||
export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const theme = app.useStore(themeSelector)
|
||||
|
||||
const style = app.useStore(selectedStyleSelector)
|
||||
|
||||
const handleToggleFilled = React.useCallback((checked: boolean) => {
|
||||
app.style({ isFilled: checked })
|
||||
}, [])
|
||||
|
||||
const handleDashChange = React.useCallback((value: string) => {
|
||||
app.style({ dash: value as DashStyle })
|
||||
}, [])
|
||||
|
||||
const handleSizeChange = React.useCallback((value: string) => {
|
||||
app.style({ size: value as SizeStyle })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon>
|
||||
<OverlapIcons
|
||||
style={{
|
||||
color: strokes[theme][style.color as ColorStyle],
|
||||
}}
|
||||
>
|
||||
{style.isFilled && (
|
||||
<CircleIcon size={16} stroke="none" fill={fills[theme][style.color as ColorStyle]} />
|
||||
)}
|
||||
{dashes[style.dash]}
|
||||
</OverlapIcons>
|
||||
</DMTriggerIcon>
|
||||
<DMContent>
|
||||
<StyledRow variant="tall">
|
||||
<span>Color</span>
|
||||
<ColorGrid>
|
||||
{Object.keys(strokes.light).map((colorStyle: string) => (
|
||||
<DropdownMenu.Item key={colorStyle} onSelect={preventEvent} asChild>
|
||||
<ToolButton
|
||||
variant="icon"
|
||||
isActive={style.color === colorStyle}
|
||||
onClick={() => app.style({ color: colorStyle as ColorStyle })}
|
||||
>
|
||||
<CircleIcon
|
||||
size={18}
|
||||
strokeWidth={2.5}
|
||||
fill={style.isFilled ? fills.light[colorStyle as ColorStyle] : 'transparent'}
|
||||
stroke={strokes.light[colorStyle as ColorStyle]}
|
||||
/>
|
||||
</ToolButton>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</ColorGrid>
|
||||
</StyledRow>
|
||||
<Divider />
|
||||
<StyledRow>
|
||||
Dash
|
||||
<StyledGroup dir="ltr" value={style.dash} onValueChange={handleDashChange}>
|
||||
{Object.values(DashStyle).map((dashStyle) => (
|
||||
<DMRadioItem
|
||||
key={dashStyle}
|
||||
isActive={dashStyle === style.dash}
|
||||
value={dashStyle}
|
||||
onSelect={preventEvent}
|
||||
bp={breakpoints}
|
||||
>
|
||||
{dashes[dashStyle as DashStyle]}
|
||||
</DMRadioItem>
|
||||
))}
|
||||
</StyledGroup>
|
||||
</StyledRow>
|
||||
<Divider />
|
||||
<StyledRow>
|
||||
Size
|
||||
<StyledGroup dir="ltr" value={style.size} onValueChange={handleSizeChange}>
|
||||
{Object.values(SizeStyle).map((sizeStyle) => (
|
||||
<DMRadioItem
|
||||
key={sizeStyle}
|
||||
isActive={sizeStyle === style.size}
|
||||
value={sizeStyle}
|
||||
onSelect={preventEvent}
|
||||
bp={breakpoints}
|
||||
>
|
||||
{sizes[sizeStyle as SizeStyle]}
|
||||
</DMRadioItem>
|
||||
))}
|
||||
</StyledGroup>
|
||||
</StyledRow>
|
||||
<Divider />
|
||||
<DMCheckboxItem checked={!!style.isFilled} onCheckedChange={handleToggleFilled}>
|
||||
Fill
|
||||
</DMCheckboxItem>
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
||||
|
||||
const ColorGrid = styled('div', {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, auto)',
|
||||
gap: 0,
|
||||
})
|
||||
|
||||
// const StyledRowInner = styled('div', {
|
||||
// height: '100%',
|
||||
// width: '100%',
|
||||
// backgroundColor: '$panel',
|
||||
// borderRadius: '$2',
|
||||
// display: 'flex',
|
||||
// gap: '$1',
|
||||
// flexDirection: 'row',
|
||||
// alignItems: 'center',
|
||||
// padding: '0 $3',
|
||||
// justifyContent: 'space-between',
|
||||
// border: '1px solid transparent',
|
||||
|
||||
// '& svg': {
|
||||
// position: 'relative',
|
||||
// stroke: '$overlay',
|
||||
// strokeWidth: 1,
|
||||
// zIndex: 1,
|
||||
// },
|
||||
// })
|
||||
|
||||
export const StyledRow = styled('div', {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
minHeight: '32px',
|
||||
outline: 'none',
|
||||
color: '$text',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
fontSize: '$1',
|
||||
padding: '0 0 0 $3',
|
||||
borderRadius: 4,
|
||||
userSelect: 'none',
|
||||
margin: 0,
|
||||
display: 'flex',
|
||||
gap: '$3',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
variants: {
|
||||
variant: {
|
||||
tall: {
|
||||
alignItems: 'flex-start',
|
||||
'& > span': {
|
||||
paddingTop: '$4',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledGroup = styled(DropdownMenu.DropdownMenuRadioGroup, {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
})
|
||||
|
||||
const OverlapIcons = styled('div', {
|
||||
display: 'grid',
|
||||
'& > *': {
|
||||
gridColumn: 1,
|
||||
gridRow: 1,
|
||||
},
|
||||
})
|
|
@ -3,10 +3,7 @@ import { Menu } from './Menu'
|
|||
import { styled } from '~styles'
|
||||
import { PageMenu } from './PageMenu'
|
||||
import { ZoomMenu } from './ZoomMenu'
|
||||
import { DashMenu } from './DashMenu'
|
||||
import { SizeMenu } from './SizeMenu'
|
||||
import { FillCheckbox } from './FillCheckbox'
|
||||
import { ColorMenu } from './ColorMenu'
|
||||
import { StyleMenu } from './StyleMenu'
|
||||
import { Panel } from '~components/Panel'
|
||||
|
||||
interface TopPanelProps {
|
||||
|
@ -15,28 +12,29 @@ interface TopPanelProps {
|
|||
showMenu: boolean
|
||||
showStyles: boolean
|
||||
showZoom: boolean
|
||||
showSponsorLink: boolean
|
||||
}
|
||||
|
||||
export function TopPanel({ readOnly, showPages, showMenu, showStyles, showZoom }: TopPanelProps) {
|
||||
export function TopPanel({
|
||||
readOnly,
|
||||
showPages,
|
||||
showMenu,
|
||||
showStyles,
|
||||
showZoom,
|
||||
showSponsorLink,
|
||||
}: TopPanelProps) {
|
||||
return (
|
||||
<StyledTopPanel>
|
||||
{(showMenu || showPages) && (
|
||||
<Panel side="left">
|
||||
{showMenu && <Menu readOnly={readOnly} />}
|
||||
{showMenu && <Menu showSponsorLink={showSponsorLink} readOnly={readOnly} />}
|
||||
{showPages && <PageMenu />}
|
||||
</Panel>
|
||||
)}
|
||||
<StyledSpacer />
|
||||
{(showStyles || showZoom) && (
|
||||
<Panel side="right">
|
||||
{showStyles && !readOnly && (
|
||||
<>
|
||||
<ColorMenu />
|
||||
<SizeMenu />
|
||||
<DashMenu />
|
||||
<FillCheckbox />
|
||||
</>
|
||||
)}
|
||||
{showStyles && !readOnly && <StyleMenu />}
|
||||
{showZoom && <ZoomMenu />}
|
||||
</Panel>
|
||||
)}
|
||||
|
|
|
@ -1,43 +1,46 @@
|
|||
import * as React from 'react'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { styled } from '~styles'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { DMItem, DMContent } from '~components/DropdownMenu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { preventEvent } from '~components/preventEvent'
|
||||
|
||||
const zoomSelector = (s: TLDrawSnapshot) =>
|
||||
s.document.pageStates[s.appState.currentPageId].camera.zoom
|
||||
const zoomSelector = (s: TDSnapshot) => s.document.pageStates[s.appState.currentPageId].camera.zoom
|
||||
|
||||
export function ZoomMenu() {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const zoom = useSelector(zoomSelector)
|
||||
export const ZoomMenu = React.memo(function ZoomMenu() {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const zoom = app.useStore(zoomSelector)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<FixedWidthToolButton variant="text">{Math.round(zoom * 100)}%</FixedWidthToolButton>
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DropdownMenu.Trigger dir="ltr" asChild>
|
||||
<FixedWidthToolButton onDoubleClick={app.resetZoom} variant="text">
|
||||
{Math.round(zoom * 100)}%
|
||||
</FixedWidthToolButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DMContent align="end">
|
||||
<DMItem onSelect={state.zoomIn} kbd="#+">
|
||||
<DMItem onSelect={preventEvent} onClick={app.zoomIn} kbd="#+">
|
||||
Zoom In
|
||||
</DMItem>
|
||||
<DMItem onSelect={state.zoomOut} kbd="#−">
|
||||
<DMItem onSelect={preventEvent} onClick={app.zoomOut} kbd="#−">
|
||||
Zoom Out
|
||||
</DMItem>
|
||||
<DMItem onSelect={state.resetZoom} kbd="⇧0">
|
||||
<DMItem onSelect={preventEvent} onClick={app.resetZoom} kbd="⇧0">
|
||||
To 100%
|
||||
</DMItem>
|
||||
<DMItem onSelect={state.zoomToFit} kbd="⇧1">
|
||||
<DMItem onSelect={preventEvent} onClick={app.zoomToFit} kbd="⇧1">
|
||||
To Fit
|
||||
</DMItem>
|
||||
<DMItem onSelect={state.zoomToSelection} kbd="⇧2">
|
||||
<DMItem onSelect={preventEvent} onClick={app.zoomToSelection} kbd="⇧2">
|
||||
To Selection
|
||||
</DMItem>
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const FixedWidthToolButton = styled(ToolButton, {
|
||||
minWidth: 56,
|
||||
|
|
|
@ -3,9 +3,11 @@ import * as React from 'react'
|
|||
export function BoxIcon({
|
||||
fill = 'none',
|
||||
stroke = 'currentColor',
|
||||
strokeWidth = 2,
|
||||
}: {
|
||||
fill?: string
|
||||
stroke?: string
|
||||
strokeWidth?: number
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
|
@ -13,10 +15,11 @@ export function BoxIcon({
|
|||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
fill={fill}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" strokeWidth="2" />
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function CircleIcon(
|
||||
props: Pick<React.SVGProps<SVGSVGElement>, 'stroke' | 'fill'> & {
|
||||
props: Pick<React.SVGProps<SVGSVGElement>, 'strokeWidth' | 'stroke' | 'fill'> & {
|
||||
size: number
|
||||
}
|
||||
) {
|
||||
|
|
21
packages/tldraw/src/components/icons/EraserIcon.tsx
Normal file
21
packages/tldraw/src/components/icons/EraserIcon.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function EraserIcon(): JSX.Element {
|
||||
return (
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.72838 9.33987L8.84935 2.34732C9.23874 1.96494 9.86279 1.96539 10.2516 2.34831L13.5636 5.60975C13.9655 6.00555 13.9607 6.65526 13.553 7.04507L8.13212 12.2278C7.94604 12.4057 7.69851 12.505 7.44107 12.505L6.06722 12.505L3.83772 12.505C3.5673 12.505 3.30842 12.3954 3.12009 12.2014L1.7114 10.7498C1.32837 10.3551 1.33596 9.72521 1.72838 9.33987Z"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<line
|
||||
x1="6.01807"
|
||||
y1="12.5"
|
||||
x2="10.7959"
|
||||
y2="12.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<line x1="5.50834" y1="5.74606" x2="10.1984" y2="10.4361" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
14
packages/tldraw/src/components/icons/HeartIcon.tsx
Normal file
14
packages/tldraw/src/components/icons/HeartIcon.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function HeartIcon() {
|
||||
return (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -11,7 +11,7 @@ export function IsFilledIcon(): JSX.Element {
|
|||
rx="2"
|
||||
strokeWidth="2"
|
||||
fill="currentColor"
|
||||
opacity=".3"
|
||||
opacity=".9"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
|
14
packages/tldraw/src/components/icons/MultiplayerIcon.tsx
Normal file
14
packages/tldraw/src/components/icons/MultiplayerIcon.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function MultiplayerIcon(): JSX.Element {
|
||||
return (
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.20634 0.691501C6.38439 0.610179 6.59352 0.640235 6.74146 0.768408L14.4999 7.49004C14.6546 7.62409 14.712 7.83893 14.6447 8.03228C14.5774 8.22564 14.399 8.35843 14.1945 8.36745L11.4343 8.48919L13.0504 12.04C13.1647 12.2913 13.0538 12.5877 12.8026 12.7022L10.9536 13.5444C10.7023 13.6589 10.4059 13.5481 10.2913 13.2968L8.67272 9.74659L6.83997 11.679L7.40596 12.9226C7.52032 13.1739 7.40939 13.4703 7.15815 13.5848L5.56925 14.3086C5.31801 14.423 5.02156 14.3122 4.90703 14.061L3.55936 11.105L2.00195 12.7471C1.86112 12.8956 1.64403 12.9433 1.45395 12.8675C1.26386 12.7917 1.13916 12.6077 1.13916 12.4031V3.59046C1.13916 3.39472 1.25338 3.21698 1.43143 3.13565C1.60949 3.05433 1.81862 3.08439 1.96656 3.21256L5.91406 6.63254V1.14631C5.91406 0.950565 6.02829 0.772823 6.20634 0.691501ZM7.13109 9.91888L6.91406 10.1477V9.92845V8.92747V8.82201V7.49891V2.24104L12.8962 7.42374L10.6504 7.52279C10.4845 7.53011 10.333 7.61939 10.2462 7.76104C10.2353 7.77874 10.2256 7.79698 10.2172 7.81563C10.1579 7.94618 10.1572 8.0971 10.2174 8.2294C10.2174 8.22941 10.2174 8.22941 10.2174 8.22942L11.9332 11.9993L10.9939 12.4272L9.27506 8.65717C9.2061 8.50591 9.06648 8.39882 8.90252 8.37142C8.73857 8.34402 8.57171 8.3999 8.45732 8.52051L8.2931 8.69366L7.1311 9.91887L7.13109 9.91888ZM5.91406 8.97158V7.95564L2.13916 4.68519V11.1493L3.34396 9.87894C3.45835 9.75833 3.62521 9.70245 3.78916 9.72985C3.95312 9.75725 4.09274 9.86434 4.1617 10.0156L5.60957 13.1913L6.28874 12.8819L4.84345 9.70633C4.77463 9.55512 4.78541 9.3796 4.87222 9.23795C4.95903 9.0963 5.11053 9.00702 5.2765 8.9997L5.91406 8.97158Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -11,3 +11,5 @@ export * from './UndoIcon'
|
|||
export * from './SizeSmallIcon'
|
||||
export * from './SizeMediumIcon'
|
||||
export * from './SizeLargeIcon'
|
||||
export * from './EraserIcon'
|
||||
export * from './MultiplayerIcon'
|
||||
|
|
1
packages/tldraw/src/components/preventEvent.ts
Normal file
1
packages/tldraw/src/components/preventEvent.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const preventEvent = (e: Event) => e.preventDefault()
|
1
packages/tldraw/src/components/stopPropagation.ts
Normal file
1
packages/tldraw/src/components/stopPropagation.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const stopPropagation = (e: Event) => e.stopPropagation()
|
|
@ -6,6 +6,8 @@ export const SNAP_DISTANCE = 5
|
|||
export const EMPTY_ARRAY = [] as any[]
|
||||
export const SLOW_SPEED = 10
|
||||
export const VERY_SLOW_SPEED = 2.5
|
||||
export const GHOSTED_OPACITY = 0.3
|
||||
export const DEAD_ZONE = 3
|
||||
|
||||
import type { Easing } from '~types'
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export * from './useKeyboardShortcuts'
|
||||
export * from './useTLDrawContext'
|
||||
export * from './useTldrawApp'
|
||||
export * from './useTheme'
|
||||
export * from './useStylesheet'
|
||||
export * from './useFileSystemHandlers'
|
||||
|
|
|
@ -1,41 +1,41 @@
|
|||
import * as React from 'react'
|
||||
import type { TLDrawState } from '~state'
|
||||
import type { TldrawApp } from '~state'
|
||||
|
||||
export function useFileSystem() {
|
||||
const promptSaveBeforeChange = React.useCallback(async (state: TLDrawState) => {
|
||||
if (state.isDirty) {
|
||||
if (state.fileSystemHandle) {
|
||||
const promptSaveBeforeChange = React.useCallback(async (app: TldrawApp) => {
|
||||
if (app.isDirty) {
|
||||
if (app.fileSystemHandle) {
|
||||
if (window.confirm('Do you want to save changes to your current project?')) {
|
||||
await state.saveProject()
|
||||
await app.saveProject()
|
||||
}
|
||||
} else {
|
||||
if (window.confirm('Do you want to save your current project?')) {
|
||||
await state.saveProject()
|
||||
await app.saveProject()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onNewProject = React.useCallback(
|
||||
async (state: TLDrawState) => {
|
||||
await promptSaveBeforeChange(state)
|
||||
state.newProject()
|
||||
async (app: TldrawApp) => {
|
||||
await promptSaveBeforeChange(app)
|
||||
app.newProject()
|
||||
},
|
||||
[promptSaveBeforeChange]
|
||||
)
|
||||
|
||||
const onSaveProject = React.useCallback((state: TLDrawState) => {
|
||||
state.saveProject()
|
||||
const onSaveProject = React.useCallback((app: TldrawApp) => {
|
||||
app.saveProject()
|
||||
}, [])
|
||||
|
||||
const onSaveProjectAs = React.useCallback((state: TLDrawState) => {
|
||||
state.saveProjectAs()
|
||||
const onSaveProjectAs = React.useCallback((app: TldrawApp) => {
|
||||
app.saveProjectAs()
|
||||
}, [])
|
||||
|
||||
const onOpenProject = React.useCallback(
|
||||
async (state: TLDrawState) => {
|
||||
await promptSaveBeforeChange(state)
|
||||
state.openProject()
|
||||
async (app: TldrawApp) => {
|
||||
await promptSaveBeforeChange(app)
|
||||
app.openProject()
|
||||
},
|
||||
[promptSaveBeforeChange]
|
||||
)
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
import * as React from 'react'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
|
||||
export function useFileSystemHandlers() {
|
||||
const { state } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const onNewProject = React.useCallback(
|
||||
async (e?: KeyboardEvent) => {
|
||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
||||
state.callbacks.onNewProject?.(state)
|
||||
async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||
app.callbacks.onNewProject?.(app)
|
||||
},
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
const onSaveProject = React.useCallback(
|
||||
(e?: KeyboardEvent) => {
|
||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
||||
state.callbacks.onSaveProject?.(state)
|
||||
(e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||
app.callbacks.onSaveProject?.(app)
|
||||
},
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
const onSaveProjectAs = React.useCallback(
|
||||
(e?: KeyboardEvent) => {
|
||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
||||
state.callbacks.onSaveProjectAs?.(state)
|
||||
(e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||
app.callbacks.onSaveProjectAs?.(app)
|
||||
},
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
const onOpenProject = React.useCallback(
|
||||
async (e?: KeyboardEvent) => {
|
||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
||||
state.callbacks.onOpenProject?.(state)
|
||||
async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||
app.callbacks.onOpenProject?.(app)
|
||||
},
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as React from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { TLDrawShapeType } from '~types'
|
||||
import { useFileSystemHandlers, useTLDrawContext } from '~hooks'
|
||||
import { TDShapeType } from '~types'
|
||||
import { useFileSystemHandlers, useTldrawApp } from '~hooks'
|
||||
|
||||
export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||
const { state } = useTLDrawContext()
|
||||
const app = useTldrawApp()
|
||||
|
||||
const canHandleEvent = React.useCallback(() => {
|
||||
const elm = ref.current
|
||||
|
@ -16,63 +16,80 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'v,1',
|
||||
() => {
|
||||
if (canHandleEvent()) state.selectTool('select')
|
||||
if (!canHandleEvent()) return
|
||||
app.selectTool('select')
|
||||
},
|
||||
[state, ref.current]
|
||||
[app, ref.current]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'd,2',
|
||||
() => {
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Draw)
|
||||
if (!canHandleEvent()) return
|
||||
app.selectTool(TDShapeType.Draw)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'r,3',
|
||||
'e,3',
|
||||
() => {
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Rectangle)
|
||||
if (!canHandleEvent()) return
|
||||
app.selectTool('erase')
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'e,4',
|
||||
'r,4',
|
||||
() => {
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Ellipse)
|
||||
if (!canHandleEvent()) return
|
||||
app.selectTool(TDShapeType.Rectangle)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'a,5',
|
||||
'5',
|
||||
() => {
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Arrow)
|
||||
if (!canHandleEvent()) return
|
||||
app.selectTool(TDShapeType.Ellipse)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
't,6',
|
||||
'a,6',
|
||||
() => {
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Text)
|
||||
if (!canHandleEvent()) return
|
||||
app.selectTool(TDShapeType.Arrow)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'n,7',
|
||||
't,7',
|
||||
() => {
|
||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Sticky)
|
||||
if (!canHandleEvent()) return
|
||||
app.selectTool(TDShapeType.Text)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'n,8',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.selectTool(TDShapeType.Sticky)
|
||||
},
|
||||
undefined,
|
||||
[app]
|
||||
)
|
||||
|
||||
/* ---------------------- Misc ---------------------- */
|
||||
|
@ -82,13 +99,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'ctrl+shift+d,command+shift+d',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
state.toggleDarkMode()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
app.toggleDarkMode()
|
||||
e.preventDefault()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Focus Mode
|
||||
|
@ -96,10 +112,11 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'ctrl+.,command+.',
|
||||
() => {
|
||||
if (canHandleEvent()) state.toggleFocusMode()
|
||||
if (!canHandleEvent()) return
|
||||
app.toggleFocusMode()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// File System
|
||||
|
@ -109,43 +126,43 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'ctrl+n,command+n',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
onNewProject(e)
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
onNewProject(e)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
useHotkeys(
|
||||
'ctrl+s,command+s',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
onSaveProject(e)
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
onSaveProject(e)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift+s,command+shift+s',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
onSaveProjectAs(e)
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
onSaveProjectAs(e)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
useHotkeys(
|
||||
'ctrl+o,command+o',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
onOpenProject(e)
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
onOpenProject(e)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Undo Redo
|
||||
|
@ -153,31 +170,31 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'command+z,ctrl+z',
|
||||
() => {
|
||||
if (canHandleEvent()) {
|
||||
if (state.session) {
|
||||
state.cancelSession()
|
||||
} else {
|
||||
state.undo()
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
if (app.session) {
|
||||
app.cancelSession()
|
||||
} else {
|
||||
app.undo()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift-z,command+shift+z',
|
||||
() => {
|
||||
if (canHandleEvent()) {
|
||||
if (state.session) {
|
||||
state.cancelSession()
|
||||
} else {
|
||||
state.redo()
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
if (app.session) {
|
||||
app.cancelSession()
|
||||
} else {
|
||||
app.redo()
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Undo Redo
|
||||
|
@ -185,19 +202,21 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'command+u,ctrl+u',
|
||||
() => {
|
||||
if (canHandleEvent()) state.undoSelect()
|
||||
if (!canHandleEvent()) return
|
||||
app.undoSelect()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift-u,command+shift+u',
|
||||
() => {
|
||||
if (canHandleEvent()) state.redoSelect()
|
||||
if (!canHandleEvent()) return
|
||||
app.redoSelect()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
/* -------------------- Commands -------------------- */
|
||||
|
@ -207,52 +226,55 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'ctrl+=,command+=',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
state.zoomIn()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.zoomIn()
|
||||
e.preventDefault()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+-,command+-',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
state.zoomOut()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.zoomOut()
|
||||
e.preventDefault()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+1',
|
||||
() => {
|
||||
if (canHandleEvent()) state.zoomToFit()
|
||||
if (!canHandleEvent()) return
|
||||
app.zoomToFit()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+2',
|
||||
() => {
|
||||
if (canHandleEvent()) state.zoomToSelection()
|
||||
if (!canHandleEvent()) return
|
||||
app.zoomToSelection()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+0',
|
||||
() => {
|
||||
if (canHandleEvent()) state.resetZoom()
|
||||
if (!canHandleEvent()) return
|
||||
app.resetZoom()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Duplicate
|
||||
|
@ -260,13 +282,13 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'ctrl+d,command+d',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
state.duplicate()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.duplicate()
|
||||
e.preventDefault()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Flip
|
||||
|
@ -274,19 +296,21 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'shift+h',
|
||||
() => {
|
||||
if (canHandleEvent()) state.flipHorizontal()
|
||||
if (!canHandleEvent()) return
|
||||
app.flipHorizontal()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+v',
|
||||
() => {
|
||||
if (canHandleEvent()) state.flipVertical()
|
||||
if (!canHandleEvent()) return
|
||||
app.flipVertical()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Cancel
|
||||
|
@ -294,12 +318,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'escape',
|
||||
() => {
|
||||
if (canHandleEvent()) {
|
||||
state.cancel()
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.cancel()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Delete
|
||||
|
@ -307,10 +331,11 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'backspace',
|
||||
() => {
|
||||
if (canHandleEvent()) state.delete()
|
||||
if (!canHandleEvent()) return
|
||||
app.delete()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Select All
|
||||
|
@ -318,10 +343,11 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'command+a,ctrl+a',
|
||||
() => {
|
||||
if (canHandleEvent()) state.selectAll()
|
||||
if (!canHandleEvent()) return
|
||||
app.selectAll()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Nudge
|
||||
|
@ -329,73 +355,91 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'up',
|
||||
() => {
|
||||
if (canHandleEvent()) state.nudge([0, -1], false)
|
||||
if (!canHandleEvent()) return
|
||||
app.nudge([0, -1], false)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
if (canHandleEvent()) state.nudge([1, 0], false)
|
||||
if (!canHandleEvent()) return
|
||||
app.nudge([1, 0], false)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'down',
|
||||
() => {
|
||||
if (canHandleEvent()) state.nudge([0, 1], false)
|
||||
if (!canHandleEvent()) return
|
||||
app.nudge([0, 1], false)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
if (canHandleEvent()) state.nudge([-1, 0], false)
|
||||
if (!canHandleEvent()) return
|
||||
app.nudge([-1, 0], false)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+up',
|
||||
() => {
|
||||
if (canHandleEvent()) state.nudge([0, -1], true)
|
||||
if (!canHandleEvent()) return
|
||||
app.nudge([0, -1], true)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+right',
|
||||
() => {
|
||||
if (canHandleEvent()) state.nudge([1, 0], true)
|
||||
if (!canHandleEvent()) return
|
||||
app.nudge([1, 0], true)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+down',
|
||||
() => {
|
||||
if (canHandleEvent()) state.nudge([0, 1], true)
|
||||
if (!canHandleEvent()) return
|
||||
app.nudge([0, 1], true)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+left',
|
||||
() => {
|
||||
if (canHandleEvent()) state.nudge([-1, 0], true)
|
||||
if (!canHandleEvent()) return
|
||||
app.nudge([-1, 0], true)
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+shift+l,ctrl+shift+l',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.toggleLocked()
|
||||
},
|
||||
undefined,
|
||||
[app]
|
||||
)
|
||||
|
||||
// Copy, Cut & Paste
|
||||
|
@ -403,28 +447,31 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'command+c,ctrl+c',
|
||||
() => {
|
||||
if (canHandleEvent()) state.copy()
|
||||
if (!canHandleEvent()) return
|
||||
app.copy()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+x,ctrl+x',
|
||||
() => {
|
||||
if (canHandleEvent()) state.cut()
|
||||
if (!canHandleEvent()) return
|
||||
app.cut()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+v,ctrl+v',
|
||||
() => {
|
||||
if (canHandleEvent()) state.paste()
|
||||
if (!canHandleEvent()) return
|
||||
app.paste()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Group & Ungroup
|
||||
|
@ -432,25 +479,25 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'command+g,ctrl+g',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
state.group()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.group()
|
||||
e.preventDefault()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+shift+g,ctrl+shift+g',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
state.ungroup()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
app.ungroup()
|
||||
e.preventDefault()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
// Move
|
||||
|
@ -458,50 +505,53 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
useHotkeys(
|
||||
'[',
|
||||
() => {
|
||||
if (canHandleEvent()) state.moveBackward()
|
||||
if (!canHandleEvent()) return
|
||||
app.moveBackward()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
']',
|
||||
() => {
|
||||
if (canHandleEvent()) state.moveForward()
|
||||
if (!canHandleEvent()) return
|
||||
app.moveForward()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+[',
|
||||
() => {
|
||||
if (canHandleEvent()) state.moveToBack()
|
||||
if (!canHandleEvent()) return
|
||||
app.moveToBack()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'shift+]',
|
||||
() => {
|
||||
if (canHandleEvent()) state.moveToFront()
|
||||
if (!canHandleEvent()) return
|
||||
app.moveToFront()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+shift+backspace',
|
||||
(e) => {
|
||||
if (canHandleEvent()) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
state.resetDocument()
|
||||
}
|
||||
e.preventDefault()
|
||||
if (!canHandleEvent()) return
|
||||
if (app.settings.isDebugMode) {
|
||||
app.resetDocument()
|
||||
}
|
||||
e.preventDefault()
|
||||
},
|
||||
undefined,
|
||||
[state]
|
||||
[app]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@ import * as React from 'react'
|
|||
|
||||
const styles = new Map<string, HTMLStyleElement>()
|
||||
|
||||
const UID = `tldraw-fonts`
|
||||
const CSS = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&display=swap')`
|
||||
const UID = `Tldraw-fonts`
|
||||
const CSS = `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&display=swap')
|
||||
`
|
||||
|
||||
export function useStylesheet() {
|
||||
React.useLayoutEffect(() => {
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import type { UseBoundStore } from 'zustand'
|
||||
import type { TLDrawState } from '~state'
|
||||
|
||||
export interface TLDrawContextType {
|
||||
state: TLDrawState
|
||||
useSelector: UseBoundStore<TLDrawSnapshot>
|
||||
}
|
||||
|
||||
export const TLDrawContext = React.createContext<TLDrawContextType>({} as TLDrawContextType)
|
||||
|
||||
export function useTLDrawContext() {
|
||||
const context = React.useContext(TLDrawContext)
|
||||
|
||||
return context
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
import type { TLDrawSnapshot, Theme } from '~types'
|
||||
import { useTLDrawContext } from './useTLDrawContext'
|
||||
import type { TDSnapshot, Theme } from '~types'
|
||||
import { useTldrawApp } from './useTldrawApp'
|
||||
|
||||
const themeSelector = (data: TLDrawSnapshot): Theme => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||
const themeSelector = (data: TDSnapshot): Theme => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||
|
||||
export function useTheme() {
|
||||
const { state, useSelector } = useTLDrawContext()
|
||||
const theme = useSelector(themeSelector)
|
||||
const app = useTldrawApp()
|
||||
const theme = app.useStore(themeSelector)
|
||||
|
||||
return {
|
||||
theme,
|
||||
toggle: state.toggleDarkMode,
|
||||
toggle: app.toggleDarkMode,
|
||||
}
|
||||
}
|
||||
|
|
10
packages/tldraw/src/hooks/useTldrawApp.tsx
Normal file
10
packages/tldraw/src/hooks/useTldrawApp.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as React from 'react'
|
||||
import type { TldrawApp } from '~state'
|
||||
|
||||
export const TldrawContext = React.createContext<TldrawApp>({} as TldrawApp)
|
||||
|
||||
export function useTldrawApp() {
|
||||
const context = React.useContext(TldrawContext)
|
||||
|
||||
return context
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
export * from './TLDraw'
|
||||
export * from './Tldraw'
|
||||
export * from './types'
|
||||
export * from './state/shapes'
|
||||
export { TLDrawState } from './state'
|
||||
export { TldrawApp } from './state'
|
||||
export { useFileSystem } from './hooks'
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
import { TLBounds, TLTransformInfo, Utils, TLPageState } from '@tldraw/core'
|
||||
import {
|
||||
TLDrawSnapshot,
|
||||
TDSnapshot,
|
||||
ShapeStyles,
|
||||
ShapesWithProp,
|
||||
TLDrawShape,
|
||||
TLDrawBinding,
|
||||
TLDrawPage,
|
||||
TLDrawCommand,
|
||||
TLDrawPatch,
|
||||
TLDrawShapeType,
|
||||
TDShape,
|
||||
TDBinding,
|
||||
TDPage,
|
||||
TldrawCommand,
|
||||
TldrawPatch,
|
||||
TDShapeType,
|
||||
ArrowShape,
|
||||
} from '~types'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import type { TLDrawShapeUtil } from './shapes/TLDrawShapeUtil'
|
||||
import { getShapeUtils } from './shapes'
|
||||
import type { TDShapeUtil } from './shapes/TDShapeUtil'
|
||||
import { getShapeUtil } from './shapes'
|
||||
|
||||
export class TLDR {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static getShapeUtils<T extends TLDrawShape>(type: T['type']): TLDrawShapeUtil<T>
|
||||
static getShapeUtil<T extends TDShape>(type: T['type']): TDShapeUtil<T>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
static getShapeUtils<T extends TLDrawShape>(shape: T): TLDrawShapeUtil<T>
|
||||
static getShapeUtils<T extends TLDrawShape>(shape: T | T['type']) {
|
||||
return getShapeUtils<T>(shape)
|
||||
static getShapeUtil<T extends TDShape>(shape: T): TDShapeUtil<T>
|
||||
static getShapeUtil<T extends TDShape>(shape: T | T['type']) {
|
||||
return getShapeUtil<T>(shape)
|
||||
}
|
||||
|
||||
static getSelectedShapes(data: TLDrawSnapshot, pageId: string) {
|
||||
static getSelectedShapes(data: TDSnapshot, pageId: string) {
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
const selectedIds = TLDR.getSelectedIds(data, pageId)
|
||||
return selectedIds.map((id) => page.shapes[id])
|
||||
}
|
||||
|
||||
static screenToWorld(data: TLDrawSnapshot, point: number[]) {
|
||||
static screenToWorld(data: TDSnapshot, point: number[]) {
|
||||
const camera = TLDR.getPageState(data, data.appState.currentPageId).camera
|
||||
return Vec.sub(Vec.div(point, camera.zoom), camera.point)
|
||||
}
|
||||
|
@ -39,59 +39,59 @@ export class TLDR {
|
|||
return Utils.clamp(zoom, 0.1, 5)
|
||||
}
|
||||
|
||||
static getPage(data: TLDrawSnapshot, pageId: string): TLDrawPage {
|
||||
static getPage(data: TDSnapshot, pageId: string): TDPage {
|
||||
return data.document.pages[pageId]
|
||||
}
|
||||
|
||||
static getPageState(data: TLDrawSnapshot, pageId: string): TLPageState {
|
||||
static getPageState(data: TDSnapshot, pageId: string): TLPageState {
|
||||
return data.document.pageStates[pageId]
|
||||
}
|
||||
|
||||
static getSelectedIds(data: TLDrawSnapshot, pageId: string): string[] {
|
||||
static getSelectedIds(data: TDSnapshot, pageId: string): string[] {
|
||||
return TLDR.getPageState(data, pageId).selectedIds
|
||||
}
|
||||
|
||||
static getShapes(data: TLDrawSnapshot, pageId: string): TLDrawShape[] {
|
||||
static getShapes(data: TDSnapshot, pageId: string): TDShape[] {
|
||||
return Object.values(TLDR.getPage(data, pageId).shapes)
|
||||
}
|
||||
|
||||
static getCamera(data: TLDrawSnapshot, pageId: string): TLPageState['camera'] {
|
||||
static getCamera(data: TDSnapshot, pageId: string): TLPageState['camera'] {
|
||||
return TLDR.getPageState(data, pageId).camera
|
||||
}
|
||||
|
||||
static getShape<T extends TLDrawShape = TLDrawShape>(
|
||||
data: TLDrawSnapshot,
|
||||
static getShape<T extends TDShape = TDShape>(
|
||||
data: TDSnapshot,
|
||||
shapeId: string,
|
||||
pageId: string
|
||||
): T {
|
||||
return TLDR.getPage(data, pageId).shapes[shapeId] as T
|
||||
}
|
||||
|
||||
static getCenter<T extends TLDrawShape>(shape: T) {
|
||||
return TLDR.getShapeUtils(shape).getCenter(shape)
|
||||
static getCenter<T extends TDShape>(shape: T) {
|
||||
return TLDR.getShapeUtil(shape).getCenter(shape)
|
||||
}
|
||||
|
||||
static getBounds<T extends TLDrawShape>(shape: T) {
|
||||
return TLDR.getShapeUtils(shape).getBounds(shape)
|
||||
static getBounds<T extends TDShape>(shape: T) {
|
||||
return TLDR.getShapeUtil(shape).getBounds(shape)
|
||||
}
|
||||
|
||||
static getRotatedBounds<T extends TLDrawShape>(shape: T) {
|
||||
return TLDR.getShapeUtils(shape).getRotatedBounds(shape)
|
||||
static getRotatedBounds<T extends TDShape>(shape: T) {
|
||||
return TLDR.getShapeUtil(shape).getRotatedBounds(shape)
|
||||
}
|
||||
|
||||
static getSelectedBounds(data: TLDrawSnapshot): TLBounds {
|
||||
static getSelectedBounds(data: TDSnapshot): TLBounds {
|
||||
return Utils.getCommonBounds(
|
||||
TLDR.getSelectedShapes(data, data.appState.currentPageId).map((shape) =>
|
||||
TLDR.getShapeUtils(shape).getBounds(shape)
|
||||
TLDR.getShapeUtil(shape).getBounds(shape)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
static getParentId(data: TLDrawSnapshot, id: string, pageId: string) {
|
||||
static getParentId(data: TDSnapshot, id: string, pageId: string) {
|
||||
return TLDR.getShape(data, id, pageId).parentId
|
||||
}
|
||||
|
||||
// static getPointedId(data: TLDrawSnapshot, id: string, pageId: string): string {
|
||||
// static getPointedId(data: TDSnapshot, id: string, pageId: string): string {
|
||||
// const page = TLDR.getPage(data, pageId)
|
||||
// const pageState = TLDR.getPageState(data, data.appState.currentPageId)
|
||||
// const shape = TLDR.getShape(data, id, pageId)
|
||||
|
@ -102,7 +102,7 @@ export class TLDR {
|
|||
// : TLDR.getPointedId(data, shape.parentId, pageId)
|
||||
// }
|
||||
|
||||
// static getDrilledPointedId(data: TLDrawSnapshot, id: string, pageId: string): string {
|
||||
// static getDrilledPointedId(data: TDSnapshot, id: string, pageId: string): string {
|
||||
// const shape = TLDR.getShape(data, id, pageId)
|
||||
// const { currentPageId } = data.appState
|
||||
// const { currentParentId, pointedId } = TLDR.getPageState(data, data.appState.currentPageId)
|
||||
|
@ -114,7 +114,7 @@ export class TLDR {
|
|||
// : TLDR.getDrilledPointedId(data, shape.parentId, pageId)
|
||||
// }
|
||||
|
||||
// static getTopParentId(data: TLDrawSnapshot, id: string, pageId: string): string {
|
||||
// static getTopParentId(data: TDSnapshot, id: string, pageId: string): string {
|
||||
// const page = TLDR.getPage(data, pageId)
|
||||
// const pageState = TLDR.getPageState(data, pageId)
|
||||
// const shape = TLDR.getShape(data, id, pageId)
|
||||
|
@ -129,7 +129,7 @@ export class TLDR {
|
|||
// }
|
||||
|
||||
// Get an array of a shape id and its descendant shapes' ids
|
||||
static getDocumentBranch(data: TLDrawSnapshot, id: string, pageId: string): string[] {
|
||||
static getDocumentBranch(data: TDSnapshot, id: string, pageId: string): string[] {
|
||||
const shape = TLDR.getShape(data, id, pageId)
|
||||
|
||||
if (shape.children === undefined) return [id]
|
||||
|
@ -142,16 +142,16 @@ export class TLDR {
|
|||
|
||||
// Get a deep array of unproxied shapes and their descendants
|
||||
static getSelectedBranchSnapshot<K>(
|
||||
data: TLDrawSnapshot,
|
||||
data: TDSnapshot,
|
||||
pageId: string,
|
||||
fn: (shape: TLDrawShape) => K
|
||||
fn: (shape: TDShape) => K
|
||||
): ({ id: string } & K)[]
|
||||
static getSelectedBranchSnapshot(data: TLDrawSnapshot, pageId: string): TLDrawShape[]
|
||||
static getSelectedBranchSnapshot(data: TDSnapshot, pageId: string): TDShape[]
|
||||
static getSelectedBranchSnapshot<K>(
|
||||
data: TLDrawSnapshot,
|
||||
data: TDSnapshot,
|
||||
pageId: string,
|
||||
fn?: (shape: TLDrawShape) => K
|
||||
): (TLDrawShape | K)[] {
|
||||
fn?: (shape: TDShape) => K
|
||||
): (TDShape | K)[] {
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
||||
const copies = TLDR.getSelectedIds(data, pageId)
|
||||
|
@ -167,17 +167,17 @@ export class TLDR {
|
|||
}
|
||||
|
||||
// Get a shallow array of unproxied shapes
|
||||
static getSelectedShapeSnapshot(data: TLDrawSnapshot, pageId: string): TLDrawShape[]
|
||||
static getSelectedShapeSnapshot(data: TDSnapshot, pageId: string): TDShape[]
|
||||
static getSelectedShapeSnapshot<K>(
|
||||
data: TLDrawSnapshot,
|
||||
data: TDSnapshot,
|
||||
pageId: string,
|
||||
fn?: (shape: TLDrawShape) => K
|
||||
fn?: (shape: TDShape) => K
|
||||
): ({ id: string } & K)[]
|
||||
static getSelectedShapeSnapshot<K>(
|
||||
data: TLDrawSnapshot,
|
||||
data: TDSnapshot,
|
||||
pageId: string,
|
||||
fn?: (shape: TLDrawShape) => K
|
||||
): (TLDrawShape | K)[] {
|
||||
fn?: (shape: TDShape) => K
|
||||
): (TDShape | K)[] {
|
||||
const copies = TLDR.getSelectedShapes(data, pageId)
|
||||
.filter((shape) => !shape.isLocked)
|
||||
.map(Utils.deepClone)
|
||||
|
@ -191,7 +191,7 @@ export class TLDR {
|
|||
|
||||
// For a given array of shape ids, an array of all other shapes that may be affected by a mutation to it.
|
||||
// Use this to decide which shapes to clone as before / after for a command.
|
||||
static getAllEffectedShapeIds(data: TLDrawSnapshot, ids: string[], pageId: string): string[] {
|
||||
static getAllEffectedShapeIds(data: TDSnapshot, ids: string[], pageId: string): string[] {
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
||||
const visited = new Set(ids)
|
||||
|
@ -200,7 +200,7 @@ export class TLDR {
|
|||
const shape = page.shapes[id]
|
||||
|
||||
// Add descendant shapes
|
||||
function collectDescendants(shape: TLDrawShape): void {
|
||||
function collectDescendants(shape: TDShape): void {
|
||||
if (shape.children === undefined) return
|
||||
shape.children
|
||||
.filter((childId) => !visited.has(childId))
|
||||
|
@ -213,7 +213,7 @@ export class TLDR {
|
|||
collectDescendants(shape)
|
||||
|
||||
// Add asecendant shapes
|
||||
function collectAscendants(shape: TLDrawShape): void {
|
||||
function collectAscendants(shape: TDShape): void {
|
||||
const parentId = shape.parentId
|
||||
if (parentId === page.id) return
|
||||
if (visited.has(parentId)) return
|
||||
|
@ -236,47 +236,47 @@ export class TLDR {
|
|||
}
|
||||
|
||||
static updateBindings(
|
||||
data: TLDrawSnapshot,
|
||||
data: TDSnapshot,
|
||||
id: string,
|
||||
beforeShapes: Record<string, Partial<TLDrawShape>> = {},
|
||||
afterShapes: Record<string, Partial<TLDrawShape>> = {},
|
||||
beforeShapes: Record<string, Partial<TDShape>> = {},
|
||||
afterShapes: Record<string, Partial<TDShape>> = {},
|
||||
pageId: string
|
||||
): TLDrawSnapshot {
|
||||
): TDSnapshot {
|
||||
const page = { ...TLDR.getPage(data, pageId) }
|
||||
return Object.values(page.bindings)
|
||||
.filter((binding) => binding.fromId === id || binding.toId === id)
|
||||
.reduce((cTLDrawSnapshot, binding) => {
|
||||
.reduce((cTDSnapshot, binding) => {
|
||||
if (!beforeShapes[binding.fromId]) {
|
||||
beforeShapes[binding.fromId] = Utils.deepClone(
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId)
|
||||
TLDR.getShape(cTDSnapshot, binding.fromId, pageId)
|
||||
)
|
||||
}
|
||||
|
||||
if (!beforeShapes[binding.toId]) {
|
||||
beforeShapes[binding.toId] = Utils.deepClone(
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId)
|
||||
TLDR.getShape(cTDSnapshot, binding.toId, pageId)
|
||||
)
|
||||
}
|
||||
|
||||
TLDR.onBindingChange(
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId),
|
||||
TLDR.getShape(cTDSnapshot, binding.fromId, pageId),
|
||||
binding,
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId)
|
||||
TLDR.getShape(cTDSnapshot, binding.toId, pageId)
|
||||
)
|
||||
|
||||
afterShapes[binding.fromId] = Utils.deepClone(
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId)
|
||||
TLDR.getShape(cTDSnapshot, binding.fromId, pageId)
|
||||
)
|
||||
afterShapes[binding.toId] = Utils.deepClone(
|
||||
TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId)
|
||||
TLDR.getShape(cTDSnapshot, binding.toId, pageId)
|
||||
)
|
||||
|
||||
return cTLDrawSnapshot
|
||||
return cTDSnapshot
|
||||
}, data)
|
||||
}
|
||||
|
||||
static getLinkedShapes(
|
||||
data: TLDrawSnapshot,
|
||||
static getLinkedShapeIds(
|
||||
data: TDSnapshot,
|
||||
pageId: string,
|
||||
direction: 'center' | 'left' | 'right',
|
||||
includeArrows = true
|
||||
|
@ -294,7 +294,7 @@ export class TLDR {
|
|||
const arrows = new Set(
|
||||
Object.values(page.shapes).filter((shape) => {
|
||||
return (
|
||||
shape.type === TLDrawShapeType.Arrow &&
|
||||
shape.type === TDShapeType.Arrow &&
|
||||
(shape.handles.start.bindingId || shape.handles?.end.bindingId)
|
||||
)
|
||||
}) as ArrowShape[]
|
||||
|
@ -378,11 +378,11 @@ export class TLDR {
|
|||
return Array.from(linkedIds.values())
|
||||
}
|
||||
|
||||
static getChildIndexAbove(data: TLDrawSnapshot, id: string, pageId: string): number {
|
||||
static getChildIndexAbove(data: TDSnapshot, id: string, pageId: string): number {
|
||||
const page = data.document.pages[pageId]
|
||||
const shape = page.shapes[id]
|
||||
|
||||
let siblings: TLDrawShape[]
|
||||
let siblings: TDShape[]
|
||||
|
||||
if (shape.parentId === page.id) {
|
||||
siblings = Object.values(page.shapes)
|
||||
|
@ -409,27 +409,29 @@ export class TLDR {
|
|||
/* Mutations */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
static getBeforeShape<T extends TLDrawShape>(shape: T, change: Partial<T>): Partial<T> {
|
||||
static getBeforeShape<T extends TDShape>(shape: T, change: Partial<T>): Partial<T> {
|
||||
return Object.fromEntries(
|
||||
Object.keys(change).map((k) => [k, shape[k as keyof T]])
|
||||
) as Partial<T>
|
||||
}
|
||||
|
||||
static mutateShapes<T extends TLDrawShape>(
|
||||
data: TLDrawSnapshot,
|
||||
static mutateShapes<T extends TDShape>(
|
||||
data: TDSnapshot,
|
||||
ids: string[],
|
||||
fn: (shape: T, i: number) => Partial<T> | void,
|
||||
pageId: string
|
||||
): {
|
||||
before: Record<string, Partial<T>>
|
||||
after: Record<string, Partial<T>>
|
||||
data: TLDrawSnapshot
|
||||
data: TDSnapshot
|
||||
} {
|
||||
const beforeShapes: Record<string, Partial<T>> = {}
|
||||
const afterShapes: Record<string, Partial<T>> = {}
|
||||
|
||||
ids.forEach((id, i) => {
|
||||
const shape = TLDR.getShape<T>(data, id, pageId)
|
||||
if (shape.isLocked) return
|
||||
|
||||
const change = fn(shape, i)
|
||||
if (change) {
|
||||
beforeShapes[id] = TLDR.getBeforeShape(shape, change)
|
||||
|
@ -446,8 +448,8 @@ export class TLDR {
|
|||
},
|
||||
},
|
||||
})
|
||||
const dataWithBindingChanges = ids.reduce<TLDrawSnapshot>((cTLDrawSnapshot, id) => {
|
||||
return TLDR.updateBindings(cTLDrawSnapshot, id, beforeShapes, afterShapes, pageId)
|
||||
const dataWithBindingChanges = ids.reduce<TDSnapshot>((cTDSnapshot, id) => {
|
||||
return TLDR.updateBindings(cTDSnapshot, id, beforeShapes, afterShapes, pageId)
|
||||
}, dataWithMutations)
|
||||
|
||||
return {
|
||||
|
@ -457,17 +459,15 @@ export class TLDR {
|
|||
}
|
||||
}
|
||||
|
||||
static createShapes(data: TLDrawSnapshot, shapes: TLDrawShape[], pageId: string): TLDrawCommand {
|
||||
const before: TLDrawPatch = {
|
||||
static createShapes(data: TDSnapshot, shapes: TDShape[], pageId: string): TldrawCommand {
|
||||
const before: TldrawPatch = {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
shapes: {
|
||||
...Object.fromEntries(
|
||||
shapes.flatMap((shape) => {
|
||||
const results: [string, Partial<TLDrawShape> | undefined][] = [
|
||||
[shape.id, undefined],
|
||||
]
|
||||
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, undefined]]
|
||||
|
||||
// If the shape is a child of another shape, also save that shape
|
||||
if (shape.parentId !== pageId) {
|
||||
|
@ -485,7 +485,7 @@ export class TLDR {
|
|||
},
|
||||
}
|
||||
|
||||
const after: TLDrawPatch = {
|
||||
const after: TldrawPatch = {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
|
@ -493,9 +493,7 @@ export class TLDR {
|
|||
shapes: {
|
||||
...Object.fromEntries(
|
||||
shapes.flatMap((shape) => {
|
||||
const results: [string, Partial<TLDrawShape> | undefined][] = [
|
||||
[shape.id, shape],
|
||||
]
|
||||
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, shape]]
|
||||
|
||||
// If the shape is a child of a different shape, update its parent
|
||||
if (shape.parentId !== pageId) {
|
||||
|
@ -521,10 +519,10 @@ export class TLDR {
|
|||
}
|
||||
|
||||
static deleteShapes(
|
||||
data: TLDrawSnapshot,
|
||||
shapes: TLDrawShape[] | string[],
|
||||
data: TDSnapshot,
|
||||
shapes: TDShape[] | string[],
|
||||
pageId?: string
|
||||
): TLDrawCommand {
|
||||
): TldrawCommand {
|
||||
pageId = pageId ? pageId : data.appState.currentPageId
|
||||
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
@ -532,9 +530,9 @@ export class TLDR {
|
|||
const shapeIds =
|
||||
typeof shapes[0] === 'string'
|
||||
? (shapes as string[])
|
||||
: (shapes as TLDrawShape[]).map((shape) => shape.id)
|
||||
: (shapes as TDShape[]).map((shape) => shape.id)
|
||||
|
||||
const before: TLDrawPatch = {
|
||||
const before: TldrawPatch = {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
|
@ -543,7 +541,7 @@ export class TLDR {
|
|||
...Object.fromEntries(
|
||||
shapeIds.flatMap((id) => {
|
||||
const shape = page.shapes[id]
|
||||
const results: [string, Partial<TLDrawShape> | undefined][] = [[shape.id, shape]]
|
||||
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, shape]]
|
||||
|
||||
// If the shape is a child of another shape, also add that shape
|
||||
if (shape.parentId !== pageId) {
|
||||
|
@ -573,7 +571,7 @@ export class TLDR {
|
|||
},
|
||||
}
|
||||
|
||||
const after: TLDrawPatch = {
|
||||
const after: TldrawPatch = {
|
||||
document: {
|
||||
pages: {
|
||||
[pageId]: {
|
||||
|
@ -581,9 +579,7 @@ export class TLDR {
|
|||
...Object.fromEntries(
|
||||
shapeIds.flatMap((id) => {
|
||||
const shape = page.shapes[id]
|
||||
const results: [string, Partial<TLDrawShape> | undefined][] = [
|
||||
[shape.id, undefined],
|
||||
]
|
||||
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, undefined]]
|
||||
|
||||
// If the shape is a child of a different shape, update its parent
|
||||
if (shape.parentId !== page.id) {
|
||||
|
@ -612,16 +608,16 @@ export class TLDR {
|
|||
}
|
||||
}
|
||||
|
||||
static onSessionComplete<T extends TLDrawShape>(shape: T) {
|
||||
const delta = TLDR.getShapeUtils(shape).onSessionComplete?.(shape)
|
||||
static onSessionComplete<T extends TDShape>(shape: T) {
|
||||
const delta = TLDR.getShapeUtil(shape).onSessionComplete?.(shape)
|
||||
if (!delta) return shape
|
||||
return { ...shape, ...delta }
|
||||
}
|
||||
|
||||
static onChildrenChange<T extends TLDrawShape>(data: TLDrawSnapshot, shape: T, pageId: string) {
|
||||
static onChildrenChange<T extends TDShape>(data: TDSnapshot, shape: T, pageId: string) {
|
||||
if (!shape.children) return
|
||||
|
||||
const delta = TLDR.getShapeUtils(shape).onChildrenChange?.(
|
||||
const delta = TLDR.getShapeUtil(shape).onChildrenChange?.(
|
||||
shape,
|
||||
shape.children.map((id) => TLDR.getShape(data, id, pageId))
|
||||
)
|
||||
|
@ -631,35 +627,27 @@ export class TLDR {
|
|||
return { ...shape, ...delta }
|
||||
}
|
||||
|
||||
static onBindingChange<T extends TLDrawShape>(
|
||||
shape: T,
|
||||
binding: TLDrawBinding,
|
||||
otherShape: TLDrawShape
|
||||
) {
|
||||
const delta = TLDR.getShapeUtils(shape).onBindingChange?.(
|
||||
static onBindingChange<T extends TDShape>(shape: T, binding: TDBinding, otherShape: TDShape) {
|
||||
const delta = TLDR.getShapeUtil(shape).onBindingChange?.(
|
||||
shape,
|
||||
binding,
|
||||
otherShape,
|
||||
TLDR.getShapeUtils(otherShape).getBounds(otherShape),
|
||||
TLDR.getShapeUtils(otherShape).getCenter(otherShape)
|
||||
TLDR.getShapeUtil(otherShape).getBounds(otherShape),
|
||||
TLDR.getShapeUtil(otherShape).getCenter(otherShape)
|
||||
)
|
||||
if (!delta) return shape
|
||||
|
||||
return { ...shape, ...delta }
|
||||
}
|
||||
|
||||
static transform<T extends TLDrawShape>(shape: T, bounds: TLBounds, info: TLTransformInfo<T>) {
|
||||
const delta = TLDR.getShapeUtils(shape).transform(shape, bounds, info)
|
||||
static transform<T extends TDShape>(shape: T, bounds: TLBounds, info: TLTransformInfo<T>) {
|
||||
const delta = TLDR.getShapeUtil(shape).transform(shape, bounds, info)
|
||||
if (!delta) return shape
|
||||
return { ...shape, ...delta }
|
||||
}
|
||||
|
||||
static transformSingle<T extends TLDrawShape>(
|
||||
shape: T,
|
||||
bounds: TLBounds,
|
||||
info: TLTransformInfo<T>
|
||||
) {
|
||||
const delta = TLDR.getShapeUtils(shape).transformSingle(shape, bounds, info)
|
||||
static transformSingle<T extends TDShape>(shape: T, bounds: TLBounds, info: TLTransformInfo<T>) {
|
||||
const delta = TLDR.getShapeUtil(shape).transformSingle(shape, bounds, info)
|
||||
if (!delta) return shape
|
||||
return { ...shape, ...delta }
|
||||
}
|
||||
|
@ -671,7 +659,7 @@ export class TLDR {
|
|||
* @param origin the page point to rotate around.
|
||||
* @param rotation the amount to rotate the shape.
|
||||
*/
|
||||
static getRotatedShapeMutation<T extends TLDrawShape>(
|
||||
static getRotatedShapeMutation<T extends TDShape>(
|
||||
shape: T, // in page space
|
||||
center: number[], // in page space
|
||||
origin: number[], // in page space (probably the center of common bounds)
|
||||
|
@ -690,7 +678,7 @@ export class TLDR {
|
|||
// of rotating the shape. Shapes with handles should never be rotated,
|
||||
// because that makes a lot of other things incredible difficult.
|
||||
if (shape.handles !== undefined) {
|
||||
const change = this.getShapeUtils(shape).onHandleChange?.(
|
||||
const change = this.getShapeUtil(shape).onHandleChange?.(
|
||||
// Base the change on a shape with the next point
|
||||
{ ...shape, point: nextPoint },
|
||||
Object.fromEntries(
|
||||
|
@ -723,7 +711,7 @@ export class TLDR {
|
|||
/* Parents */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
static updateParents(data: TLDrawSnapshot, pageId: string, changedShapeIds: string[]): void {
|
||||
static updateParents(data: TDSnapshot, pageId: string, changedShapeIds: string[]): void {
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
||||
if (changedShapeIds.length === 0) return
|
||||
|
@ -747,7 +735,7 @@ export class TLDR {
|
|||
TLDR.updateParents(data, pageId, parentToUpdateIds)
|
||||
}
|
||||
|
||||
static getSelectedStyle(data: TLDrawSnapshot, pageId: string): ShapeStyles | false {
|
||||
static getSelectedStyle(data: TDSnapshot, pageId: string): ShapeStyles | false {
|
||||
const { currentStyle } = data.appState
|
||||
|
||||
const page = data.document.pages[pageId]
|
||||
|
@ -788,27 +776,23 @@ export class TLDR {
|
|||
/* Bindings */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
static getBinding(data: TLDrawSnapshot, id: string, pageId: string): TLDrawBinding {
|
||||
static getBinding(data: TDSnapshot, id: string, pageId: string): TDBinding {
|
||||
return TLDR.getPage(data, pageId).bindings[id]
|
||||
}
|
||||
|
||||
static getBindings(data: TLDrawSnapshot, pageId: string): TLDrawBinding[] {
|
||||
static getBindings(data: TDSnapshot, pageId: string): TDBinding[] {
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
return Object.values(page.bindings)
|
||||
}
|
||||
|
||||
static getBindableShapeIds(data: TLDrawSnapshot) {
|
||||
static getBindableShapeIds(data: TDSnapshot) {
|
||||
return TLDR.getShapes(data, data.appState.currentPageId)
|
||||
.filter((shape) => TLDR.getShapeUtils(shape).canBind)
|
||||
.filter((shape) => TLDR.getShapeUtil(shape).canBind)
|
||||
.sort((a, b) => b.childIndex - a.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
}
|
||||
|
||||
static getBindingsWithShapeIds(
|
||||
data: TLDrawSnapshot,
|
||||
ids: string[],
|
||||
pageId: string
|
||||
): TLDrawBinding[] {
|
||||
static getBindingsWithShapeIds(data: TDSnapshot, ids: string[], pageId: string): TDBinding[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
TLDR.getBindings(data, pageId).filter((binding) => {
|
||||
|
@ -818,7 +802,7 @@ export class TLDR {
|
|||
)
|
||||
}
|
||||
|
||||
static getRelatedBindings(data: TLDrawSnapshot, ids: string[], pageId: string): TLDrawBinding[] {
|
||||
static getRelatedBindings(data: TDSnapshot, ids: string[], pageId: string): TDBinding[] {
|
||||
const changedShapeIds = new Set(ids)
|
||||
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
@ -897,7 +881,7 @@ export class TLDR {
|
|||
/* Groups */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
static flattenShape = (data: TLDrawSnapshot, shape: TLDrawShape): TLDrawShape[] => {
|
||||
static flattenShape = (data: TDSnapshot, shape: TDShape): TDShape[] => {
|
||||
return [
|
||||
shape,
|
||||
...(shape.children ?? [])
|
||||
|
@ -907,13 +891,13 @@ export class TLDR {
|
|||
]
|
||||
}
|
||||
|
||||
static flattenPage = (data: TLDrawSnapshot, pageId: string): TLDrawShape[] => {
|
||||
static flattenPage = (data: TDSnapshot, pageId: string): TDShape[] => {
|
||||
return Object.values(data.document.pages[pageId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.reduce<TLDrawShape[]>((acc, shape) => [...acc, ...TLDR.flattenShape(data, shape)], [])
|
||||
.reduce<TDShape[]>((acc, shape) => [...acc, ...TLDR.flattenShape(data, shape)], [])
|
||||
}
|
||||
|
||||
static getTopChildIndex = (data: TLDrawSnapshot, pageId: string): number => {
|
||||
static getTopChildIndex = (data: TDSnapshot, pageId: string): number => {
|
||||
const shapes = TLDR.getShapes(data, pageId)
|
||||
return shapes.length === 0
|
||||
? 1
|
||||
|
@ -922,12 +906,23 @@ export class TLDR {
|
|||
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Text */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
static fixNewLines = /\r?\n|\r/g
|
||||
static fixSpaces = / /g
|
||||
|
||||
static normalizeText(text: string) {
|
||||
return text.replace(TLDR.fixNewLines, '\n').replace(TLDR.fixSpaces, '\u00a0')
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Assertions */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
static assertShapeHasProperty<P extends keyof TLDrawShape>(
|
||||
shape: TLDrawShape,
|
||||
static assertShapeHasProperty<P extends keyof TDShape>(
|
||||
shape: TDShape,
|
||||
prop: P
|
||||
): asserts shape is ShapesWithProp<P> {
|
||||
if (shape[prop] === undefined) {
|
||||
|
|
|
@ -1,653 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { TLDrawState } from './TLDrawState'
|
||||
import { mockDocument, TLDrawStateUtils } from '~test'
|
||||
import { ArrowShape, ColorStyle, SessionType, TLDrawShapeType } from '~types'
|
||||
import type { SelectTool } from './tools/SelectTool'
|
||||
|
||||
describe('TLDrawState', () => {
|
||||
const state = new TLDrawState()
|
||||
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
|
||||
describe('When copying and pasting...', () => {
|
||||
it('copies a shape', () => {
|
||||
state.loadDocument(mockDocument).selectNone().copy(['rect1'])
|
||||
})
|
||||
|
||||
it('pastes a shape', () => {
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
const prevCount = Object.keys(state.page.shapes).length
|
||||
|
||||
state.selectNone().copy(['rect1']).paste()
|
||||
|
||||
expect(Object.keys(state.page.shapes).length).toBe(prevCount + 1)
|
||||
|
||||
state.undo()
|
||||
|
||||
expect(Object.keys(state.page.shapes).length).toBe(prevCount)
|
||||
|
||||
state.redo()
|
||||
|
||||
expect(Object.keys(state.page.shapes).length).toBe(prevCount + 1)
|
||||
})
|
||||
|
||||
it('pastes a shape to a new page', () => {
|
||||
state.loadDocument(mockDocument)
|
||||
|
||||
state.selectNone().copy(['rect1']).createPage().paste()
|
||||
|
||||
expect(Object.keys(state.page.shapes).length).toBe(1)
|
||||
|
||||
state.undo()
|
||||
|
||||
expect(Object.keys(state.page.shapes).length).toBe(0)
|
||||
|
||||
state.redo()
|
||||
|
||||
expect(Object.keys(state.page.shapes).length).toBe(1)
|
||||
})
|
||||
|
||||
it('Copies grouped shapes.', () => {
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('groupA')
|
||||
.copy()
|
||||
|
||||
const beforeShapes = state.shapes
|
||||
|
||||
state.paste()
|
||||
|
||||
expect(state.shapes.filter((shape) => shape.type === TLDrawShapeType.Group).length).toBe(2)
|
||||
|
||||
const afterShapes = state.shapes
|
||||
|
||||
const newShapes = afterShapes.filter(
|
||||
(shape) => !beforeShapes.find(({ id }) => id === shape.id)
|
||||
)
|
||||
|
||||
const newGroup = newShapes.find((shape) => shape.type === TLDrawShapeType.Group)
|
||||
|
||||
const newChildIds = newShapes
|
||||
.filter((shape) => shape.type !== TLDrawShapeType.Group)
|
||||
.map((shape) => shape.id)
|
||||
|
||||
expect(new Set(newGroup!.children)).toEqual(new Set(newChildIds))
|
||||
})
|
||||
|
||||
it.todo("Pastes in to the top child index of the page's children.")
|
||||
|
||||
it.todo('Pastes in the correct child index order.')
|
||||
})
|
||||
|
||||
describe('When copying and pasting a shape with bindings', () => {
|
||||
it('copies two bound shapes and their binding', () => {
|
||||
const state = new TLDrawState()
|
||||
|
||||
state
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([55, 55])
|
||||
.completeSession()
|
||||
|
||||
expect(state.bindings.length).toBe(1)
|
||||
|
||||
state.selectAll().copy().paste()
|
||||
|
||||
const newArrow = state.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
|
||||
|
||||
expect(newArrow.handles.start.bindingId).not.toBe(
|
||||
state.getShape<ArrowShape>('arrow1').handles.start.bindingId
|
||||
)
|
||||
|
||||
expect(state.bindings.length).toBe(2)
|
||||
})
|
||||
|
||||
it('removes bindings from copied shape handles', () => {
|
||||
const state = new TLDrawState()
|
||||
|
||||
state
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([55, 55])
|
||||
.completeSession()
|
||||
|
||||
expect(state.bindings.length).toBe(1)
|
||||
|
||||
expect(state.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeDefined()
|
||||
|
||||
state.select('arrow1').copy().paste()
|
||||
|
||||
const newArrow = state.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
|
||||
|
||||
expect(newArrow.handles.start.bindingId).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Selection', () => {
|
||||
it('selects a shape', () => {
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('selects and deselects a shape', () => {
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickCanvas()
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('selects multiple shapes', () => {
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
expect(state.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('shift-selects to deselect shapes', () => {
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('clears selection when clicking bounds', () => {
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
state.startSession(SessionType.Brush, [-10, -10])
|
||||
state.updateSession([110, 110])
|
||||
state.completeSession()
|
||||
expect(state.selectedIds.length).toBe(3)
|
||||
})
|
||||
|
||||
it('selects selected shape when single-clicked', () => {
|
||||
state.loadDocument(mockDocument).selectAll()
|
||||
tlu.clickShape('rect2')
|
||||
expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
})
|
||||
|
||||
// it('selects shape when double-clicked', () => {
|
||||
// state.loadDocument(mockDocument).selectAll()
|
||||
// tlu.doubleClickShape('rect2')
|
||||
// expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
// })
|
||||
|
||||
it('does not select on meta-click', () => {
|
||||
state.loadDocument(mockDocument).selectNone()
|
||||
tlu.clickShape('rect1', { ctrlKey: true })
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it.todo('deletes shapes if cancelled during creating')
|
||||
|
||||
it.todo('deletes shapes on undo after creating')
|
||||
|
||||
it.todo('re-creates shapes on redo after creating')
|
||||
|
||||
describe('When selecting all', () => {
|
||||
it('selects all', () => {
|
||||
const state = new TLDrawState().loadDocument(mockDocument).selectAll()
|
||||
expect(state.selectedIds).toMatchSnapshot('selected all')
|
||||
})
|
||||
|
||||
it('does not select children of a group', () => {
|
||||
const state = new TLDrawState().loadDocument(mockDocument).selectAll().group()
|
||||
expect(state.selectedIds.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Single click on a selected shape to select just that shape
|
||||
|
||||
it('single-selects shape in selection on click', () => {
|
||||
state.selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
tlu.clickShape('rect2')
|
||||
expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('single-selects shape in selection on pointerup only', () => {
|
||||
state.selectNone()
|
||||
tlu.clickShape('rect1')
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
tlu.pointShape('rect2')
|
||||
expect(state.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
tlu.stopPointing('rect2')
|
||||
expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(state.appState.status).toBe('idle')
|
||||
})
|
||||
|
||||
// it('selects shapes if shift key is lifted before pointerup', () => {
|
||||
// state.selectNone()
|
||||
// tlu.clickShape('rect1')
|
||||
// tlu.pointShape('rect2', { shiftKey: true })
|
||||
// expect(state.appState.status).toBe('pointingBounds')
|
||||
// tlu.stopPointing('rect2')
|
||||
// expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
// expect(state.appState.status).toBe('idle')
|
||||
// })
|
||||
})
|
||||
|
||||
describe('Select history', () => {
|
||||
it('selects, undoes and redoes', () => {
|
||||
state.reset().loadDocument(mockDocument)
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(0)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
|
||||
tlu.pointShape('rect1')
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(1)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlu.stopPointing('rect1')
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(1)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlu.clickShape('rect2', { shiftKey: true })
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(2)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
|
||||
state.undoSelect()
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(1)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
state.undoSelect()
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(0)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
|
||||
state.redoSelect()
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(1)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
state.select('rect2')
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(2)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect2']])
|
||||
expect(state.selectedIds).toStrictEqual(['rect2'])
|
||||
|
||||
state.delete()
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(0)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
|
||||
state.undoSelect()
|
||||
|
||||
expect(state.selectHistory.pointer).toBe(0)
|
||||
expect(state.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Copies to JSON', () => {
|
||||
state.selectAll()
|
||||
expect(state.copyJson()).toMatchSnapshot('copied json')
|
||||
})
|
||||
|
||||
describe('Mutates bound shapes', () => {
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{
|
||||
id: 'rect',
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
childIndex: 1,
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
},
|
||||
{
|
||||
id: 'arrow',
|
||||
point: [200, 200],
|
||||
childIndex: 2,
|
||||
type: TLDrawShapeType.Arrow,
|
||||
}
|
||||
)
|
||||
.select('arrow')
|
||||
.startSession(SessionType.Arrow, [200, 200], 'start')
|
||||
.updateSession([10, 10])
|
||||
.completeSession()
|
||||
.selectAll()
|
||||
.style({ color: ColorStyle.Red })
|
||||
|
||||
expect(state.getShape('arrow').style.color).toBe(ColorStyle.Red)
|
||||
expect(state.getShape('rect').style.color).toBe(ColorStyle.Red)
|
||||
|
||||
state.undo()
|
||||
|
||||
expect(state.getShape('arrow').style.color).toBe(ColorStyle.Black)
|
||||
expect(state.getShape('rect').style.color).toBe(ColorStyle.Black)
|
||||
|
||||
state.redo()
|
||||
|
||||
expect(state.getShape('arrow').style.color).toBe(ColorStyle.Red)
|
||||
expect(state.getShape('rect').style.color).toBe(ColorStyle.Red)
|
||||
})
|
||||
|
||||
describe('when selecting shapes in a group', () => {
|
||||
it('selects the group when a grouped shape is clicked', () => {
|
||||
const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
tlu.clickShape('rect1')
|
||||
expect((state.currentTool as SelectTool).selectedGroupId).toBeUndefined()
|
||||
expect(state.selectedIds).toStrictEqual(['groupA'])
|
||||
})
|
||||
|
||||
it('selects the grouped shape when double clicked', () => {
|
||||
const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
tlu.doubleClickShape('rect1')
|
||||
expect((state.currentTool as SelectTool).selectedGroupId).toStrictEqual('groupA')
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
})
|
||||
|
||||
it('clears the selectedGroupId when selecting a different shape', () => {
|
||||
const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
tlu.doubleClickShape('rect1')
|
||||
tlu.clickShape('rect3')
|
||||
expect((state.currentTool as SelectTool).selectedGroupId).toBeUndefined()
|
||||
expect(state.selectedIds).toStrictEqual(['rect3'])
|
||||
})
|
||||
|
||||
it('selects a grouped shape when meta-shift-clicked', () => {
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.selectNone()
|
||||
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
|
||||
tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
})
|
||||
|
||||
it('selects a hovered shape from the selected group when meta-shift-clicked', () => {
|
||||
const state = new TLDrawState().loadDocument(mockDocument).group(['rect1', 'rect2'], 'groupA')
|
||||
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
|
||||
tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
expect(state.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
tlu.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
expect(state.selectedIds).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when creating shapes', () => {
|
||||
it('Creates shapes with the correct child index', () => {
|
||||
const state = new TLDrawState()
|
||||
.createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
childIndex: 1,
|
||||
},
|
||||
{
|
||||
id: 'rect2',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
childIndex: 2,
|
||||
},
|
||||
{
|
||||
id: 'rect3',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
childIndex: 3,
|
||||
}
|
||||
)
|
||||
.selectTool(TLDrawShapeType.Rectangle)
|
||||
|
||||
const tlu = new TLDrawStateUtils(state)
|
||||
|
||||
const prevA = state.shapes.map((shape) => shape.id)
|
||||
|
||||
tlu.pointCanvas({ x: 0, y: 0 })
|
||||
tlu.movePointer({ x: 100, y: 100 })
|
||||
tlu.stopPointing()
|
||||
|
||||
const newIdA = state.shapes.map((shape) => shape.id).find((id) => !prevA.includes(id))!
|
||||
const shapeA = state.getShape(newIdA)
|
||||
expect(shapeA.childIndex).toBe(4)
|
||||
|
||||
state.group(['rect2', 'rect3', newIdA], 'groupA')
|
||||
|
||||
expect(state.getShape('groupA').childIndex).toBe(2)
|
||||
|
||||
state.selectNone()
|
||||
state.selectTool(TLDrawShapeType.Rectangle)
|
||||
|
||||
const prevB = state.shapes.map((shape) => shape.id)
|
||||
|
||||
tlu.pointCanvas({ x: 0, y: 0 })
|
||||
tlu.movePointer({ x: 100, y: 100 })
|
||||
tlu.stopPointing()
|
||||
|
||||
const newIdB = state.shapes.map((shape) => shape.id).find((id) => !prevB.includes(id))!
|
||||
const shapeB = state.getShape(newIdB)
|
||||
expect(shapeB.childIndex).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
it('Exposes undo/redo stack', () => {
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
.createShapes({
|
||||
id: 'rect2',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
|
||||
expect(state.history.length).toBe(2)
|
||||
|
||||
expect(state.history).toBeDefined()
|
||||
expect(state.history).toMatchSnapshot('history')
|
||||
|
||||
state.history = []
|
||||
expect(state.history).toEqual([])
|
||||
|
||||
const before = state.state
|
||||
state.undo()
|
||||
const after = state.state
|
||||
|
||||
expect(before).toBe(after)
|
||||
})
|
||||
|
||||
it('Exposes undo/redo stack up to the current pointer', () => {
|
||||
const state = new TLDrawState()
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
.createShapes({
|
||||
id: 'rect2',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
.undo()
|
||||
|
||||
expect(state.history.length).toBe(1)
|
||||
})
|
||||
|
||||
it('Sets the undo/redo history', () => {
|
||||
const state = new TLDrawState('some_state_a')
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
.createShapes({
|
||||
id: 'rect2',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
|
||||
// Save the history and document from the first state
|
||||
const doc = state.document
|
||||
const history = state.history
|
||||
|
||||
// Create a new state
|
||||
const state2 = new TLDrawState('some_state_b')
|
||||
|
||||
// Load the document and set the history
|
||||
state2.loadDocument(doc)
|
||||
state2.history = history
|
||||
|
||||
expect(state2.shapes.length).toBe(2)
|
||||
|
||||
// We should be able to undo the change that was made on the first
|
||||
// state, now that we've brought in its undo / redo stack
|
||||
state2.undo()
|
||||
|
||||
expect(state2.shapes.length).toBe(1)
|
||||
})
|
||||
|
||||
describe('When copying to SVG', () => {
|
||||
it('Copies shapes.', () => {
|
||||
const state = new TLDrawState()
|
||||
const result = state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.rotate(0.1)
|
||||
.selectAll()
|
||||
.copySvg()
|
||||
expect(result).toMatchSnapshot('copied svg')
|
||||
})
|
||||
|
||||
it('Copies grouped shapes.', () => {
|
||||
const state = new TLDrawState()
|
||||
const result = state
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.group()
|
||||
.selectAll()
|
||||
.copySvg()
|
||||
|
||||
expect(result).toMatchSnapshot('copied svg with group')
|
||||
})
|
||||
|
||||
it.todo('Copies Text shapes as <text> elements.')
|
||||
// it('Copies Text shapes as <text> elements.', () => {
|
||||
// const state2 = new TLDrawState()
|
||||
|
||||
// const svgString = state2
|
||||
// .createShapes({
|
||||
// id: 'text1',
|
||||
// type: TLDrawShapeType.Text,
|
||||
// text: 'hello world!',
|
||||
// })
|
||||
// .select('text1')
|
||||
// .copySvg()
|
||||
|
||||
// expect(svgString).toBeTruthy()
|
||||
// })
|
||||
})
|
||||
|
||||
describe('when the document prop changes', () => {
|
||||
it.todo('replaces the document if the ids are different')
|
||||
|
||||
it.todo('updates the document if the new id is the same as the old one')
|
||||
})
|
||||
/*
|
||||
We want to be able to use the `document` property to update the
|
||||
document without blowing out the current state. For example, we
|
||||
may want to patch in changes that occurred from another user.
|
||||
|
||||
When the `document` prop changes in the TLDraw component, we want
|
||||
to update the document in a way that preserves the identity of as
|
||||
much as possible, while still protecting against invalid states.
|
||||
|
||||
If this isn't possible, then we should guide the developer to
|
||||
instead use a helper like `patchDocument` to update the document.
|
||||
|
||||
If the `id` property of the new document is the same as the
|
||||
previous document, then we call `updateDocument`. Otherwise, we
|
||||
call `replaceDocument`, which does a harder reset of the state's
|
||||
internal state.
|
||||
*/
|
||||
|
||||
jest.setTimeout(10000)
|
||||
|
||||
describe('When changing versions', () => {
|
||||
it('migrates correctly', (done) => {
|
||||
const defaultState = TLDrawState.defaultState
|
||||
|
||||
const withoutRoom = {
|
||||
...defaultState,
|
||||
}
|
||||
|
||||
delete withoutRoom.room
|
||||
|
||||
TLDrawState.defaultState = withoutRoom
|
||||
|
||||
const state = new TLDrawState('migrate_1')
|
||||
|
||||
state.createShapes({
|
||||
id: 'rect1',
|
||||
type: TLDrawShapeType.Rectangle,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: Force the version to change and restore room.
|
||||
TLDrawState.version = 100
|
||||
TLDrawState.defaultState.room = defaultState.room
|
||||
|
||||
const state2 = new TLDrawState('migrate_1')
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
expect(state2.getShape('rect1')).toBeTruthy()
|
||||
done()
|
||||
} catch (e) {
|
||||
done(e)
|
||||
}
|
||||
}, 100)
|
||||
}, 100)
|
||||
})
|
||||
})
|
||||
})
|
661
packages/tldraw/src/state/TldrawApp.spec.ts
Normal file
661
packages/tldraw/src/state/TldrawApp.spec.ts
Normal file
|
@ -0,0 +1,661 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { mockDocument, TldrawTestApp } from '~test'
|
||||
import { ArrowShape, ColorStyle, SessionType, TDShapeType } from '~types'
|
||||
import type { SelectTool } from './tools/SelectTool'
|
||||
|
||||
describe('TldrawTestApp', () => {
|
||||
describe('When copying and pasting...', () => {
|
||||
it('copies a shape', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument).selectNone().copy(['rect1'])
|
||||
})
|
||||
|
||||
it('pastes a shape', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument)
|
||||
|
||||
const prevCount = Object.keys(app.page.shapes).length
|
||||
|
||||
app.selectNone().copy(['rect1']).paste()
|
||||
|
||||
expect(Object.keys(app.page.shapes).length).toBe(prevCount + 1)
|
||||
|
||||
app.undo()
|
||||
|
||||
expect(Object.keys(app.page.shapes).length).toBe(prevCount)
|
||||
|
||||
app.redo()
|
||||
|
||||
expect(Object.keys(app.page.shapes).length).toBe(prevCount + 1)
|
||||
})
|
||||
|
||||
it('pastes a shape to a new page', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument)
|
||||
|
||||
app.selectNone().copy(['rect1']).createPage().paste()
|
||||
|
||||
expect(Object.keys(app.page.shapes).length).toBe(1)
|
||||
|
||||
app.undo()
|
||||
|
||||
expect(Object.keys(app.page.shapes).length).toBe(0)
|
||||
|
||||
app.redo()
|
||||
|
||||
expect(Object.keys(app.page.shapes).length).toBe(1)
|
||||
})
|
||||
|
||||
it('Copies grouped shapes.', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('groupA')
|
||||
.copy()
|
||||
|
||||
const beforeShapes = app.shapes
|
||||
|
||||
app.paste()
|
||||
|
||||
expect(app.shapes.filter((shape) => shape.type === TDShapeType.Group).length).toBe(2)
|
||||
|
||||
const afterShapes = app.shapes
|
||||
|
||||
const newShapes = afterShapes.filter(
|
||||
(shape) => !beforeShapes.find(({ id }) => id === shape.id)
|
||||
)
|
||||
|
||||
const newGroup = newShapes.find((shape) => shape.type === TDShapeType.Group)
|
||||
|
||||
const newChildIds = newShapes
|
||||
.filter((shape) => shape.type !== TDShapeType.Group)
|
||||
.map((shape) => shape.id)
|
||||
|
||||
expect(new Set(newGroup!.children)).toEqual(new Set(newChildIds))
|
||||
})
|
||||
|
||||
it.todo("Pastes in to the top child index of the page's children.")
|
||||
|
||||
it.todo('Pastes in the correct child index order.')
|
||||
})
|
||||
|
||||
describe('When copying and pasting a shape with bindings', () => {
|
||||
it('copies two bound shapes and their binding', () => {
|
||||
const app = new TldrawTestApp()
|
||||
|
||||
app
|
||||
.createShapes(
|
||||
{ type: TDShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||
{ type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.movePointer([200, 200])
|
||||
.startSession(SessionType.Arrow, 'arrow1', 'start')
|
||||
.movePointer([55, 55])
|
||||
.completeSession()
|
||||
|
||||
expect(app.bindings.length).toBe(1)
|
||||
|
||||
app.selectAll().copy().paste()
|
||||
|
||||
const newArrow = app.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
|
||||
|
||||
expect(newArrow.handles.start.bindingId).not.toBe(
|
||||
app.getShape<ArrowShape>('arrow1').handles.start.bindingId
|
||||
)
|
||||
|
||||
expect(app.bindings.length).toBe(2)
|
||||
})
|
||||
|
||||
it('removes bindings from copied shape handles', () => {
|
||||
const app = new TldrawTestApp()
|
||||
|
||||
app
|
||||
.createShapes(
|
||||
{ type: TDShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||
{ type: TDShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.movePointer([200, 200])
|
||||
.startSession(SessionType.Arrow, 'arrow1', 'start')
|
||||
.movePointer([55, 55])
|
||||
.completeSession()
|
||||
|
||||
expect(app.bindings.length).toBe(1)
|
||||
|
||||
expect(app.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBeDefined()
|
||||
|
||||
app.select('arrow1').copy().paste()
|
||||
|
||||
const newArrow = app.shapes.sort((a, b) => b.childIndex - a.childIndex)[0] as ArrowShape
|
||||
|
||||
expect(newArrow.handles.start.bindingId).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Selection', () => {
|
||||
it('selects a shape', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument).selectNone().clickShape('rect1')
|
||||
expect(app.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(app.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('selects and deselects a shape', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.selectNone()
|
||||
.clickShape('rect1')
|
||||
.clickCanvas()
|
||||
expect(app.selectedIds).toStrictEqual([])
|
||||
expect(app.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('selects multiple shapes', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.selectNone()
|
||||
.clickShape('rect1')
|
||||
.clickShape('rect2', { shiftKey: true })
|
||||
expect(app.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(app.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('shift-selects to deselect shapes', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.selectNone()
|
||||
.clickShape('rect1')
|
||||
.clickShape('rect2', { shiftKey: true })
|
||||
.clickShape('rect2', { shiftKey: true })
|
||||
expect(app.selectedIds).toStrictEqual(['rect1'])
|
||||
expect(app.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('clears selection when clicking bounds', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.selectAll()
|
||||
.clickBounds()
|
||||
.completeSession()
|
||||
expect(app.selectedIds.length).toBe(0)
|
||||
})
|
||||
|
||||
it('selects selected shape when single-clicked', () => {
|
||||
new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.selectAll()
|
||||
.expectSelectedIdsToBe(['rect1', 'rect2', 'rect3'])
|
||||
.pointShape('rect1')
|
||||
.pointBounds() // because it is selected, argh
|
||||
.stopPointing('rect1')
|
||||
.expectSelectedIdsToBe(['rect1'])
|
||||
})
|
||||
|
||||
// it('selects shape when double-clicked', () => {
|
||||
// app.loadDocument(mockDocument).selectAll()
|
||||
// .doubleClickShape('rect2')
|
||||
// expect(app.selectedIds).toStrictEqual(['rect2'])
|
||||
// })
|
||||
|
||||
it('does not select on meta-click', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.selectNone()
|
||||
.clickShape('rect1', { ctrlKey: true })
|
||||
.expectSelectedIdsToBe([])
|
||||
|
||||
expect(app.status).toBe('idle')
|
||||
})
|
||||
|
||||
it.todo('deletes shapes if cancelled during creating')
|
||||
|
||||
it.todo('deletes shapes on undo after creating')
|
||||
|
||||
it.todo('re-creates shapes on redo after creating')
|
||||
|
||||
describe('When selecting all', () => {
|
||||
it('selects all', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument).selectAll()
|
||||
expect(app.selectedIds).toMatchSnapshot('selected all')
|
||||
})
|
||||
|
||||
it('does not select children of a group', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument).selectAll().group()
|
||||
expect(app.selectedIds.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Single click on a selected shape to select just that shape
|
||||
|
||||
it('single-selects shape in selection on click', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.clickShape('rect1')
|
||||
.clickShape('rect2', { shiftKey: true })
|
||||
.clickShape('rect2')
|
||||
expect(app.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(app.status).toBe('idle')
|
||||
})
|
||||
|
||||
it('single-selects shape in selection on pointerup only', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.clickShape('rect1')
|
||||
.clickShape('rect2', { shiftKey: true })
|
||||
.pointShape('rect2')
|
||||
expect(app.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
app.stopPointing('rect2')
|
||||
expect(app.selectedIds).toStrictEqual(['rect2'])
|
||||
expect(app.status).toBe('idle')
|
||||
})
|
||||
|
||||
// it('selects shapes if shift key is lifted before pointerup', () => {
|
||||
// app.selectNone()
|
||||
// .clickShape('rect1')
|
||||
// .pointShape('rect2', { shiftKey: true })
|
||||
// expect(app.status).toBe('pointingBounds')
|
||||
// .stopPointing('rect2')
|
||||
// expect(app.selectedIds).toStrictEqual(['rect2'])
|
||||
// expect(app.status).toBe('idle')
|
||||
// })
|
||||
})
|
||||
|
||||
describe('Select history', () => {
|
||||
it('selects, undoes and redoes', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument)
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(0)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(app.selectedIds).toStrictEqual([])
|
||||
app.pointShape('rect1')
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(1)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[], ['rect1']])
|
||||
expect(app.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
app.stopPointing('rect1')
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(1)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[], ['rect1']])
|
||||
expect(app.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
app.clickShape('rect2', { shiftKey: true })
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(2)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(app.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
|
||||
app.undoSelect()
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(1)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(app.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
app.undoSelect()
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(0)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(app.selectedIds).toStrictEqual([])
|
||||
|
||||
app.redoSelect()
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(1)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect1', 'rect2']])
|
||||
expect(app.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
app.select('rect2')
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(2)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[], ['rect1'], ['rect2']])
|
||||
expect(app.selectedIds).toStrictEqual(['rect2'])
|
||||
|
||||
app.delete()
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(0)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(app.selectedIds).toStrictEqual([])
|
||||
|
||||
app.undoSelect()
|
||||
|
||||
expect(app.selectHistory.pointer).toBe(0)
|
||||
expect(app.selectHistory.stack).toStrictEqual([[]])
|
||||
expect(app.selectedIds).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Copies to JSON', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument).selectAll()
|
||||
expect(app.copyJson()).toMatchSnapshot('copied json')
|
||||
})
|
||||
|
||||
describe('Mutates bound shapes', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.createShapes(
|
||||
{
|
||||
id: 'rect',
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
childIndex: 1,
|
||||
type: TDShapeType.Rectangle,
|
||||
},
|
||||
{
|
||||
id: 'arrow',
|
||||
point: [200, 200],
|
||||
childIndex: 2,
|
||||
type: TDShapeType.Arrow,
|
||||
}
|
||||
)
|
||||
.select('arrow')
|
||||
.movePointer([200, 200])
|
||||
.startSession(SessionType.Arrow, 'arrow', 'start')
|
||||
.movePointer([10, 10])
|
||||
.completeSession()
|
||||
.selectAll()
|
||||
.style({ color: ColorStyle.Red })
|
||||
|
||||
expect(app.getShape('arrow').style.color).toBe(ColorStyle.Red)
|
||||
expect(app.getShape('rect').style.color).toBe(ColorStyle.Red)
|
||||
|
||||
app.undo()
|
||||
|
||||
expect(app.getShape('arrow').style.color).toBe(ColorStyle.Black)
|
||||
expect(app.getShape('rect').style.color).toBe(ColorStyle.Black)
|
||||
|
||||
app.redo()
|
||||
|
||||
expect(app.getShape('arrow').style.color).toBe(ColorStyle.Red)
|
||||
expect(app.getShape('rect').style.color).toBe(ColorStyle.Red)
|
||||
})
|
||||
|
||||
describe('when selecting shapes in a group', () => {
|
||||
it('selects the group when a grouped shape is clicked', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.clickShape('rect1')
|
||||
|
||||
expect((app.currentTool as SelectTool).selectedGroupId).toBeUndefined()
|
||||
expect(app.selectedIds).toStrictEqual(['groupA'])
|
||||
})
|
||||
|
||||
it('selects the grouped shape when double clicked', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.doubleClickShape('rect1')
|
||||
|
||||
expect((app.currentTool as SelectTool).selectedGroupId).toStrictEqual('groupA')
|
||||
expect(app.selectedIds).toStrictEqual(['rect1'])
|
||||
})
|
||||
|
||||
it('clears the selectedGroupId when selecting a different shape', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.doubleClickShape('rect1')
|
||||
.clickShape('rect3')
|
||||
|
||||
expect((app.currentTool as SelectTool).selectedGroupId).toBeUndefined()
|
||||
expect(app.selectedIds).toStrictEqual(['rect3'])
|
||||
})
|
||||
|
||||
it('selects a grouped shape when meta-shift-clicked', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.selectNone()
|
||||
.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
|
||||
expect(app.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
app.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
|
||||
expect(app.selectedIds).toStrictEqual([])
|
||||
})
|
||||
|
||||
it('selects a hovered shape from the selected group when meta-shift-clicked', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
|
||||
expect(app.selectedIds).toStrictEqual(['rect1'])
|
||||
|
||||
app.clickShape('rect1', { ctrlKey: true, shiftKey: true })
|
||||
|
||||
expect(app.selectedIds).toStrictEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when creating shapes', () => {
|
||||
it('Creates shapes with the correct child index', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.createShapes(
|
||||
{
|
||||
id: 'rect1',
|
||||
type: TDShapeType.Rectangle,
|
||||
childIndex: 1,
|
||||
},
|
||||
{
|
||||
id: 'rect2',
|
||||
type: TDShapeType.Rectangle,
|
||||
childIndex: 2,
|
||||
},
|
||||
{
|
||||
id: 'rect3',
|
||||
type: TDShapeType.Rectangle,
|
||||
childIndex: 3,
|
||||
}
|
||||
)
|
||||
.selectTool(TDShapeType.Rectangle)
|
||||
|
||||
const prevA = app.shapes.map((shape) => shape.id)
|
||||
|
||||
app.pointCanvas({ x: 0, y: 0 }).movePointer({ x: 100, y: 100 }).stopPointing()
|
||||
|
||||
const newIdA = app.shapes.map((shape) => shape.id).find((id) => !prevA.includes(id))!
|
||||
const shapeA = app.getShape(newIdA)
|
||||
expect(shapeA.childIndex).toBe(4)
|
||||
|
||||
app.group(['rect2', 'rect3', newIdA], 'groupA')
|
||||
|
||||
expect(app.getShape('groupA').childIndex).toBe(2)
|
||||
|
||||
app.selectNone()
|
||||
app.selectTool(TDShapeType.Rectangle)
|
||||
|
||||
const prevB = app.shapes.map((shape) => shape.id)
|
||||
|
||||
app.pointCanvas({ x: 0, y: 0 }).movePointer({ x: 100, y: 100 }).stopPointing()
|
||||
|
||||
const newIdB = app.shapes.map((shape) => shape.id).find((id) => !prevB.includes(id))!
|
||||
const shapeB = app.getShape(newIdB)
|
||||
expect(shapeB.childIndex).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
it('Exposes undo/redo stack', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
type: TDShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
.createShapes({
|
||||
id: 'rect2',
|
||||
type: TDShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
|
||||
expect(app.history.length).toBe(2)
|
||||
|
||||
expect(app.history).toBeDefined()
|
||||
expect(app.history).toMatchSnapshot('history')
|
||||
|
||||
app.history = []
|
||||
expect(app.history).toEqual([])
|
||||
|
||||
const before = app.state
|
||||
app.undo()
|
||||
const after = app.state
|
||||
|
||||
expect(before).toBe(after)
|
||||
})
|
||||
|
||||
it('Exposes undo/redo stack up to the current pointer', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
type: TDShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
.createShapes({
|
||||
id: 'rect2',
|
||||
type: TDShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
.undo()
|
||||
|
||||
expect(app.history.length).toBe(1)
|
||||
})
|
||||
|
||||
it('Sets the undo/redo history', () => {
|
||||
const app = new TldrawTestApp('some_state_a')
|
||||
.createShapes({
|
||||
id: 'rect1',
|
||||
type: TDShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
.createShapes({
|
||||
id: 'rect2',
|
||||
type: TDShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 200],
|
||||
})
|
||||
|
||||
// Save the history and document from the first state
|
||||
const doc = app.document
|
||||
const history = app.history
|
||||
|
||||
// Create a new state
|
||||
const state2 = new TldrawTestApp('some_state_b')
|
||||
|
||||
// Load the document and set the history
|
||||
state2.loadDocument(doc)
|
||||
state2.history = history
|
||||
|
||||
expect(state2.shapes.length).toBe(2)
|
||||
|
||||
// We should be able to undo the change that was made on the first
|
||||
// state, now that we've brought in its undo / redo stack
|
||||
state2.undo()
|
||||
|
||||
expect(state2.shapes.length).toBe(1)
|
||||
})
|
||||
|
||||
describe('When copying to SVG', () => {
|
||||
it('Copies shapes.', () => {
|
||||
const result = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1')
|
||||
.rotate(0.1)
|
||||
.selectAll()
|
||||
.copySvg()
|
||||
expect(result).toMatchSnapshot('copied svg')
|
||||
})
|
||||
|
||||
it('Copies grouped shapes.', () => {
|
||||
const result = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
.select('rect1', 'rect2')
|
||||
.group()
|
||||
.selectAll()
|
||||
.copySvg()
|
||||
|
||||
expect(result).toMatchSnapshot('copied svg with group')
|
||||
})
|
||||
|
||||
it.todo('Copies Text shapes as <text> elements.')
|
||||
// it('Copies Text shapes as <text> elements.', () => {
|
||||
// const state2 = new TldrawTestApp()
|
||||
|
||||
// const svgString = state2
|
||||
// .createShapes({
|
||||
// id: 'text1',
|
||||
// type: TDShapeType.Text,
|
||||
// text: 'hello world!',
|
||||
// })
|
||||
// .select('text1')
|
||||
// .copySvg()
|
||||
|
||||
// expect(svgString).toBeTruthy()
|
||||
// })
|
||||
})
|
||||
|
||||
describe('when the document prop changes', () => {
|
||||
it.todo('replaces the document if the ids are different')
|
||||
|
||||
it.todo('updates the document if the new id is the same as the old one')
|
||||
})
|
||||
/*
|
||||
We want to be able to use the `document` property to update the
|
||||
document without blowing out the current app. For example, we
|
||||
may want to patch in changes that occurred from another user.
|
||||
|
||||
When the `document` prop changes in the Tldraw component, we want
|
||||
to update the document in a way that preserves the identity of as
|
||||
much as possible, while still protecting against invalid states.
|
||||
|
||||
If this isn't possible, then we should guide the developer to
|
||||
instead use a helper like `patchDocument` to update the document.
|
||||
|
||||
If the `id` property of the new document is the same as the
|
||||
previous document, then we call `updateDocument`. Otherwise, we
|
||||
call `replaceDocument`, which does a harder reset of the state's
|
||||
internal app.
|
||||
*/
|
||||
|
||||
jest.setTimeout(10000)
|
||||
|
||||
describe('When changing versions', () => {
|
||||
it('migrates correctly', (done) => {
|
||||
const defaultState = TldrawTestApp.defaultState
|
||||
|
||||
const withoutRoom = {
|
||||
...defaultState,
|
||||
}
|
||||
|
||||
delete withoutRoom.room
|
||||
|
||||
TldrawTestApp.defaultState = withoutRoom
|
||||
|
||||
const app = new TldrawTestApp('migrate_1')
|
||||
|
||||
app.createShapes({
|
||||
id: 'rect1',
|
||||
type: TDShapeType.Rectangle,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
// TODO: Force the version to change and restore room.
|
||||
TldrawTestApp.version = 100
|
||||
TldrawTestApp.defaultState.room = defaultState.room
|
||||
|
||||
const app2 = new TldrawTestApp('migrate_1')
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
expect(app2.getShape('rect1')).toBeTruthy()
|
||||
done()
|
||||
} catch (e) {
|
||||
done(e)
|
||||
}
|
||||
}, 100)
|
||||
}, 100)
|
||||
})
|
||||
})
|
||||
})
|
File diff suppressed because it is too large
Load diff
|
@ -1,8 +1,71 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[` 1`] = `"[]"`;
|
||||
exports[` 1`] = `
|
||||
"[
|
||||
{
|
||||
\\"id\\": \\"rect1\\",
|
||||
\\"parentId\\": \\"page1\\",
|
||||
\\"name\\": \\"Rectangle\\",
|
||||
\\"childIndex\\": 1,
|
||||
\\"type\\": \\"rectangle\\",
|
||||
\\"point\\": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
\\"size\\": [
|
||||
100,
|
||||
100
|
||||
],
|
||||
\\"style\\": {
|
||||
\\"dash\\": \\"draw\\",
|
||||
\\"size\\": \\"medium\\",
|
||||
\\"color\\": \\"blue\\"
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"rect2\\",
|
||||
\\"parentId\\": \\"page1\\",
|
||||
\\"name\\": \\"Rectangle\\",
|
||||
\\"childIndex\\": 2,
|
||||
\\"type\\": \\"rectangle\\",
|
||||
\\"point\\": [
|
||||
100,
|
||||
100
|
||||
],
|
||||
\\"size\\": [
|
||||
100,
|
||||
100
|
||||
],
|
||||
\\"style\\": {
|
||||
\\"dash\\": \\"draw\\",
|
||||
\\"size\\": \\"medium\\",
|
||||
\\"color\\": \\"blue\\"
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"id\\": \\"rect3\\",
|
||||
\\"parentId\\": \\"page1\\",
|
||||
\\"name\\": \\"Rectangle\\",
|
||||
\\"childIndex\\": 3,
|
||||
\\"type\\": \\"rectangle\\",
|
||||
\\"point\\": [
|
||||
20,
|
||||
20
|
||||
],
|
||||
\\"size\\": [
|
||||
100,
|
||||
100
|
||||
],
|
||||
\\"style\\": {
|
||||
\\"dash\\": \\"draw\\",
|
||||
\\"size\\": \\"medium\\",
|
||||
\\"color\\": \\"blue\\"
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`TLDrawState Exposes undo/redo stack: history 1`] = `
|
||||
exports[`TldrawTestApp Exposes undo/redo stack: history 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"after": Object {
|
||||
|
@ -36,6 +99,7 @@ Array [
|
|||
"color": "black",
|
||||
"dash": "draw",
|
||||
"isFilled": false,
|
||||
"scale": 1,
|
||||
"size": "small",
|
||||
},
|
||||
"type": "rectangle",
|
||||
|
@ -96,6 +160,7 @@ Array [
|
|||
"color": "black",
|
||||
"dash": "draw",
|
||||
"isFilled": false,
|
||||
"scale": 1,
|
||||
"size": "small",
|
||||
},
|
||||
"type": "rectangle",
|
||||
|
@ -129,7 +194,7 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`TLDrawState Selection When selecting all selects all: selected all 1`] = `
|
||||
exports[`TldrawTestApp Selection When selecting all selects all: selected all 1`] = `
|
||||
Array [
|
||||
"rect1",
|
||||
"rect2",
|
||||
|
@ -137,6 +202,6 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`TLDrawState When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"-16 -16 232 232\\" width=\\"200\\" height=\\"200\\"><g/></svg>"`;
|
||||
exports[`TldrawTestApp When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"-16 -16 232 232\\" width=\\"200\\" height=\\"200\\"><g/></svg>"`;
|
||||
|
||||
exports[`TLDrawState When copying to SVG Copies shapes.: copied svg 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"-20.741879096242684 -20.741879096242684 236.74 236.74\\" width=\\"204.74\\" height=\\"204.74\\"/>"`;
|
||||
exports[`TldrawTestApp When copying to SVG Copies shapes.: copied svg 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"-20.741879096242684 -20.741879096242684 236.74 236.74\\" width=\\"204.74\\" height=\\"204.74\\"/>"`;
|
|
@ -1,17 +1,16 @@
|
|||
import Vec from '@tldraw/vec'
|
||||
import { TLDrawState } from '~state'
|
||||
import { mockDocument, TLDrawStateUtils } from '~test'
|
||||
import { AlignType, TLDrawShapeType } from '~types'
|
||||
import { mockDocument, TldrawTestApp } from '~test'
|
||||
import { AlignType, TDShapeType } from '~types'
|
||||
|
||||
describe('Align command', () => {
|
||||
const state = new TLDrawState()
|
||||
const app = new TldrawTestApp()
|
||||
|
||||
describe('when less than two shapes are selected', () => {
|
||||
it('does nothing', () => {
|
||||
state.loadDocument(mockDocument).select('rect2')
|
||||
const initialState = state.state
|
||||
state.align(AlignType.Top)
|
||||
const currentState = state.state
|
||||
app.loadDocument(mockDocument).select('rect2')
|
||||
const initialState = app.state
|
||||
app.align(AlignType.Top)
|
||||
const currentState = app.state
|
||||
|
||||
expect(currentState).toEqual(initialState)
|
||||
})
|
||||
|
@ -19,86 +18,89 @@ describe('Align command', () => {
|
|||
|
||||
describe('when multiple shapes are selected', () => {
|
||||
beforeEach(() => {
|
||||
state.loadDocument(mockDocument)
|
||||
state.selectAll()
|
||||
app.loadDocument(mockDocument)
|
||||
app.selectAll()
|
||||
})
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
state.align(AlignType.Top)
|
||||
app.align(AlignType.Top)
|
||||
|
||||
expect(state.getShape('rect2').point).toEqual([100, 0])
|
||||
expect(app.getShape('rect2').point).toEqual([100, 0])
|
||||
|
||||
state.undo()
|
||||
app.undo()
|
||||
|
||||
expect(state.getShape('rect2').point).toEqual([100, 100])
|
||||
expect(app.getShape('rect2').point).toEqual([100, 100])
|
||||
|
||||
state.redo()
|
||||
app.redo()
|
||||
|
||||
expect(state.getShape('rect2').point).toEqual([100, 0])
|
||||
expect(app.getShape('rect2').point).toEqual([100, 0])
|
||||
})
|
||||
|
||||
it('aligns top', () => {
|
||||
state.align(AlignType.Top)
|
||||
app.align(AlignType.Top)
|
||||
|
||||
expect(state.getShape('rect2').point).toEqual([100, 0])
|
||||
expect(app.getShape('rect2').point).toEqual([100, 0])
|
||||
})
|
||||
|
||||
it('aligns right', () => {
|
||||
state.align(AlignType.Right)
|
||||
app.align(AlignType.Right)
|
||||
|
||||
expect(state.getShape('rect1').point).toEqual([100, 0])
|
||||
expect(app.getShape('rect1').point).toEqual([100, 0])
|
||||
})
|
||||
|
||||
it('aligns bottom', () => {
|
||||
state.align(AlignType.Bottom)
|
||||
app.align(AlignType.Bottom)
|
||||
|
||||
expect(state.getShape('rect1').point).toEqual([0, 100])
|
||||
expect(app.getShape('rect1').point).toEqual([0, 100])
|
||||
})
|
||||
|
||||
it('aligns left', () => {
|
||||
state.align(AlignType.Left)
|
||||
app.align(AlignType.Left)
|
||||
|
||||
expect(state.getShape('rect2').point).toEqual([0, 100])
|
||||
expect(app.getShape('rect2').point).toEqual([0, 100])
|
||||
})
|
||||
|
||||
it('aligns center horizontal', () => {
|
||||
state.align(AlignType.CenterHorizontal)
|
||||
app.align(AlignType.CenterHorizontal)
|
||||
|
||||
expect(state.getShape('rect1').point).toEqual([50, 0])
|
||||
expect(state.getShape('rect2').point).toEqual([50, 100])
|
||||
expect(app.getShape('rect1').point).toEqual([50, 0])
|
||||
expect(app.getShape('rect2').point).toEqual([50, 100])
|
||||
})
|
||||
|
||||
it('aligns center vertical', () => {
|
||||
state.align(AlignType.CenterVertical)
|
||||
app.align(AlignType.CenterVertical)
|
||||
|
||||
expect(state.getShape('rect1').point).toEqual([0, 50])
|
||||
expect(state.getShape('rect2').point).toEqual([100, 50])
|
||||
expect(app.getShape('rect1').point).toEqual([0, 50])
|
||||
expect(app.getShape('rect2').point).toEqual([100, 50])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when aligning groups', () => {
|
||||
it('aligns children', () => {
|
||||
const state = new TLDrawState()
|
||||
const app = new TldrawTestApp()
|
||||
.createShapes(
|
||||
{ id: 'rect1', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [100, 100] },
|
||||
{ id: 'rect2', type: TLDrawShapeType.Rectangle, point: [100, 100], size: [100, 100] },
|
||||
{ id: 'rect3', type: TLDrawShapeType.Rectangle, point: [200, 200], size: [100, 100] },
|
||||
{ id: 'rect4', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [200, 200] }
|
||||
{ id: 'rect1', type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100] },
|
||||
{ id: 'rect2', type: TDShapeType.Rectangle, point: [100, 100], size: [100, 100] },
|
||||
{ id: 'rect3', type: TDShapeType.Rectangle, point: [200, 200], size: [100, 100] },
|
||||
{ id: 'rect4', type: TDShapeType.Rectangle, point: [0, 0], size: [200, 200] }
|
||||
)
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.select('rect3', 'rect4')
|
||||
.align(AlignType.CenterVertical)
|
||||
|
||||
const p0 = state.getShape('rect4').point
|
||||
const p1 = state.getShape('rect3').point
|
||||
const p0 = app.getShape('rect4').point
|
||||
const p1 = app.getShape('rect3').point
|
||||
|
||||
state.undo().delete(['rect4']).selectAll().align(AlignType.CenterVertical)
|
||||
|
||||
new TLDrawStateUtils(state).expectShapesToBeAtPoints({
|
||||
rect1: p0,
|
||||
rect2: Vec.add(p0, [100, 100]),
|
||||
rect3: p1,
|
||||
})
|
||||
app
|
||||
.undo()
|
||||
.delete(['rect4'])
|
||||
.selectAll()
|
||||
.align(AlignType.CenterVertical)
|
||||
.expectShapesToBeAtPoints({
|
||||
rect1: p0,
|
||||
rect2: Vec.add(p0, [100, 100]),
|
||||
rect3: p1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Utils } from '@tldraw/core'
|
||||
import { AlignType, TLDrawCommand, TLDrawShapeType } from '~types'
|
||||
import type { TLDrawSnapshot } from '~types'
|
||||
import { AlignType, TldrawCommand, TDShapeType } from '~types'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import Vec from '@tldraw/vec'
|
||||
import type { TldrawApp } from '../../internal'
|
||||
|
||||
export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
export function alignShapes(app: TldrawApp, ids: string[], type: AlignType): TldrawCommand {
|
||||
const { currentPageId } = app
|
||||
|
||||
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
|
||||
const initialShapes = ids.map((id) => app.getShape(id))
|
||||
|
||||
const boundsForShapes = initialShapes.map((shape) => {
|
||||
return {
|
||||
id: shape.id,
|
||||
point: [...shape.point],
|
||||
bounds: TLDR.getShapeUtils(shape).getBounds(shape),
|
||||
bounds: TLDR.getBounds(shape),
|
||||
}
|
||||
})
|
||||
|
||||
const commonBounds = Utils.getCommonBounds(boundsForShapes.map(({ bounds }) => bounds))
|
||||
|
||||
const midX = commonBounds.minX + commonBounds.width / 2
|
||||
|
||||
const midY = commonBounds.minY + commonBounds.height / 2
|
||||
|
||||
const deltaMap = Object.fromEntries(
|
||||
|
@ -44,7 +44,7 @@ export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType
|
|||
)
|
||||
|
||||
const { before, after } = TLDR.mutateShapes(
|
||||
data,
|
||||
app.state,
|
||||
ids,
|
||||
(shape) => {
|
||||
if (!deltaMap[shape.id]) return shape
|
||||
|
@ -54,11 +54,11 @@ export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType
|
|||
)
|
||||
|
||||
initialShapes.forEach((shape) => {
|
||||
if (shape.type === TLDrawShapeType.Group) {
|
||||
if (shape.type === TDShapeType.Group) {
|
||||
const delta = Vec.sub(after[shape.id].point!, before[shape.id].point!)
|
||||
|
||||
shape.children.forEach((id) => {
|
||||
const child = TLDR.getShape(data, id, currentPageId)
|
||||
const child = app.getShape(id)
|
||||
before[child.id] = { point: child.point }
|
||||
after[child.id] = { point: Vec.add(child.point, delta) }
|
||||
})
|
||||
|
|
|
@ -1,32 +1,31 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { mockDocument, TldrawTestApp } from '~test'
|
||||
|
||||
describe('Change page command', () => {
|
||||
const state = new TLDrawState()
|
||||
const app = new TldrawTestApp()
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
state.loadDocument(mockDocument)
|
||||
app.loadDocument(mockDocument)
|
||||
|
||||
const initialId = state.page.id
|
||||
const initialId = app.page.id
|
||||
|
||||
state.createPage()
|
||||
app.createPage()
|
||||
|
||||
const nextId = state.page.id
|
||||
const nextId = app.page.id
|
||||
|
||||
state.changePage(initialId)
|
||||
app.changePage(initialId)
|
||||
|
||||
expect(state.page.id).toBe(initialId)
|
||||
expect(app.page.id).toBe(initialId)
|
||||
|
||||
state.changePage(nextId)
|
||||
app.changePage(nextId)
|
||||
|
||||
expect(state.page.id).toBe(nextId)
|
||||
expect(app.page.id).toBe(nextId)
|
||||
|
||||
state.undo()
|
||||
app.undo()
|
||||
|
||||
expect(state.page.id).toBe(initialId)
|
||||
expect(app.page.id).toBe(initialId)
|
||||
|
||||
state.redo()
|
||||
app.redo()
|
||||
|
||||
expect(state.page.id).toBe(nextId)
|
||||
expect(app.page.id).toBe(nextId)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import type { TLDrawSnapshot, TLDrawCommand } from '~types'
|
||||
import type { TldrawCommand } from '~types'
|
||||
import type { TldrawApp } from '../../internal'
|
||||
|
||||
export function changePage(data: TLDrawSnapshot, pageId: string): TLDrawCommand {
|
||||
export function changePage(app: TldrawApp, pageId: string): TldrawCommand {
|
||||
return {
|
||||
id: 'change_page',
|
||||
before: {
|
||||
appState: {
|
||||
currentPageId: data.appState.currentPageId,
|
||||
currentPageId: app.currentPageId,
|
||||
},
|
||||
},
|
||||
after: {
|
||||
|
|
|
@ -1,34 +1,33 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { mockDocument, TldrawTestApp } from '~test'
|
||||
|
||||
describe('Create page command', () => {
|
||||
const state = new TLDrawState()
|
||||
const app = new TldrawTestApp()
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
state.loadDocument(mockDocument)
|
||||
app.loadDocument(mockDocument)
|
||||
|
||||
const initialId = state.page.id
|
||||
const initialPageState = state.pageState
|
||||
const initialId = app.page.id
|
||||
const initialPageState = app.pageState
|
||||
|
||||
state.createPage()
|
||||
app.createPage()
|
||||
|
||||
const nextId = state.page.id
|
||||
const nextPageState = state.pageState
|
||||
const nextId = app.page.id
|
||||
const nextPageState = app.pageState
|
||||
|
||||
expect(Object.keys(state.document.pages).length).toBe(2)
|
||||
expect(state.page.id).toBe(nextId)
|
||||
expect(state.pageState).toEqual(nextPageState)
|
||||
expect(Object.keys(app.document.pages).length).toBe(2)
|
||||
expect(app.page.id).toBe(nextId)
|
||||
expect(app.pageState).toEqual(nextPageState)
|
||||
|
||||
state.undo()
|
||||
app.undo()
|
||||
|
||||
expect(Object.keys(state.document.pages).length).toBe(1)
|
||||
expect(state.page.id).toBe(initialId)
|
||||
expect(state.pageState).toEqual(initialPageState)
|
||||
expect(Object.keys(app.document.pages).length).toBe(1)
|
||||
expect(app.page.id).toBe(initialId)
|
||||
expect(app.pageState).toEqual(initialPageState)
|
||||
|
||||
state.redo()
|
||||
app.redo()
|
||||
|
||||
expect(Object.keys(state.document.pages).length).toBe(2)
|
||||
expect(state.page.id).toBe(nextId)
|
||||
expect(state.pageState).toEqual(nextPageState)
|
||||
expect(Object.keys(app.document.pages).length).toBe(2)
|
||||
expect(app.page.id).toBe(nextId)
|
||||
expect(app.pageState).toEqual(nextPageState)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import type { TLDrawSnapshot, TLDrawCommand, TLDrawPage } from '~types'
|
||||
import type { TldrawCommand, TDPage } from '~types'
|
||||
import { Utils, TLPageState } from '@tldraw/core'
|
||||
import type { TldrawApp } from '~state'
|
||||
|
||||
export function createPage(
|
||||
data: TLDrawSnapshot,
|
||||
app: TldrawApp,
|
||||
center: number[],
|
||||
pageId = Utils.uniqueId()
|
||||
): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
): TldrawCommand {
|
||||
const { currentPageId } = app
|
||||
|
||||
const topPage = Object.values(data.document.pages).sort(
|
||||
const topPage = Object.values(app.state.document.pages).sort(
|
||||
(a, b) => (b.childIndex || 0) - (a.childIndex || 0)
|
||||
)[0]
|
||||
|
||||
|
@ -17,7 +18,7 @@ export function createPage(
|
|||
// TODO: Iterate the name better
|
||||
const nextName = `New Page`
|
||||
|
||||
const page: TLDrawPage = {
|
||||
const page: TDPage = {
|
||||
id: pageId,
|
||||
name: nextName,
|
||||
childIndex: nextChildIndex,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue