1.0.0 (#267)
* remove sponsorwall for main route * Adds sponsorship link * Remove all sponsorwall * Fix sponsorship link appearance on dark mode * Add heart icon * Fix text bug * Fix toolbar, hide resize handles on sticky * Add eraser * Update Kbd.tsx * cleanup * base zoom delta on event deltaMode * Fix image in example * Fix eraser icon * eraser tool resets to previous tool * Update EraseTool.spec.ts * Improves support for locked shapes * Update _document.tsx * Update CHANGELOG.md * Adds multiplayer menu, fix develop route in example * Tighten up top panel padding * Update top bar, bump packages * refactor TLDrawState -> TLDrawApp, mutables, new tests * Fix scaling bug, delete groups bug * fix snapping * add pressure to points * Remove mutables, rename to tldraw (or Tldraw) * Clean up types, add darkmode prop * more renaming * rename getShapeUtils to getShapeUtil * Fix file names * Fix last bugs related to renaming * Update state to app in tests * rename types to TD * remove unused styles / rename styles * slight update to panel * Fix rogue radix perf issue * Update ZoomMenu.tsx * Consolidate style panel * Fix text wrapping in text shape, improve action menu * Fix props * add indicators for tool lock * fix calloits * Add click to erase shapes * Slightly improve loading screen * Update PrimaryTools.tsx * remove force consistent filenames from tsconfig * Update useTldrawApp.tsx * fix capitalization * Update main.yml
This commit is contained in:
parent
b7570ae6a3
commit
0c5f8dda48
255 changed files with 7978 additions and 7197 deletions
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
|
@ -17,4 +17,4 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
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/worker-*
|
||||||
www/public/sw.js
|
www/public/sw.js
|
||||||
www/public/sw.js.map
|
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).
|
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:.
|
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
|
# @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).
|
💕 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).
|
🙌 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
|
## Installation
|
||||||
|
|
||||||
|
@ -24,13 +24,13 @@ npm i @tldraw/tldraw
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Import the `TLDraw` React component and use it in your app.
|
Import the `tldraw` React component and use it in your app.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { TLDraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
|
|
||||||
function App() {
|
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.
|
You can use the `id` to persist the state in a user's browser storage.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { TLDraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return <TLDraw id="myState" />
|
return <Tldraw id="myState" />
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Controlling the Component through Props
|
### Controlling the Component through Props
|
||||||
|
|
||||||
You can control the `TLDraw` component through its props.
|
You can control the `tldraw` component through its props.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { TLDraw, TLDrawDocument } from '@tldraw/tldraw'
|
import { Tldraw, TDDocument } from '@tldraw/tldraw'
|
||||||
|
|
||||||
function App() {
|
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
|
```tsx
|
||||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
import { Tldraw, tldrawApp } from '@tldraw/tldraw'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
const handleMount = React.useCallback((state: tldrawApp) => {
|
||||||
state.selectAll()
|
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
|
### 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.
|
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
|
```tsx
|
||||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
import { Tldraw, tldrawApp } from '@tldraw/tldraw'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const handleChange = React.useCallback((state: TLDrawState, reason: string) => {
|
const handleChange = React.useCallback((state: tldrawApp, reason: string) => {
|
||||||
// Do something with the change
|
// 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
|
## 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
|
## Community
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# @tldraw/tldraw-electron
|
# @tldraw/tldraw-electron
|
||||||
|
|
||||||
An electron wrapper for TLDraw. Not yet published.
|
An electron wrapper for tldraw. Not yet published.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ export async function createWindow() {
|
||||||
minHeight: 480,
|
minHeight: 480,
|
||||||
minWidth: 600,
|
minWidth: 600,
|
||||||
titleBarStyle: 'hidden',
|
titleBarStyle: 'hidden',
|
||||||
title: 'TLDraw',
|
title: 'Tldraw',
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
devTools: true,
|
devTools: true,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { contextBridge, ipcRenderer } from 'electron'
|
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) => {
|
send: (channel: string, data: Message) => {
|
||||||
ipcRenderer.send(channel, data)
|
ipcRenderer.send(channel, data)
|
||||||
},
|
},
|
||||||
|
@ -10,6 +10,6 @@ const api: TLApi = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
contextBridge?.exposeInMainWorld('TLApi', api)
|
contextBridge?.exposeInMainWorld('TldrawBridgeApi', api)
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|
|
@ -1,85 +1,86 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
import { Tldraw, TldrawApp } from '@tldraw/tldraw'
|
||||||
import type { IpcMainEvent, IpcMain, IpcRenderer } from 'electron'
|
import type { Message, TldrawBridgeApi } from 'src/types'
|
||||||
import type { Message, TLApi } from 'src/types'
|
|
||||||
|
declare const window: Window & { TldrawBridgeApi: TldrawBridgeApi }
|
||||||
|
|
||||||
export default function App(): JSX.Element {
|
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.
|
// When the editor mounts, save the state instance in a ref.
|
||||||
const handleMount = React.useCallback((tldr: TLDrawState) => {
|
const handleMount = React.useCallback((tldr: TldrawApp) => {
|
||||||
rTLDrawState.current = tldr
|
rTldrawApp.current = tldr
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function handleEvent(message: Message) {
|
function handleEvent(message: Message) {
|
||||||
const state = rTLDrawState.current
|
const app = rTldrawApp.current
|
||||||
if (!state) return
|
if (!app) return
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'resetZoom': {
|
case 'resetZoom': {
|
||||||
state.resetZoom()
|
app.resetZoom()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'zoomIn': {
|
case 'zoomIn': {
|
||||||
state.zoomIn()
|
app.zoomIn()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'zoomOut': {
|
case 'zoomOut': {
|
||||||
state.zoomOut()
|
app.zoomOut()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'zoomToFit': {
|
case 'zoomToFit': {
|
||||||
state.zoomToFit()
|
app.zoomToFit()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'zoomToSelection': {
|
case 'zoomToSelection': {
|
||||||
state.zoomToSelection()
|
app.zoomToSelection()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'undo': {
|
case 'undo': {
|
||||||
state.undo()
|
app.undo()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'redo': {
|
case 'redo': {
|
||||||
state.redo()
|
app.redo()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'cut': {
|
case 'cut': {
|
||||||
state.cut()
|
app.cut()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'copy': {
|
case 'copy': {
|
||||||
state.copy()
|
app.copy()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'paste': {
|
case 'paste': {
|
||||||
state.paste()
|
app.paste()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'delete': {
|
case 'delete': {
|
||||||
state.delete()
|
app.delete()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'selectAll': {
|
case 'selectAll': {
|
||||||
state.selectAll()
|
app.selectAll()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'selectNone': {
|
case 'selectNone': {
|
||||||
state.selectNone()
|
app.selectNone()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { send, on } = (window as unknown as Window & { TLApi: TLApi })['TLApi']
|
const { on } = window.TldrawBridgeApi
|
||||||
|
|
||||||
on('projectMsg', handleEvent)
|
on('projectMsg', handleEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw id="electron" onMount={handleMount} autofocus showMenu={false} />
|
<Tldraw id="electron" onMount={handleMount} autofocus showMenu={false} />
|
||||||
</div>
|
</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: 'selectAll' }
|
||||||
| { type: 'selectNone' }
|
| { type: 'selectNone' }
|
||||||
|
|
||||||
export type TLApi = {
|
export type TldrawBridgeApi = {
|
||||||
send: (channel: string, data: Message) => void
|
send: (channel: string, data: Message) => void
|
||||||
on: (channel: string, cb: (message: Message) => void) => void
|
on: (channel: string, cb: (message: Message) => void) => void
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,5 +35,9 @@
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"typescript": "4.2.3"
|
"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 */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
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 {
|
export default function Imperative(): JSX.Element {
|
||||||
const rTLDrawState = React.useRef<TLDrawState>()
|
const rTldrawApp = React.useRef<TldrawApp>()
|
||||||
|
|
||||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
const handleMount = React.useCallback((app: TldrawApp) => {
|
||||||
rTLDrawState.current = state
|
rTldrawApp.current = app
|
||||||
|
|
||||||
state.createShapes(
|
app.createShapes(
|
||||||
{
|
{
|
||||||
id: 'rect1',
|
id: 'rect1',
|
||||||
type: TLDrawShapeType.Rectangle,
|
type: TDShapeType.Rectangle,
|
||||||
name: 'Rectangle',
|
name: 'Rectangle',
|
||||||
childIndex: 1,
|
childIndex: 1,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
|
@ -20,7 +20,7 @@ export default function Imperative(): JSX.Element {
|
||||||
{
|
{
|
||||||
id: 'rect2',
|
id: 'rect2',
|
||||||
name: 'Rectangle',
|
name: 'Rectangle',
|
||||||
type: TLDrawShapeType.Rectangle,
|
type: TDShapeType.Rectangle,
|
||||||
point: [200, 200],
|
point: [200, 200],
|
||||||
size: [100, 100],
|
size: [100, 100],
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,13 @@ export default function Imperative(): JSX.Element {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let i = 0
|
let i = 0
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const state = rTLDrawState.current!
|
const app = rTldrawApp.current!
|
||||||
const rect1 = state.getShape('rect1')
|
const rect1 = app.getShape('rect1')
|
||||||
|
|
||||||
if (!rect1) {
|
if (!rect1) {
|
||||||
state.createShapes({
|
app.createShapes({
|
||||||
id: 'rect1',
|
id: 'rect1',
|
||||||
type: TLDrawShapeType.Rectangle,
|
type: TDShapeType.Rectangle,
|
||||||
name: 'Rectangle',
|
name: 'Rectangle',
|
||||||
childIndex: 1,
|
childIndex: 1,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
|
@ -47,7 +47,7 @@ export default function Imperative(): JSX.Element {
|
||||||
|
|
||||||
const color = i % 2 ? ColorStyle.Red : ColorStyle.Blue
|
const color = i % 2 ? ColorStyle.Red : ColorStyle.Blue
|
||||||
|
|
||||||
state.patchShapes({
|
app.patchShapes({
|
||||||
id: 'rect1',
|
id: 'rect1',
|
||||||
style: {
|
style: {
|
||||||
...rect1.style,
|
...rect1.style,
|
||||||
|
@ -60,5 +60,5 @@ export default function Imperative(): JSX.Element {
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <TLDraw onMount={handleMount} />
|
return <Tldraw onMount={handleMount} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import * as React from 'react'
|
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 {
|
export default function Api(): JSX.Element {
|
||||||
const rTLDrawState = React.useRef<TLDrawState>()
|
const rTldrawApp = React.useRef<TldrawApp>()
|
||||||
|
|
||||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
const handleMount = React.useCallback((app: TldrawApp) => {
|
||||||
rTLDrawState.current = state
|
rTldrawApp.current = app
|
||||||
|
|
||||||
state
|
window.app = app
|
||||||
|
|
||||||
|
app
|
||||||
.createShapes({
|
.createShapes({
|
||||||
id: 'rect1',
|
id: 'rect1',
|
||||||
type: TLDrawShapeType.Rectangle,
|
type: TDShapeType.Rectangle,
|
||||||
point: [100, 100],
|
point: [100, 100],
|
||||||
size: [200, 200],
|
size: [200, 200],
|
||||||
})
|
})
|
||||||
|
@ -24,7 +28,7 @@ export default function Api(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw onMount={handleMount} />
|
<Tldraw onMount={handleMount} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,8 @@ import './styles.css'
|
||||||
export default function App(): JSX.Element {
|
export default function App(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<img className="hero" src="./card-repo.png" />
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/basic">
|
<Route path="/develop">
|
||||||
<Develop />
|
<Develop />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/basic">
|
<Route path="/basic">
|
||||||
|
@ -64,9 +63,10 @@ export default function App(): JSX.Element {
|
||||||
<Multiplayer />
|
<Multiplayer />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
|
<img className="hero" src="./card-repo.png" />
|
||||||
<ul className="links">
|
<ul className="links">
|
||||||
<li>
|
<li>
|
||||||
<Link to="/basic">Develop</Link>
|
<Link to="/develop">Develop</Link>
|
||||||
</li>
|
</li>
|
||||||
<hr />
|
<hr />
|
||||||
<li>
|
<li>
|
||||||
|
@ -94,10 +94,10 @@ export default function App(): JSX.Element {
|
||||||
<Link to="/controlled">Controlled via Props</Link>
|
<Link to="/controlled">Controlled via Props</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/api">Using the TLDrawState API</Link>
|
<Link to="/api">Using the TldrawApp API</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/imperative">Controlled via TLDrawState API</Link>
|
<Link to="/imperative">Controlled via TldrawApp API</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link to="/changing-id">Changing ID</Link>
|
<Link to="/changing-id">Changing ID</Link>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLDraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
|
|
||||||
export default function Basic(): JSX.Element {
|
export default function Basic(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw />
|
<Tldraw />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLDraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
|
|
||||||
export default function ChangingId() {
|
export default function ChangingId() {
|
||||||
const [id, setId] = React.useState('example')
|
const [id, setId] = React.useState('example')
|
||||||
|
@ -10,5 +10,5 @@ export default function ChangingId() {
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <TLDraw id={id} />
|
return <Tldraw id={id} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import * as React from 'react'
|
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 {
|
export default function Develop(): JSX.Element {
|
||||||
const rTLDrawState = React.useRef<TLDrawState>()
|
const rTldrawApp = React.useRef<TldrawApp>()
|
||||||
|
|
||||||
const fileSystemEvents = useFileSystem()
|
const fileSystemEvents = useFileSystem()
|
||||||
|
|
||||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
const handleMount = React.useCallback((app: TldrawApp) => {
|
||||||
window.state = state
|
window.app = app
|
||||||
rTLDrawState.current = state
|
rTldrawApp.current = app
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleSignOut = React.useCallback(() => {
|
const handleSignOut = React.useCallback(() => {
|
||||||
|
@ -28,13 +28,14 @@ export default function Develop(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw
|
<Tldraw
|
||||||
id="develop"
|
id="develop"
|
||||||
{...fileSystemEvents}
|
{...fileSystemEvents}
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onSignOut={handleSignOut}
|
onSignOut={handleSignOut}
|
||||||
onPersist={handlePersist}
|
onPersist={handlePersist}
|
||||||
|
showSponsorLink={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TLDraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
export default function Embedded(): JSX.Element {
|
export default function Embedded(): JSX.Element {
|
||||||
|
@ -13,7 +13,7 @@ export default function Embedded(): JSX.Element {
|
||||||
marginBottom: '32px',
|
marginBottom: '32px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TLDraw id="small5" />
|
<Tldraw id="small5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -24,7 +24,7 @@ export default function Embedded(): JSX.Element {
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TLDraw id="embedded" />
|
<Tldraw id="embedded" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLDraw, useFileSystem } from '@tldraw/tldraw'
|
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
||||||
|
|
||||||
export default function FileSystem(): JSX.Element {
|
export default function FileSystem(): JSX.Element {
|
||||||
const fileSystemEvents = useFileSystem()
|
const fileSystemEvents = useFileSystem()
|
||||||
|
@ -8,7 +8,7 @@ export default function FileSystem(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw {...fileSystemEvents} />
|
<Tldraw {...fileSystemEvents} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { TLDraw, TLDrawFile } from '@tldraw/tldraw'
|
import { Tldraw, TDFile } from '@tldraw/tldraw'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
export default function LoadingFiles(): JSX.Element {
|
export default function LoadingFiles(): JSX.Element {
|
||||||
const [file, setFile] = React.useState<TLDrawFile>()
|
const [file, setFile] = React.useState<TDFile>()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function loadFile(): Promise<void> {
|
async function loadFile(): Promise<void> {
|
||||||
|
@ -13,5 +13,5 @@ export default function LoadingFiles(): JSX.Element {
|
||||||
loadFile()
|
loadFile()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <TLDraw document={file?.document} />
|
return <Tldraw document={file?.document} />
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
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 { createClient, Presence } from '@liveblocks/client'
|
||||||
import { LiveblocksProvider, RoomProvider, useErrorListener, useObject } from '@liveblocks/react'
|
import { LiveblocksProvider, RoomProvider, useErrorListener, useObject } from '@liveblocks/react'
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
|
|
||||||
interface TLDrawUserPresence extends Presence {
|
declare const window: Window & { app: TldrawApp }
|
||||||
user: TLDrawUser
|
|
||||||
|
interface TDUserPresence extends Presence {
|
||||||
|
user: TDUser
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createClient({
|
const client = createClient({
|
||||||
|
@ -20,37 +22,34 @@ export function Multiplayer() {
|
||||||
return (
|
return (
|
||||||
<LiveblocksProvider client={client}>
|
<LiveblocksProvider client={client}>
|
||||||
<RoomProvider id={roomId}>
|
<RoomProvider id={roomId}>
|
||||||
<TLDrawWrapper />
|
<TldrawWrapper />
|
||||||
</RoomProvider>
|
</RoomProvider>
|
||||||
</LiveblocksProvider>
|
</LiveblocksProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TLDrawWrapper() {
|
function TldrawWrapper() {
|
||||||
const [docId] = React.useState(() => Utils.uniqueId())
|
const [docId] = React.useState(() => Utils.uniqueId())
|
||||||
|
|
||||||
const [error, setError] = React.useState<Error>()
|
const [error, setError] = React.useState<Error>()
|
||||||
|
|
||||||
const [state, setstate] = React.useState<TLDrawState>()
|
const [app, setApp] = React.useState<TldrawApp>()
|
||||||
|
|
||||||
useErrorListener((err) => setError(err))
|
useErrorListener((err) => setError(err))
|
||||||
|
|
||||||
const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', {
|
const doc = useObject<{ uuid: string; document: TDDocument }>('doc', {
|
||||||
uuid: docId,
|
uuid: docId,
|
||||||
document: {
|
document: {
|
||||||
...TLDrawState.defaultDocument,
|
...TldrawApp.defaultDocument,
|
||||||
id: roomId,
|
id: roomId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Put the state into the window, for debugging.
|
// Put the state into the window, for debugging.
|
||||||
const handleMount = React.useCallback(
|
const handleMount = React.useCallback(
|
||||||
(state: TLDrawState) => {
|
(app: TldrawApp) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
window.app = app
|
||||||
// @ts-ignore
|
setApp(app)
|
||||||
window.state = state
|
|
||||||
state.loadRoom(roomId)
|
|
||||||
setstate(state)
|
|
||||||
},
|
},
|
||||||
[roomId]
|
[roomId]
|
||||||
)
|
)
|
||||||
|
@ -60,17 +59,11 @@ function TLDrawWrapper() {
|
||||||
|
|
||||||
if (!room) return
|
if (!room) return
|
||||||
if (!doc) return
|
if (!doc) return
|
||||||
if (!state) return
|
if (!app) 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] })
|
|
||||||
|
|
||||||
// Subscribe to presence changes; when others change, update the state
|
// Subscribe to presence changes; when others change, update the state
|
||||||
room.subscribe<TLDrawUserPresence>('others', (others) => {
|
room.subscribe<TDUserPresence>('others', (others) => {
|
||||||
state.updateUsers(
|
app.updateUsers(
|
||||||
others
|
others
|
||||||
.toArray()
|
.toArray()
|
||||||
.filter((other) => other.presence)
|
.filter((other) => other.presence)
|
||||||
|
@ -81,23 +74,23 @@ function TLDrawWrapper() {
|
||||||
|
|
||||||
room.subscribe('event', (event) => {
|
room.subscribe('event', (event) => {
|
||||||
if (event.event?.name === 'exit') {
|
if (event.event?.name === 'exit') {
|
||||||
state.removeUser(event.event.userId)
|
app.removeUser(event.event.userId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleDocumentUpdates() {
|
function handleDocumentUpdates() {
|
||||||
if (!doc) return
|
if (!doc) return
|
||||||
if (!state) return
|
if (!app) return
|
||||||
if (!state.state.room) return
|
if (!app.room) return
|
||||||
|
|
||||||
const docObject = doc.toObject()
|
const docObject = doc.toObject()
|
||||||
|
|
||||||
// Only merge the change if it caused by someone else
|
// Only merge the change if it caused by someone else
|
||||||
if (docObject.uuid !== docId) {
|
if (docObject.uuid !== docId) {
|
||||||
state.mergeDocument(docObject.document)
|
app.mergeDocument(docObject.document)
|
||||||
} else {
|
} else {
|
||||||
state.updateUsers(
|
app.updateUsers(
|
||||||
Object.values(state.state.room.users).map((user) => {
|
Object.values(app.room.users).map((user) => {
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
selectedIds: user.selectedIds,
|
selectedIds: user.selectedIds,
|
||||||
|
@ -108,8 +101,8 @@ function TLDrawWrapper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExit() {
|
function handleExit() {
|
||||||
if (!(state && state.state.room)) return
|
if (!(app && app.room)) return
|
||||||
room?.broadcastEvent({ name: 'exit', userId: state.state.room.userId })
|
room?.broadcastEvent({ name: 'exit', userId: app.room.userId })
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('beforeunload', handleExit)
|
window.addEventListener('beforeunload', handleExit)
|
||||||
|
@ -121,26 +114,33 @@ function TLDrawWrapper() {
|
||||||
const newDocument = doc.toObject().document
|
const newDocument = doc.toObject().document
|
||||||
|
|
||||||
if (newDocument) {
|
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 () => {
|
return () => {
|
||||||
window.removeEventListener('beforeunload', handleExit)
|
window.removeEventListener('beforeunload', handleExit)
|
||||||
doc.unsubscribe(handleDocumentUpdates)
|
doc.unsubscribe(handleDocumentUpdates)
|
||||||
}
|
}
|
||||||
}, [doc, docId, state])
|
}, [doc, docId, app])
|
||||||
|
|
||||||
const handlePersist = React.useCallback(
|
const handlePersist = React.useCallback(
|
||||||
(state: TLDrawState) => {
|
(app: TldrawApp) => {
|
||||||
doc?.update({ uuid: docId, document: state.document })
|
doc?.update({ uuid: docId, document: app.document })
|
||||||
},
|
},
|
||||||
[docId, doc]
|
[docId, doc]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleUserChange = React.useCallback(
|
const handleUserChange = React.useCallback(
|
||||||
(state: TLDrawState, user: TLDrawUser) => {
|
(app: TldrawApp, user: TDUser) => {
|
||||||
const room = client.getRoom(roomId)
|
const room = client.getRoom(roomId)
|
||||||
room?.updatePresence({ id: state.state.room?.userId, user })
|
room?.updatePresence({ id: app.room?.userId, user })
|
||||||
},
|
},
|
||||||
[client]
|
[client]
|
||||||
)
|
)
|
||||||
|
@ -151,7 +151,7 @@ function TLDrawWrapper() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw
|
<Tldraw
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
onPersist={handlePersist}
|
onPersist={handlePersist}
|
||||||
onUserChange={handleUserChange}
|
onUserChange={handleUserChange}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { TLDraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
export default function NoSizeEmbedded(): JSX.Element {
|
export default function NoSizeEmbedded(): JSX.Element {
|
||||||
return <TLDraw />
|
return <Tldraw />
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLDraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
|
|
||||||
export default function Persisted(): JSX.Element {
|
export default function Persisted(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw id="tldraw-persisted-id" />
|
<Tldraw id="Tldraw-persisted-id" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {
|
import {
|
||||||
TLDraw,
|
Tldraw,
|
||||||
ColorStyle,
|
ColorStyle,
|
||||||
DashStyle,
|
DashStyle,
|
||||||
SizeStyle,
|
SizeStyle,
|
||||||
TLDrawDocument,
|
TDDocument,
|
||||||
TLDrawShapeType,
|
TDShapeType,
|
||||||
TLDrawState,
|
TldrawApp,
|
||||||
} from '@tldraw/tldraw'
|
} from '@tldraw/tldraw'
|
||||||
|
|
||||||
export default function Controlled() {
|
export default function Controlled() {
|
||||||
const rDocument = React.useRef<TLDrawDocument>({
|
const rDocument = React.useRef<TDDocument>({
|
||||||
name: 'New Document',
|
name: 'New Document',
|
||||||
version: TLDrawState.version,
|
version: TldrawApp.version,
|
||||||
id: 'doc',
|
id: 'doc',
|
||||||
pages: {
|
pages: {
|
||||||
page1: {
|
page1: {
|
||||||
|
@ -20,7 +20,7 @@ export default function Controlled() {
|
||||||
shapes: {
|
shapes: {
|
||||||
rect1: {
|
rect1: {
|
||||||
id: 'rect1',
|
id: 'rect1',
|
||||||
type: TLDrawShapeType.Rectangle,
|
type: TDShapeType.Rectangle,
|
||||||
parentId: 'page1',
|
parentId: 'page1',
|
||||||
name: 'Rectangle',
|
name: 'Rectangle',
|
||||||
childIndex: 1,
|
childIndex: 1,
|
||||||
|
@ -34,7 +34,7 @@ export default function Controlled() {
|
||||||
},
|
},
|
||||||
rect2: {
|
rect2: {
|
||||||
id: 'rect2',
|
id: 'rect2',
|
||||||
type: TLDrawShapeType.Rectangle,
|
type: TDShapeType.Rectangle,
|
||||||
parentId: 'page1',
|
parentId: 'page1',
|
||||||
name: 'Rectangle',
|
name: 'Rectangle',
|
||||||
childIndex: 1,
|
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(() => {
|
React.useEffect(() => {
|
||||||
let i = 0
|
let i = 0
|
||||||
|
@ -105,5 +105,5 @@ export default function Controlled() {
|
||||||
rDocument.current = state.document
|
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'
|
import * as React from 'react'
|
||||||
|
|
||||||
export default function ReadOnly(): JSX.Element {
|
export default function ReadOnly(): JSX.Element {
|
||||||
const [file, setFile] = React.useState<TLDrawFile>()
|
const [file, setFile] = React.useState<TDFile>()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
async function loadFile(): Promise<void> {
|
async function loadFile(): Promise<void> {
|
||||||
|
@ -15,7 +15,7 @@ export default function ReadOnly(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw readOnly document={file?.document} />
|
<Tldraw readOnly document={file?.document} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLDraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
|
|
||||||
export default function UIOptions(): JSX.Element {
|
export default function UIOptions(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<TLDraw
|
<Tldraw
|
||||||
showUI={true}
|
showUI={true}
|
||||||
showMenu={true}
|
showMenu={true}
|
||||||
showPages={true}
|
showPages={true}
|
||||||
|
|
|
@ -8,7 +8,7 @@ From the root folder:
|
||||||
|
|
||||||
- Open `localhost:5420` to view the example project.
|
- Open `localhost:5420` to view the example project.
|
||||||
|
|
||||||
**Note:** The multiplayer examples and endpoints currently require an API key from [Liveblocks](https://liveblocks.io/), however the storage services that are used in TLDraw are currently in alpha and (as of November 2021) not accessible to the general public. You won't be able to authenticate and run these parts of the project.
|
**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:
|
Other scripts:
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Introduction
|
## 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:
|
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:
|
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 |
|
| Prop | Type | Description |
|
||||||
| ----------------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
|
| ----------------- | ------------ | --------------------------------------------------------------------------------------------------------- |
|
||||||
| `id` | `string` | An id under which to persist the component's state. |
|
| `id` | `string` | An id under which to persist the component's state. |
|
||||||
| `document` | `TLDrawDocument` | An initial [`TLDrawDocument`](#tldrawdocument) object. |
|
| `document` | `TDDocument` | An initial [`TDDocument`](#TDDocument) object. |
|
||||||
| `currentPageId` | `string` | A current page id, referencing the `TLDrawDocument` object provided via the `document` prop. |
|
| `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. |
|
| `autofocus` | `boolean` | Whether the editor should immediately receive focus. Defaults to true. |
|
||||||
| `showMenu` | `boolean` | Whether to show the menu. |
|
| `showMenu` | `boolean` | Whether to show the menu. |
|
||||||
| `showPages` | `boolean` | Whether to show the pages menu. |
|
| `showPages` | `boolean` | Whether to show the pages menu. |
|
||||||
| `showStyles` | `boolean` | Whether to show the styles menu. |
|
| `showStyles` | `boolean` | Whether to show the styles menu. |
|
||||||
| `showTools` | `boolean` | Whether to show the tools. |
|
| `showTools` | `boolean` | Whether to show the tools. |
|
||||||
| `showUI` | `boolean` | Whether to show any UI other than the canvas. |
|
| `showUI` | `boolean` | Whether to show any UI other than the canvas. |
|
||||||
| `onMount` | `Function` | Called when the editor first mounts, receiving the current `TLDrawState`. |
|
| `showSponsorLink` | `boolean` | Whether to show a sponsor link. |
|
||||||
| `onPatch` | `Function` | Called when the state is updated via a patch. |
|
| `onMount` | `Function` | Called when the editor first mounts, receiving the current `TldrawApp`. |
|
||||||
| `onCommand` | `Function` | Called when the state is updated via a command. |
|
| `onPatch` | `Function` | Called when the state is updated via a patch. |
|
||||||
| `onPersist` | `Function` | Called when the state is persisted after an action. |
|
| `onCommand` | `Function` | Called when the state is updated via a command. |
|
||||||
| `onChange` | `Function` | Called when the `TLDrawState` updates for any reason. |
|
| `onPersist` | `Function` | Called when the state is persisted after an action. |
|
||||||
| `onUserChange` | `Function` | Called when the user's "presence" information changes. |
|
| `onChange` | `Function` | Called when the `TldrawApp` updates for any reason. |
|
||||||
| `onUndo` | `Function` | Called when the `TLDrawState` updates after an undo. |
|
| `onUserChange` | `Function` | Called when the user's "presence" information changes. |
|
||||||
| `onRedo` | `Function` | Called when the `TLDrawState` updates after a redo. |
|
| `onUndo` | `Function` | Called when the `TldrawApp` updates after an undo. |
|
||||||
| `onSignIn` | `Function` | Called when the user selects Sign In from the menu. |
|
| `onRedo` | `Function` | Called when the `TldrawApp` updates after a redo. |
|
||||||
| `onSignOut` | `Function` | Called when the user selects Sign Out from the menu. |
|
| `onSignIn` | `Function` | Called when the user selects Sign In from the menu. |
|
||||||
| `onNewProject` | `Function` | Called when the user when the user creates a new project through the menu or through a keyboard shortcut. |
|
| `onSignOut` | `Function` | Called when the user selects Sign Out from the menu. |
|
||||||
| `onSaveProject` | `Function` | Called when the user saves a project through the menu or through a keyboard shortcut. |
|
| `onNewProject` | `Function` | Called when the user when the user creates a new 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. |
|
| `onSaveProject` | `Function` | Called when the user saves a 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. |
|
| `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`.
|
> **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.
|
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
|
```ts
|
||||||
import { TLDraw, useFileSystem } from '@tldraw/tldraw'
|
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const fileSystemEvents = useFileSystem()
|
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
|
- `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
|
- `pageStates` - A table of `TLPageState` objects
|
||||||
- `version` - The document's version, used internally for migrations.
|
- `version` - The document's version, used internally for migrations.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { TLDrawDocument, TLDrawState } from '@tldraw/tldraw'
|
import { TDDocument, TldrawApp } from '@tldraw/tldraw'
|
||||||
|
|
||||||
const myDocument: TLDrawDocument = {
|
const myDocument: TDDocument = {
|
||||||
id: 'doc',
|
id: 'doc',
|
||||||
version: TLDrawState.version,
|
version: TldrawApp.version,
|
||||||
pages: {
|
pages: {
|
||||||
page1: {
|
page1: {
|
||||||
id: 'page1',
|
id: 'page1',
|
||||||
|
@ -95,34 +96,34 @@ const myDocument: TLDrawDocument = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
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.
|
**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
|
## 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 |
|
| Property | Type | Description |
|
||||||
| --------------------- | ---------------- | --------------------------------------------------------------- |
|
| --------------------- | ------------ | --------------------------------------------------------------- |
|
||||||
| `id` | `string` | A unique ID for the shape. |
|
| `id` | `string` | A unique ID for the shape. |
|
||||||
| `name` | `string` | The shape's name. |
|
| `name` | `string` | The shape's name. |
|
||||||
| `type` | `string` | The shape's type. |
|
| `type` | `string` | The shape's type. |
|
||||||
| `parentId` | `string` | The ID of the shape's parent (a shape or its page). |
|
| `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. |
|
| `childIndex` | `number` | The shape's order within its parent's children, indexed from 1. |
|
||||||
| `point` | `number[]` | The `[x, y]` position of the shape. |
|
| `point` | `number[]` | The `[x, y]` position of the shape. |
|
||||||
| `rotation` | `number[]` | (optional) The shape's rotation in radians. |
|
| `rotation` | `number[]` | (optional) The shape's rotation in radians. |
|
||||||
| `children` | `string[]` | (optional) The shape's child shape ids. |
|
| `children` | `string[]` | (optional) The shape's child shape ids. |
|
||||||
| `handles` | `TLDrawHandle{}` | (optional) A table of `TLHandle` objects. |
|
| `handles` | `TDHandle{}` | (optional) A table of `TLHandle` objects. |
|
||||||
| `isLocked` | `boolean` | (optional) True if the shape is locked. |
|
| `isLocked` | `boolean` | (optional) True if the shape is locked. |
|
||||||
| `isHidden` | `boolean` | (optional) True if the shape is hidden. |
|
| `isHidden` | `boolean` | (optional) True if the shape is hidden. |
|
||||||
| `isEditing` | `boolean` | (optional) True if the shape is currently editing. |
|
| `isEditing` | `boolean` | (optional) True if the shape is currently editing. |
|
||||||
| `isGenerated` | `boolean` | (optional) True if the shape is generated. |
|
| `isGenerated` | `boolean` | (optional) True if the shape is generated. |
|
||||||
| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. |
|
| `isAspectRatioLocked` | `boolean` | (optional) True if the shape's aspect ratio is locked. |
|
||||||
|
|
||||||
> **Important:** In order for re-ordering to work, a shape's `childIndex` values _must_ start from 1, not 0. The page or parent shape's "bottom-most" child should have a `childIndex` of 1.
|
> **Important:** In order for re-ordering to work, a shape's `childIndex` values _must_ start from 1, not 0. The page or parent shape's "bottom-most" child should have a `childIndex` of 1.
|
||||||
|
|
||||||
|
@ -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. |
|
| `distance` | `number` | The distance from the bound point. |
|
||||||
| `point` | `number[]` | A normalized point representing 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
|
```tsx
|
||||||
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
|
import { Tldraw, TldrawApp } from '@tldraw/tldraw'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const handleMount = React.useCallback((state: TLDrawState) => {
|
const handleMount = React.useCallback((state: TldrawApp) => {
|
||||||
state.selectAll()
|
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:
|
Here are some useful methods:
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"author": "@steveruizok",
|
"author": "@steveruizok",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tldraw/tldraw.git"
|
"url": "https://github.com/tldraw/tldraw.git"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"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
|
## 0.1.17
|
||||||
|
|
||||||
- Fixes "shifting" bug with drawing tool. Finally!
|
- Fixes "shifting" bug with drawing tool. Finally!
|
||||||
|
@ -61,7 +87,7 @@
|
||||||
|
|
||||||
- Extracted into its own repository (`tldraw/core`) and open sourced! :clap:
|
- Extracted into its own repository (`tldraw/core`) and open sourced! :clap:
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Updated with latest `@tldraw/core`, updated ShapeUtils API.
|
- Updated with latest `@tldraw/core`, updated ShapeUtils API.
|
||||||
|
|
||||||
|
@ -71,31 +97,31 @@
|
||||||
|
|
||||||
- Major change to ShapeUtils API.
|
- Major change to ShapeUtils API.
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Rewrite utils with new API.
|
- Rewrite utils with new API.
|
||||||
|
|
||||||
## 0.0.126
|
## 0.0.126
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Swap behavior of command and alt keys in arrow shapes.
|
- Swap behavior of command and alt keys in arrow shapes.
|
||||||
|
|
||||||
## 0.0.125
|
## 0.0.125
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Bug fixes.
|
- Bug fixes.
|
||||||
|
|
||||||
## 0.0.124
|
## 0.0.124
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Fix typings.
|
- Fix typings.
|
||||||
|
|
||||||
## 0.0.123
|
## 0.0.123
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Adds bound shape controls.
|
- Adds bound shape controls.
|
||||||
- Drag a bound shape control to translate shapes in that direction.
|
- Drag a bound shape control to translate shapes in that direction.
|
||||||
|
@ -104,7 +130,7 @@
|
||||||
|
|
||||||
## 0.0.122
|
## 0.0.122
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Adds snapping for transforming shapes.
|
- Adds snapping for transforming shapes.
|
||||||
|
|
||||||
|
@ -114,20 +140,20 @@
|
||||||
|
|
||||||
- Adds `snapLines`.
|
- Adds `snapLines`.
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Adds shape snapping while translating. Hold Command/Control to disable while dragging.
|
- Adds shape snapping while translating. Hold Command/Control to disable while dragging.
|
||||||
|
|
||||||
## 0.0.120
|
## 0.0.120
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Improves rectangle rendering.
|
- Improves rectangle rendering.
|
||||||
- Improves zoom to fit and zoom to selection.
|
- Improves zoom to fit and zoom to selection.
|
||||||
|
|
||||||
## 0.0.119
|
## 0.0.119
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Fixes bug with bound arrows after undo.
|
- Fixes bug with bound arrows after undo.
|
||||||
|
|
||||||
|
@ -137,7 +163,7 @@
|
||||||
|
|
||||||
- Improves multiplayer features.
|
- Improves multiplayer features.
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Fixes bugs in text, arrows, stickies.
|
- Fixes bugs in text, arrows, stickies.
|
||||||
- Adds start binding for new arrows.
|
- Adds start binding for new arrows.
|
||||||
|
@ -151,7 +177,7 @@
|
||||||
|
|
||||||
- Improves rendering on Safari.
|
- Improves rendering on Safari.
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Improves rendering on Safari.
|
- Improves rendering on Safari.
|
||||||
- Minor bug fixes around selection.
|
- Minor bug fixes around selection.
|
||||||
|
@ -164,32 +190,32 @@
|
||||||
- Adds [side cloning](https://github.com/tldraw/tldraw/pull/149).
|
- Adds [side cloning](https://github.com/tldraw/tldraw/pull/149).
|
||||||
- Improves rendering.
|
- Improves rendering.
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Adds sticky note [side cloning](https://github.com/tldraw/tldraw/pull/149).
|
- Adds sticky note [side cloning](https://github.com/tldraw/tldraw/pull/149).
|
||||||
|
|
||||||
## 0.0.114
|
## 0.0.114
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Improves fills for filled shapes.
|
- Improves fills for filled shapes.
|
||||||
|
|
||||||
## 0.0.113
|
## 0.0.113
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Improves grouping and ungrouping.
|
- Improves grouping and ungrouping.
|
||||||
|
|
||||||
## 0.0.112
|
## 0.0.112
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Fixes centering on embedded TLDraw components.
|
- Fixes centering on embedded tldraw components.
|
||||||
- Removes expensive calls to window.
|
- Removes expensive calls to window.
|
||||||
|
|
||||||
## 0.0.111
|
## 0.0.111
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Adjust stroke widths and sizes.
|
- Adjust stroke widths and sizes.
|
||||||
- Fixes a bug on very small dashed shapes.
|
- 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.
|
- Adds `user` and `users` props (optional) for multiplayer cursors. This feature is very lightly implemented.
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Adds multiplayer support.
|
- Adds multiplayer support.
|
||||||
- Adds `showMenu` and `showPages` props.
|
- Adds `showMenu` and `showPages` props.
|
||||||
|
@ -208,7 +234,7 @@
|
||||||
|
|
||||||
## 0.0.109
|
## 0.0.109
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Bumps perfect-freehand
|
- Bumps perfect-freehand
|
||||||
- Fixes dots for small sized draw shapes
|
- Fixes dots for small sized draw shapes
|
||||||
|
@ -217,6 +243,6 @@
|
||||||
|
|
||||||
- Adds CHANGELOG. Only 108 releases late!
|
- Adds CHANGELOG. Only 108 releases late!
|
||||||
|
|
||||||
### TLDraw
|
### tldraw
|
||||||
|
|
||||||
- Fixes a bug with bounding boxes on arrows.
|
- Fixes a bug with bounding boxes on arrows.
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
|
|
||||||
# @tldraw/tldraw
|
# @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).
|
💕 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",
|
"author": "@steveruizok",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/tldraw/tldraw.git"
|
"url": "https://github.com/tldraw/tldraw.git"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -59,4 +59,4 @@
|
||||||
"tsconfig-replace-paths": "^0.0.5"
|
"tsconfig-replace-paths": "^0.0.5"
|
||||||
},
|
},
|
||||||
"gitHead": "083b36e167b6911927a6b58cbbb830b11b33f00a"
|
"gitHead": "083b36e167b6911927a6b58cbbb830b11b33f00a"
|
||||||
}
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { render, waitFor } from '@testing-library/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 () => {
|
test('mounts component and calls onMount', async () => {
|
||||||
const onMount = jest.fn()
|
const onMount = jest.fn()
|
||||||
render(<TLDraw onMount={onMount} />)
|
render(<Tldraw onMount={onMount} />)
|
||||||
await waitFor(onMount)
|
await waitFor(onMount)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('mounts component and calls onMount when id is present', async () => {
|
test('mounts component and calls onMount when id is present', async () => {
|
||||||
const onMount = jest.fn()
|
const onMount = jest.fn()
|
||||||
render(<TLDraw id="someId" onMount={onMount} />)
|
render(<Tldraw id="someId" onMount={onMount} />)
|
||||||
await waitFor(onMount)
|
await waitFor(onMount)
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -2,15 +2,9 @@ import * as React from 'react'
|
||||||
import { IdProvider } from '@radix-ui/react-id'
|
import { IdProvider } from '@radix-ui/react-id'
|
||||||
import { Renderer } from '@tldraw/core'
|
import { Renderer } from '@tldraw/core'
|
||||||
import { styled, dark } from '~styles'
|
import { styled, dark } from '~styles'
|
||||||
import { TLDrawSnapshot, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types'
|
import { TDDocument, TDStatus, TDUser } from '~types'
|
||||||
import { TLDrawCallbacks, TLDrawState } from '~state'
|
import { TldrawApp, TDCallbacks } from '~state'
|
||||||
import {
|
import { TldrawContext, useStylesheet, useKeyboardShortcuts, useTldrawApp } from '~hooks'
|
||||||
TLDrawContext,
|
|
||||||
TLDrawContextType,
|
|
||||||
useStylesheet,
|
|
||||||
useKeyboardShortcuts,
|
|
||||||
useTLDrawContext,
|
|
||||||
} from '~hooks'
|
|
||||||
import { shapeUtils } from '~state/shapes'
|
import { shapeUtils } from '~state/shapes'
|
||||||
import { ToolsPanel } from '~components/ToolsPanel'
|
import { ToolsPanel } from '~components/ToolsPanel'
|
||||||
import { TopPanel } from '~components/TopPanel'
|
import { TopPanel } from '~components/TopPanel'
|
||||||
|
@ -18,29 +12,7 @@ import { TLDR } from '~state/TLDR'
|
||||||
import { ContextMenu } from '~components/ContextMenu'
|
import { ContextMenu } from '~components/ContextMenu'
|
||||||
import { FocusButton } from '~components/FocusButton/FocusButton'
|
import { FocusButton } from '~components/FocusButton/FocusButton'
|
||||||
|
|
||||||
// Selectors
|
export interface TldrawProps extends TDCallbacks {
|
||||||
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 {
|
|
||||||
/**
|
/**
|
||||||
* (optional) If provided, the component will load / persist state under this key.
|
* (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.
|
* (optional) The document to load or update from.
|
||||||
*/
|
*/
|
||||||
document?: TLDrawDocument
|
document?: TDDocument
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (optional) The current page id.
|
* (optional) The current page id.
|
||||||
|
@ -86,6 +58,11 @@ export interface TLDrawProps extends TLDrawCallbacks {
|
||||||
*/
|
*/
|
||||||
showTools?: boolean
|
showTools?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional) Whether to show a sponsor link for Tldraw.
|
||||||
|
*/
|
||||||
|
showSponsorLink?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (optional) Whether to show the UI.
|
* (optional) Whether to show the UI.
|
||||||
*/
|
*/
|
||||||
|
@ -96,69 +73,81 @@ export interface TLDrawProps extends TLDrawCallbacks {
|
||||||
*/
|
*/
|
||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional) Whether to to show the app's dark mode UI.
|
||||||
|
*/
|
||||||
|
darkMode?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (optional) A callback to run when the component mounts.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (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.
|
* (optional) A callback to run when the user redos.
|
||||||
*/
|
*/
|
||||||
onRedo?: (state: TLDrawState) => void
|
onRedo?: (state: TldrawApp) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TLDraw({
|
export function Tldraw({
|
||||||
id,
|
id,
|
||||||
document,
|
document,
|
||||||
currentPageId,
|
currentPageId,
|
||||||
|
darkMode = false,
|
||||||
autofocus = true,
|
autofocus = true,
|
||||||
showMenu = true,
|
showMenu = true,
|
||||||
showPages = true,
|
showPages = true,
|
||||||
|
@ -167,6 +156,7 @@ export function TLDraw({
|
||||||
showStyles = true,
|
showStyles = true,
|
||||||
showUI = true,
|
showUI = true,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
|
showSponsorLink = false,
|
||||||
onMount,
|
onMount,
|
||||||
onChange,
|
onChange,
|
||||||
onUserChange,
|
onUserChange,
|
||||||
|
@ -181,12 +171,13 @@ export function TLDraw({
|
||||||
onPersist,
|
onPersist,
|
||||||
onPatch,
|
onPatch,
|
||||||
onCommand,
|
onCommand,
|
||||||
}: TLDrawProps) {
|
}: TldrawProps) {
|
||||||
const [sId, setSId] = React.useState(id)
|
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,
|
onMount,
|
||||||
onChange,
|
onChange,
|
||||||
onUserChange,
|
onUserChange,
|
||||||
|
@ -204,15 +195,11 @@ export function TLDraw({
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const [context, setContext] = React.useState<TLDrawContextType>(() => ({
|
// Create a new app if the `id` prop changes.
|
||||||
state,
|
|
||||||
useSelector: state.useStore,
|
|
||||||
}))
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (id === sId) return
|
if (id === sId) return
|
||||||
|
|
||||||
const newState = new TLDrawState(id, {
|
const newApp = new TldrawApp(id, {
|
||||||
onMount,
|
onMount,
|
||||||
onChange,
|
onChange,
|
||||||
onUserChange,
|
onUserChange,
|
||||||
|
@ -231,31 +218,42 @@ export function TLDraw({
|
||||||
|
|
||||||
setSId(id)
|
setSId(id)
|
||||||
|
|
||||||
setContext((ctx) => ({
|
setApp(newApp)
|
||||||
...ctx,
|
|
||||||
state: newState,
|
|
||||||
useSelector: newState.useStore,
|
|
||||||
}))
|
|
||||||
|
|
||||||
setState(newState)
|
|
||||||
}, [sId, id])
|
}, [sId, id])
|
||||||
|
|
||||||
React.useEffect(() => {
|
// Update the document if the `document` prop changes but the ids,
|
||||||
state.readOnly = readOnly
|
// are the same, or else load a new document if the ids are different.
|
||||||
}, [state, readOnly])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!document) return
|
if (!document) return
|
||||||
|
|
||||||
if (document.id === state.document.id) {
|
if (document.id === app.document.id) {
|
||||||
state.updateDocument(document)
|
app.updateDocument(document)
|
||||||
} else {
|
} else {
|
||||||
state.loadDocument(document)
|
app.loadDocument(document)
|
||||||
}
|
}
|
||||||
}, [document, state])
|
}, [document, app])
|
||||||
|
|
||||||
|
// Change the page when the `currentPageId` prop changes
|
||||||
React.useEffect(() => {
|
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,
|
onMount,
|
||||||
onChange,
|
onChange,
|
||||||
onUserChange,
|
onUserChange,
|
||||||
|
@ -272,7 +270,7 @@ export function TLDraw({
|
||||||
onPersist,
|
onPersist,
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
state,
|
app,
|
||||||
onMount,
|
onMount,
|
||||||
onChange,
|
onChange,
|
||||||
onUserChange,
|
onUserChange,
|
||||||
|
@ -291,12 +289,11 @@ export function TLDraw({
|
||||||
|
|
||||||
// Use the `key` to ensure that new selector hooks are made when the id changes
|
// Use the `key` to ensure that new selector hooks are made when the id changes
|
||||||
return (
|
return (
|
||||||
<TLDrawContext.Provider value={context}>
|
<TldrawContext.Provider value={app}>
|
||||||
<IdProvider>
|
<IdProvider>
|
||||||
<InnerTLDraw
|
<InnerTldraw
|
||||||
key={sId || 'tldraw'}
|
key={sId || 'Tldraw'}
|
||||||
id={sId}
|
id={sId}
|
||||||
currentPageId={currentPageId}
|
|
||||||
autofocus={autofocus}
|
autofocus={autofocus}
|
||||||
showPages={showPages}
|
showPages={showPages}
|
||||||
showMenu={showMenu}
|
showMenu={showMenu}
|
||||||
|
@ -304,16 +301,16 @@ export function TLDraw({
|
||||||
showZoom={showZoom}
|
showZoom={showZoom}
|
||||||
showTools={showTools}
|
showTools={showTools}
|
||||||
showUI={showUI}
|
showUI={showUI}
|
||||||
|
showSponsorLink={showSponsorLink}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
</IdProvider>
|
</IdProvider>
|
||||||
</TLDrawContext.Provider>
|
</TldrawContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InnerTLDrawProps {
|
interface InnerTldrawProps {
|
||||||
id?: string
|
id?: string
|
||||||
currentPageId?: string
|
|
||||||
autofocus: boolean
|
autofocus: boolean
|
||||||
showPages: boolean
|
showPages: boolean
|
||||||
showMenu: boolean
|
showMenu: boolean
|
||||||
|
@ -321,44 +318,49 @@ interface InnerTLDrawProps {
|
||||||
showStyles: boolean
|
showStyles: boolean
|
||||||
showUI: boolean
|
showUI: boolean
|
||||||
showTools: boolean
|
showTools: boolean
|
||||||
|
showSponsorLink: boolean
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const InnerTLDraw = React.memo(function InnerTLDraw({
|
const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
id,
|
id,
|
||||||
currentPageId,
|
|
||||||
autofocus,
|
autofocus,
|
||||||
showPages,
|
showPages,
|
||||||
showMenu,
|
showMenu,
|
||||||
showZoom,
|
showZoom,
|
||||||
showStyles,
|
showStyles,
|
||||||
showTools,
|
showTools,
|
||||||
|
showSponsorLink,
|
||||||
readOnly,
|
readOnly,
|
||||||
showUI,
|
showUI,
|
||||||
}: InnerTLDrawProps) {
|
}: InnerTldrawProps) {
|
||||||
const { state, useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const rWrapper = React.useRef<HTMLDivElement>(null)
|
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 = app.session !== undefined
|
||||||
|
|
||||||
const isInSession = state.session !== undefined
|
|
||||||
|
|
||||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||||
const hideBounds =
|
const hideBounds =
|
||||||
(isInSession && state.session?.constructor.name !== 'BrushSession') ||
|
(isInSession && app.session?.constructor.name !== 'BrushSession') ||
|
||||||
!isSelecting ||
|
!isSelecting ||
|
||||||
isHideBoundsShape ||
|
isHideBoundsShape ||
|
||||||
!!pageState.editingId
|
!!pageState.editingId
|
||||||
|
@ -368,10 +370,12 @@ const InnerTLDraw = React.memo(function InnerTLDraw({
|
||||||
|
|
||||||
// Hide indicators when not using the select tool, or when in session
|
// Hide indicators when not using the select tool, or when in session
|
||||||
const hideIndicators =
|
const hideIndicators =
|
||||||
(isInSession && state.appState.status !== TLDrawStatus.Brushing) || !isSelecting
|
(isInSession && state.appState.status !== TDStatus.Brushing) || !isSelecting
|
||||||
|
|
||||||
// Custom rendering meta, with dark mode for shapes
|
// 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
|
// Custom theme, based on darkmode
|
||||||
const theme = React.useMemo(() => {
|
const theme = React.useMemo(() => {
|
||||||
|
@ -381,7 +385,7 @@ const InnerTLDraw = React.memo(function InnerTLDraw({
|
||||||
brushStroke: 'rgba(180, 180, 180, .25)',
|
brushStroke: 'rgba(180, 180, 180, .25)',
|
||||||
selected: 'rgba(38, 150, 255, 1.000)',
|
selected: 'rgba(38, 150, 255, 1.000)',
|
||||||
selectFill: 'rgba(38, 150, 255, 0.05)',
|
selectFill: 'rgba(38, 150, 255, 0.05)',
|
||||||
background: '#343d45',
|
background: '#212529',
|
||||||
foreground: '#49555f',
|
foreground: '#49555f',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -389,11 +393,6 @@ const InnerTLDraw = React.memo(function InnerTLDraw({
|
||||||
return {}
|
return {}
|
||||||
}, [settings.isDarkMode])
|
}, [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
|
// 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
|
// 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.
|
// 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}
|
shapeUtils={shapeUtils}
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
snapLines={snapLines}
|
snapLines={appState.snapLines}
|
||||||
users={users}
|
users={room?.users}
|
||||||
userId={state.state.room?.userId}
|
userId={room?.userId}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
meta={meta}
|
meta={meta}
|
||||||
hideBounds={hideBounds}
|
hideBounds={hideBounds}
|
||||||
hideHandles={hideHandles}
|
hideHandles={hideHandles}
|
||||||
|
hideResizeHandles={isHideResizeHandlesShape}
|
||||||
hideIndicators={hideIndicators}
|
hideIndicators={hideIndicators}
|
||||||
hideBindingHandles={!settings.showBindingHandles}
|
hideBindingHandles={!settings.showBindingHandles}
|
||||||
hideCloneHandles={!settings.showCloneHandles}
|
hideCloneHandles={!settings.showCloneHandles}
|
||||||
hideRotateHandles={!settings.showRotateHandles}
|
hideRotateHandles={!settings.showRotateHandles}
|
||||||
onPinchStart={state.onPinchStart}
|
onPinchStart={app.onPinchStart}
|
||||||
onPinchEnd={state.onPinchEnd}
|
onPinchEnd={app.onPinchEnd}
|
||||||
onPinch={state.onPinch}
|
onPinch={app.onPinch}
|
||||||
onPan={state.onPan}
|
onPan={app.onPan}
|
||||||
onZoom={state.onZoom}
|
onZoom={app.onZoom}
|
||||||
onPointerDown={state.onPointerDown}
|
onPointerDown={app.onPointerDown}
|
||||||
onPointerMove={state.onPointerMove}
|
onPointerMove={app.onPointerMove}
|
||||||
onPointerUp={state.onPointerUp}
|
onPointerUp={app.onPointerUp}
|
||||||
onPointCanvas={state.onPointCanvas}
|
onPointCanvas={app.onPointCanvas}
|
||||||
onDoubleClickCanvas={state.onDoubleClickCanvas}
|
onDoubleClickCanvas={app.onDoubleClickCanvas}
|
||||||
onRightPointCanvas={state.onRightPointCanvas}
|
onRightPointCanvas={app.onRightPointCanvas}
|
||||||
onDragCanvas={state.onDragCanvas}
|
onDragCanvas={app.onDragCanvas}
|
||||||
onReleaseCanvas={state.onReleaseCanvas}
|
onReleaseCanvas={app.onReleaseCanvas}
|
||||||
onPointShape={state.onPointShape}
|
onPointShape={app.onPointShape}
|
||||||
onDoubleClickShape={state.onDoubleClickShape}
|
onDoubleClickShape={app.onDoubleClickShape}
|
||||||
onRightPointShape={state.onRightPointShape}
|
onRightPointShape={app.onRightPointShape}
|
||||||
onDragShape={state.onDragShape}
|
onDragShape={app.onDragShape}
|
||||||
onHoverShape={state.onHoverShape}
|
onHoverShape={app.onHoverShape}
|
||||||
onUnhoverShape={state.onUnhoverShape}
|
onUnhoverShape={app.onUnhoverShape}
|
||||||
onReleaseShape={state.onReleaseShape}
|
onReleaseShape={app.onReleaseShape}
|
||||||
onPointBounds={state.onPointBounds}
|
onPointBounds={app.onPointBounds}
|
||||||
onDoubleClickBounds={state.onDoubleClickBounds}
|
onDoubleClickBounds={app.onDoubleClickBounds}
|
||||||
onRightPointBounds={state.onRightPointBounds}
|
onRightPointBounds={app.onRightPointBounds}
|
||||||
onDragBounds={state.onDragBounds}
|
onDragBounds={app.onDragBounds}
|
||||||
onHoverBounds={state.onHoverBounds}
|
onHoverBounds={app.onHoverBounds}
|
||||||
onUnhoverBounds={state.onUnhoverBounds}
|
onUnhoverBounds={app.onUnhoverBounds}
|
||||||
onReleaseBounds={state.onReleaseBounds}
|
onReleaseBounds={app.onReleaseBounds}
|
||||||
onPointBoundsHandle={state.onPointBoundsHandle}
|
onPointBoundsHandle={app.onPointBoundsHandle}
|
||||||
onDoubleClickBoundsHandle={state.onDoubleClickBoundsHandle}
|
onDoubleClickBoundsHandle={app.onDoubleClickBoundsHandle}
|
||||||
onRightPointBoundsHandle={state.onRightPointBoundsHandle}
|
onRightPointBoundsHandle={app.onRightPointBoundsHandle}
|
||||||
onDragBoundsHandle={state.onDragBoundsHandle}
|
onDragBoundsHandle={app.onDragBoundsHandle}
|
||||||
onHoverBoundsHandle={state.onHoverBoundsHandle}
|
onHoverBoundsHandle={app.onHoverBoundsHandle}
|
||||||
onUnhoverBoundsHandle={state.onUnhoverBoundsHandle}
|
onUnhoverBoundsHandle={app.onUnhoverBoundsHandle}
|
||||||
onReleaseBoundsHandle={state.onReleaseBoundsHandle}
|
onReleaseBoundsHandle={app.onReleaseBoundsHandle}
|
||||||
onPointHandle={state.onPointHandle}
|
onPointHandle={app.onPointHandle}
|
||||||
onDoubleClickHandle={state.onDoubleClickHandle}
|
onDoubleClickHandle={app.onDoubleClickHandle}
|
||||||
onRightPointHandle={state.onRightPointHandle}
|
onRightPointHandle={app.onRightPointHandle}
|
||||||
onDragHandle={state.onDragHandle}
|
onDragHandle={app.onDragHandle}
|
||||||
onHoverHandle={state.onHoverHandle}
|
onHoverHandle={app.onHoverHandle}
|
||||||
onUnhoverHandle={state.onUnhoverHandle}
|
onUnhoverHandle={app.onUnhoverHandle}
|
||||||
onReleaseHandle={state.onReleaseHandle}
|
onReleaseHandle={app.onReleaseHandle}
|
||||||
onError={state.onError}
|
onError={app.onError}
|
||||||
onRenderCountChange={state.onRenderCountChange}
|
onRenderCountChange={app.onRenderCountChange}
|
||||||
onShapeChange={state.onShapeChange}
|
onShapeChange={app.onShapeChange}
|
||||||
onShapeBlur={state.onShapeBlur}
|
onShapeBlur={app.onShapeBlur}
|
||||||
onShapeClone={state.onShapeClone}
|
onShapeClone={app.onShapeClone}
|
||||||
onBoundsChange={state.updateBounds}
|
onBoundsChange={app.updateBounds}
|
||||||
onKeyDown={state.onKeyDown}
|
onKeyDown={app.onKeyDown}
|
||||||
onKeyUp={state.onKeyUp}
|
onKeyUp={app.onKeyUp}
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{showUI && (
|
{showUI && (
|
||||||
<StyledUI>
|
<StyledUI>
|
||||||
{settings.isFocusMode ? (
|
{settings.isFocusMode ? (
|
||||||
<FocusButton onSelect={state.toggleFocusMode} />
|
<FocusButton onSelect={app.toggleFocusMode} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<TopPanel
|
<TopPanel
|
||||||
|
@ -489,6 +489,7 @@ const InnerTLDraw = React.memo(function InnerTLDraw({
|
||||||
showMenu={showMenu}
|
showMenu={showMenu}
|
||||||
showStyles={showStyles}
|
showStyles={showStyles}
|
||||||
showZoom={showZoom}
|
showZoom={showZoom}
|
||||||
|
showSponsorLink={showSponsorLink}
|
||||||
/>
|
/>
|
||||||
<StyledSpacer />
|
<StyledSpacer />
|
||||||
{showTools && !readOnly && <ToolsPanel />}
|
{showTools && !readOnly && <ToolsPanel />}
|
||||||
|
@ -539,6 +540,13 @@ const StyledLayout = styled('div', {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
zIndex: 1,
|
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', {
|
const StyledUI = styled('div', {
|
|
@ -2,9 +2,9 @@ import * as React from 'react'
|
||||||
import { ContextMenuItem } from '@radix-ui/react-context-menu'
|
import { ContextMenuItem } from '@radix-ui/react-context-menu'
|
||||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||||
|
|
||||||
export const CMRowButton = ({ onSelect, ...rest }: RowButtonProps) => {
|
export const CMRowButton = ({ ...rest }: RowButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<ContextMenuItem asChild onSelect={onSelect}>
|
<ContextMenuItem asChild>
|
||||||
<RowButton {...rest} />
|
<RowButton {...rest} />
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import * as RadixContextMenu from '@radix-ui/react-context-menu'
|
import * as RadixContextMenu from '@radix-ui/react-context-menu'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { TLDrawSnapshot, AlignType, DistributeType, StretchType } from '~types'
|
import { TDSnapshot, AlignType, DistributeType, StretchType } from '~types'
|
||||||
import {
|
import {
|
||||||
AlignBottomIcon,
|
AlignBottomIcon,
|
||||||
AlignCenterHorizontallyIcon,
|
AlignCenterHorizontallyIcon,
|
||||||
|
@ -21,21 +21,21 @@ import { CMTriggerButton } from './CMTriggerButton'
|
||||||
import { Divider } from '~components/Divider'
|
import { Divider } from '~components/Divider'
|
||||||
import { MenuContent } from '~components/MenuContent'
|
import { MenuContent } from '~components/MenuContent'
|
||||||
|
|
||||||
const has1SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
const has1SelectedIdsSelector = (s: TDSnapshot) => {
|
||||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
|
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
|
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
|
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDebugModeSelector = (s: TLDrawSnapshot) => {
|
const isDebugModeSelector = (s: TDSnapshot) => {
|
||||||
return s.settings.isDebugMode
|
return s.settings.isDebugMode
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasGroupSelectedSelector = (s: TLDrawSnapshot) => {
|
const hasGroupSelectedSelector = (s: TDSnapshot) => {
|
||||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
|
return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
|
||||||
(id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined
|
(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 => {
|
export const ContextMenu = ({ onBlur, children }: ContextMenuProps): JSX.Element => {
|
||||||
const { state, useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
const hasSelection = useSelector(has1SelectedIdsSelector)
|
const hasSelection = app.useStore(has1SelectedIdsSelector)
|
||||||
const hasTwoOrMore = useSelector(has2SelectedIdsSelector)
|
const hasTwoOrMore = app.useStore(has2SelectedIdsSelector)
|
||||||
const hasThreeOrMore = useSelector(has3SelectedIdsSelector)
|
const hasThreeOrMore = app.useStore(has3SelectedIdsSelector)
|
||||||
const isDebugMode = useSelector(isDebugModeSelector)
|
const isDebugMode = app.useStore(isDebugModeSelector)
|
||||||
const hasGroupSelected = useSelector(hasGroupSelectedSelector)
|
const hasGroupSelected = app.useStore(hasGroupSelectedSelector)
|
||||||
|
|
||||||
const rContent = React.useRef<HTMLDivElement>(null)
|
const rContent = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const handleFlipHorizontal = React.useCallback(() => {
|
const handleFlipHorizontal = React.useCallback(() => {
|
||||||
state.flipHorizontal()
|
app.flipHorizontal()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleFlipVertical = React.useCallback(() => {
|
const handleFlipVertical = React.useCallback(() => {
|
||||||
state.flipVertical()
|
app.flipVertical()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleDuplicate = React.useCallback(() => {
|
const handleDuplicate = React.useCallback(() => {
|
||||||
state.duplicate()
|
app.duplicate()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
|
const handleLock = React.useCallback(() => {
|
||||||
|
app.toggleLocked()
|
||||||
|
}, [app])
|
||||||
|
|
||||||
const handleGroup = React.useCallback(() => {
|
const handleGroup = React.useCallback(() => {
|
||||||
state.group()
|
app.group()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleMoveToBack = React.useCallback(() => {
|
const handleMoveToBack = React.useCallback(() => {
|
||||||
state.moveToBack()
|
app.moveToBack()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleMoveBackward = React.useCallback(() => {
|
const handleMoveBackward = React.useCallback(() => {
|
||||||
state.moveBackward()
|
app.moveBackward()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleMoveForward = React.useCallback(() => {
|
const handleMoveForward = React.useCallback(() => {
|
||||||
state.moveForward()
|
app.moveForward()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleMoveToFront = React.useCallback(() => {
|
const handleMoveToFront = React.useCallback(() => {
|
||||||
state.moveToFront()
|
app.moveToFront()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleDelete = React.useCallback(() => {
|
const handleDelete = React.useCallback(() => {
|
||||||
state.delete()
|
app.delete()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleCopyJson = React.useCallback(() => {
|
const handleCopyJson = React.useCallback(() => {
|
||||||
state.copyJson()
|
app.copyJson()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleCopy = React.useCallback(() => {
|
const handleCopy = React.useCallback(() => {
|
||||||
state.copy()
|
app.copy()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handlePaste = React.useCallback(() => {
|
const handlePaste = React.useCallback(() => {
|
||||||
state.paste()
|
app.paste()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleCopySvg = React.useCallback(() => {
|
const handleCopySvg = React.useCallback(() => {
|
||||||
state.copySvg()
|
app.copySvg()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleUndo = React.useCallback(() => {
|
const handleUndo = React.useCallback(() => {
|
||||||
state.undo()
|
app.undo()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleRedo = React.useCallback(() => {
|
const handleRedo = React.useCallback(() => {
|
||||||
state.redo()
|
app.redo()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadixContextMenu.Root>
|
<RadixContextMenu.Root dir="ltr">
|
||||||
<RadixContextMenu.Trigger dir="ltr">{children}</RadixContextMenu.Trigger>
|
<RadixContextMenu.Trigger dir="ltr">{children}</RadixContextMenu.Trigger>
|
||||||
<RadixContextMenu.Content
|
<RadixContextMenu.Content
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
@ -132,38 +136,41 @@ export const ContextMenu = ({ onBlur, children }: ContextMenuProps): JSX.Element
|
||||||
<MenuContent>
|
<MenuContent>
|
||||||
{hasSelection ? (
|
{hasSelection ? (
|
||||||
<>
|
<>
|
||||||
<CMRowButton onSelect={handleFlipHorizontal} kbd="⇧H">
|
<CMRowButton onClick={handleDuplicate} kbd="#D">
|
||||||
|
Duplicate
|
||||||
|
</CMRowButton>
|
||||||
|
<CMRowButton onClick={handleFlipHorizontal} kbd="⇧H">
|
||||||
Flip Horizontal
|
Flip Horizontal
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
<CMRowButton onSelect={handleFlipVertical} kbd="⇧V">
|
<CMRowButton onClick={handleFlipVertical} kbd="⇧V">
|
||||||
Flip Vertical
|
Flip Vertical
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
<CMRowButton onSelect={handleDuplicate} kbd="#D">
|
<CMRowButton onClick={handleLock} kbd="#⇧L">
|
||||||
Duplicate
|
Lock / Unlock
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
{(hasTwoOrMore || hasGroupSelected) && <Divider />}
|
{(hasTwoOrMore || hasGroupSelected) && <Divider />}
|
||||||
{hasTwoOrMore && (
|
{hasTwoOrMore && (
|
||||||
<CMRowButton onSelect={handleGroup} kbd="#G">
|
<CMRowButton onClick={handleGroup} kbd="#G">
|
||||||
Group
|
Group
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
)}
|
)}
|
||||||
{hasGroupSelected && (
|
{hasGroupSelected && (
|
||||||
<CMRowButton onSelect={handleGroup} kbd="#⇧G">
|
<CMRowButton onClick={handleGroup} kbd="#⇧G">
|
||||||
Ungroup
|
Ungroup
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
<ContextMenuSubMenu label="Move">
|
<ContextMenuSubMenu label="Move">
|
||||||
<CMRowButton onSelect={handleMoveToFront} kbd="⇧]">
|
<CMRowButton onClick={handleMoveToFront} kbd="⇧]">
|
||||||
To Front
|
To Front
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
<CMRowButton onSelect={handleMoveForward} kbd="]">
|
<CMRowButton onClick={handleMoveForward} kbd="]">
|
||||||
Forward
|
Forward
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
<CMRowButton onSelect={handleMoveBackward} kbd="[">
|
<CMRowButton onClick={handleMoveBackward} kbd="[">
|
||||||
Backward
|
Backward
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
<CMRowButton onSelect={handleMoveToBack} kbd="⇧[">
|
<CMRowButton onClick={handleMoveToBack} kbd="⇧[">
|
||||||
To Back
|
To Back
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
</ContextMenuSubMenu>
|
</ContextMenuSubMenu>
|
||||||
|
@ -175,30 +182,30 @@ export const ContextMenu = ({ onBlur, children }: ContextMenuProps): JSX.Element
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
<CMRowButton onSelect={handleCopy} kbd="#C">
|
<CMRowButton onClick={handleCopy} kbd="#C">
|
||||||
Copy
|
Copy
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
<CMRowButton onSelect={handleCopySvg} kbd="⇧#C">
|
<CMRowButton onClick={handleCopySvg} kbd="⇧#C">
|
||||||
Copy as SVG
|
Copy as SVG
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
{isDebugMode && <CMRowButton onSelect={handleCopyJson}>Copy as JSON</CMRowButton>}
|
{isDebugMode && <CMRowButton onClick={handleCopyJson}>Copy as JSON</CMRowButton>}
|
||||||
<CMRowButton onSelect={handlePaste} kbd="#V">
|
<CMRowButton onClick={handlePaste} kbd="#V">
|
||||||
Paste
|
Paste
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
<Divider />
|
<Divider />
|
||||||
<CMRowButton onSelect={handleDelete} kbd="⌫">
|
<CMRowButton onClick={handleDelete} kbd="⌫">
|
||||||
Delete
|
Delete
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CMRowButton onSelect={handlePaste} kbd="#V">
|
<CMRowButton onClick={handlePaste} kbd="#V">
|
||||||
Paste
|
Paste
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
<CMRowButton onSelect={handleUndo} kbd="#Z">
|
<CMRowButton onClick={handleUndo} kbd="#Z">
|
||||||
Undo
|
Undo
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
<CMRowButton onSelect={handleRedo} kbd="#⇧Z">
|
<CMRowButton onClick={handleRedo} kbd="#⇧Z">
|
||||||
Redo
|
Redo
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
</>
|
</>
|
||||||
|
@ -215,84 +222,84 @@ function AlignDistributeSubMenu({
|
||||||
hasTwoOrMore: boolean
|
hasTwoOrMore: boolean
|
||||||
hasThreeOrMore: boolean
|
hasThreeOrMore: boolean
|
||||||
}) {
|
}) {
|
||||||
const { state } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const alignTop = React.useCallback(() => {
|
const alignTop = React.useCallback(() => {
|
||||||
state.align(AlignType.Top)
|
app.align(AlignType.Top)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignCenterVertical = React.useCallback(() => {
|
const alignCenterVertical = React.useCallback(() => {
|
||||||
state.align(AlignType.CenterVertical)
|
app.align(AlignType.CenterVertical)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignBottom = React.useCallback(() => {
|
const alignBottom = React.useCallback(() => {
|
||||||
state.align(AlignType.Bottom)
|
app.align(AlignType.Bottom)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const stretchVertically = React.useCallback(() => {
|
const stretchVertically = React.useCallback(() => {
|
||||||
state.stretch(StretchType.Vertical)
|
app.stretch(StretchType.Vertical)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const distributeVertically = React.useCallback(() => {
|
const distributeVertically = React.useCallback(() => {
|
||||||
state.distribute(DistributeType.Vertical)
|
app.distribute(DistributeType.Vertical)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignLeft = React.useCallback(() => {
|
const alignLeft = React.useCallback(() => {
|
||||||
state.align(AlignType.Left)
|
app.align(AlignType.Left)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignCenterHorizontal = React.useCallback(() => {
|
const alignCenterHorizontal = React.useCallback(() => {
|
||||||
state.align(AlignType.CenterHorizontal)
|
app.align(AlignType.CenterHorizontal)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignRight = React.useCallback(() => {
|
const alignRight = React.useCallback(() => {
|
||||||
state.align(AlignType.Right)
|
app.align(AlignType.Right)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const stretchHorizontally = React.useCallback(() => {
|
const stretchHorizontally = React.useCallback(() => {
|
||||||
state.stretch(StretchType.Horizontal)
|
app.stretch(StretchType.Horizontal)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const distributeHorizontally = React.useCallback(() => {
|
const distributeHorizontally = React.useCallback(() => {
|
||||||
state.distribute(DistributeType.Horizontal)
|
app.distribute(DistributeType.Horizontal)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RadixContextMenu.Root>
|
<RadixContextMenu.Root dir="ltr">
|
||||||
<CMTriggerButton isSubmenu>Align / Distribute</CMTriggerButton>
|
<CMTriggerButton isSubmenu>Align / Distribute</CMTriggerButton>
|
||||||
<RadixContextMenu.Content asChild sideOffset={2} alignOffset={-2}>
|
<RadixContextMenu.Content asChild sideOffset={2} alignOffset={-2}>
|
||||||
<StyledGridContent selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}>
|
<StyledGridContent selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}>
|
||||||
<CMIconButton onSelect={alignLeft}>
|
<CMIconButton onClick={alignLeft}>
|
||||||
<AlignLeftIcon />
|
<AlignLeftIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
<CMIconButton onSelect={alignCenterHorizontal}>
|
<CMIconButton onClick={alignCenterHorizontal}>
|
||||||
<AlignCenterHorizontallyIcon />
|
<AlignCenterHorizontallyIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
<CMIconButton onSelect={alignRight}>
|
<CMIconButton onClick={alignRight}>
|
||||||
<AlignRightIcon />
|
<AlignRightIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
<CMIconButton onSelect={stretchHorizontally}>
|
<CMIconButton onClick={stretchHorizontally}>
|
||||||
<StretchHorizontallyIcon />
|
<StretchHorizontallyIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
{hasThreeOrMore && (
|
{hasThreeOrMore && (
|
||||||
<CMIconButton onSelect={distributeHorizontally}>
|
<CMIconButton onClick={distributeHorizontally}>
|
||||||
<SpaceEvenlyHorizontallyIcon />
|
<SpaceEvenlyHorizontallyIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
)}
|
)}
|
||||||
<CMIconButton onSelect={alignTop}>
|
<CMIconButton onClick={alignTop}>
|
||||||
<AlignTopIcon />
|
<AlignTopIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
<CMIconButton onSelect={alignCenterVertical}>
|
<CMIconButton onClick={alignCenterVertical}>
|
||||||
<AlignCenterVerticallyIcon />
|
<AlignCenterVerticallyIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
<CMIconButton onSelect={alignBottom}>
|
<CMIconButton onClick={alignBottom}>
|
||||||
<AlignBottomIcon />
|
<AlignBottomIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
<CMIconButton onSelect={stretchVertically}>
|
<CMIconButton onClick={stretchVertically}>
|
||||||
<StretchVerticallyIcon />
|
<StretchVerticallyIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
{hasThreeOrMore && (
|
{hasThreeOrMore && (
|
||||||
<CMIconButton onSelect={distributeVertically}>
|
<CMIconButton onClick={distributeVertically}>
|
||||||
<SpaceEvenlyVerticallyIcon />
|
<SpaceEvenlyVerticallyIcon />
|
||||||
</CMIconButton>
|
</CMIconButton>
|
||||||
)}
|
)}
|
||||||
|
@ -319,13 +326,13 @@ const StyledGridContent = styled(MenuContent, {
|
||||||
|
|
||||||
/* ------------------ Move to Page ------------------ */
|
/* ------------------ Move to Page ------------------ */
|
||||||
|
|
||||||
const currentPageIdSelector = (s: TLDrawSnapshot) => s.appState.currentPageId
|
const currentPageIdSelector = (s: TDSnapshot) => s.appState.currentPageId
|
||||||
const documentPagesSelector = (s: TLDrawSnapshot) => s.document.pages
|
const documentPagesSelector = (s: TDSnapshot) => s.document.pages
|
||||||
|
|
||||||
function MoveToPageMenu(): JSX.Element | null {
|
function MoveToPageMenu(): JSX.Element | null {
|
||||||
const { state, useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
const currentPageId = useSelector(currentPageIdSelector)
|
const currentPageId = app.useStore(currentPageIdSelector)
|
||||||
const documentPages = useSelector(documentPagesSelector)
|
const documentPages = app.useStore(documentPagesSelector)
|
||||||
|
|
||||||
const sorted = Object.values(documentPages)
|
const sorted = Object.values(documentPages)
|
||||||
.sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
.sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||||
|
@ -342,7 +349,7 @@ function MoveToPageMenu(): JSX.Element | null {
|
||||||
<CMRowButton
|
<CMRowButton
|
||||||
key={id}
|
key={id}
|
||||||
disabled={id === currentPageId}
|
disabled={id === currentPageId}
|
||||||
onSelect={() => state.moveToPage(id)}
|
onClick={() => app.moveToPage(id)}
|
||||||
>
|
>
|
||||||
{name || `Page ${i}`}
|
{name || `Page ${i}`}
|
||||||
</CMRowButton>
|
</CMRowButton>
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { CheckboxItem } from '@radix-ui/react-dropdown-menu'
|
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 {
|
interface DMCheckboxItemProps {
|
||||||
checked: boolean
|
checked: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onCheckedChange: (isChecked: boolean) => void
|
onCheckedChange: (isChecked: boolean) => void
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
variant?: RowButtonProps['variant']
|
||||||
kbd?: string
|
kbd?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DMCheckboxItem({
|
export function DMCheckboxItem({
|
||||||
checked,
|
checked,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
variant,
|
||||||
onCheckedChange,
|
onCheckedChange,
|
||||||
kbd,
|
kbd,
|
||||||
children,
|
children,
|
||||||
|
@ -20,12 +23,13 @@ export function DMCheckboxItem({
|
||||||
return (
|
return (
|
||||||
<CheckboxItem
|
<CheckboxItem
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
|
onSelect={preventEvent}
|
||||||
onCheckedChange={onCheckedChange}
|
onCheckedChange={onCheckedChange}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<RowButton kbd={kbd} hasIndicator>
|
<RowButton kbd={kbd} variant={variant} hasIndicator>
|
||||||
{children}
|
{children}
|
||||||
</RowButton>
|
</RowButton>
|
||||||
</CheckboxItem>
|
</CheckboxItem>
|
||||||
|
|
|
@ -2,18 +2,29 @@ import * as React from 'react'
|
||||||
import { Content } from '@radix-ui/react-dropdown-menu'
|
import { Content } from '@radix-ui/react-dropdown-menu'
|
||||||
import { styled } from '~styles/stitches.config'
|
import { styled } from '~styles/stitches.config'
|
||||||
import { MenuContent } from '~components/MenuContent'
|
import { MenuContent } from '~components/MenuContent'
|
||||||
|
import { stopPropagation } from '~components/stopPropagation'
|
||||||
|
|
||||||
export interface DMContentProps {
|
export interface DMContentProps {
|
||||||
variant?: 'grid' | 'menu' | 'horizontal'
|
variant?: 'menu' | 'horizontal'
|
||||||
align?: 'start' | 'center' | 'end'
|
align?: 'start' | 'center' | 'end'
|
||||||
|
sideOffset?: number
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const preventDefault = (e: Event) => e.stopPropagation()
|
export function DMContent({
|
||||||
|
sideOffset = 8,
|
||||||
export function DMContent({ children, align, variant }: DMContentProps): JSX.Element {
|
children,
|
||||||
|
align,
|
||||||
|
variant,
|
||||||
|
}: DMContentProps): JSX.Element {
|
||||||
return (
|
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>
|
<StyledContent variant={variant}>{children}</StyledContent>
|
||||||
</Content>
|
</Content>
|
||||||
)
|
)
|
||||||
|
@ -25,11 +36,6 @@ export const StyledContent = styled(MenuContent, {
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
grid: {
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(4, auto)',
|
|
||||||
gap: 0,
|
|
||||||
},
|
|
||||||
horizontal: {
|
horizontal: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,10 @@ import * as React from 'react'
|
||||||
import { Item } from '@radix-ui/react-dropdown-menu'
|
import { Item } from '@radix-ui/react-dropdown-menu'
|
||||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
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 (
|
return (
|
||||||
<Item dir="ltr" asChild onSelect={onSelect}>
|
<Item dir="ltr" asChild onSelect={onSelect}>
|
||||||
<RowButton {...rest} />
|
<RowButton {...rest} />
|
||||||
|
|
|
@ -16,11 +16,32 @@ export const DMRadioItem = styled(RadioItem, {
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|
||||||
'&:focus': {
|
variants: {
|
||||||
backgroundColor: '$hover',
|
isActive: {
|
||||||
|
true: {
|
||||||
|
backgroundColor: '$selected',
|
||||||
|
color: '$panel',
|
||||||
|
},
|
||||||
|
false: {},
|
||||||
|
},
|
||||||
|
bp: {
|
||||||
|
mobile: {},
|
||||||
|
small: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
'&:hover:not(:disabled)': {
|
compoundVariants: [
|
||||||
backgroundColor: '$hover',
|
{
|
||||||
},
|
isActive: false,
|
||||||
|
bp: 'small',
|
||||||
|
css: {
|
||||||
|
'&:focus': {
|
||||||
|
backgroundColor: '$hover',
|
||||||
|
},
|
||||||
|
'&:hover:not(:disabled)': {
|
||||||
|
backgroundColor: '$hover',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Trigger } from '@radix-ui/react-dropdown-menu'
|
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
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DMTriggerIcon({ children }: DMTriggerIconProps) {
|
export function DMTriggerIcon({ children, ...rest }: DMTriggerIconProps) {
|
||||||
return (
|
return (
|
||||||
<Trigger asChild>
|
<Trigger asChild>
|
||||||
<ToolButton>{children}</ToolButton>
|
<ToolButton {...rest}>{children}</ToolButton>
|
||||||
</Trigger>
|
</Trigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const StyledButtonContainer = styled('div', {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
|
|
||||||
'& svg': {
|
'& svg': {
|
||||||
color: '$muted',
|
color: '$text',
|
||||||
},
|
},
|
||||||
|
|
||||||
'&:hover svg': {
|
'&:hover svg': {
|
||||||
|
|
|
@ -33,8 +33,9 @@ export const StyledKbd = styled('kbd', {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: '$0',
|
fontSize: '$0',
|
||||||
fontFamily: '$ui',
|
fontFamily: '$ui',
|
||||||
fontWeight: 400,
|
|
||||||
color: '$text',
|
color: '$text',
|
||||||
|
background: 'none',
|
||||||
|
fontWeight: 400,
|
||||||
gap: '$1',
|
gap: '$1',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
@ -51,7 +52,7 @@ export const StyledKbd = styled('kbd', {
|
||||||
variant: {
|
variant: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
'& > span': {
|
'& > span': {
|
||||||
color: '$tooltipText',
|
color: '$tooltipContrast',
|
||||||
background: '$overlayContrast',
|
background: '$overlayContrast',
|
||||||
boxShadow: '$key',
|
boxShadow: '$key',
|
||||||
width: '20px',
|
width: '20px',
|
||||||
|
|
|
@ -6,17 +6,28 @@ export const Panel = styled('div', {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
boxShadow: '$panel',
|
boxShadow: '$panel',
|
||||||
padding: '$2',
|
padding: '$2',
|
||||||
|
border: '1px solid $panelContrast',
|
||||||
|
gap: 0,
|
||||||
variants: {
|
variants: {
|
||||||
side: {
|
side: {
|
||||||
center: {
|
center: {
|
||||||
borderTopLeftRadius: '$4',
|
borderRadius: '$4',
|
||||||
borderTopRightRadius: '$4',
|
|
||||||
},
|
},
|
||||||
left: {
|
left: {
|
||||||
|
padding: 0,
|
||||||
|
borderTop: 0,
|
||||||
|
borderLeft: 0,
|
||||||
|
borderTopRightRadius: '$1',
|
||||||
borderBottomRightRadius: '$3',
|
borderBottomRightRadius: '$3',
|
||||||
|
borderBottomLeftRadius: '$1',
|
||||||
},
|
},
|
||||||
right: {
|
right: {
|
||||||
|
padding: 0,
|
||||||
|
borderTop: 0,
|
||||||
|
borderRight: 0,
|
||||||
|
borderTopLeftRadius: '$1',
|
||||||
borderBottomLeftRadius: '$3',
|
borderBottomLeftRadius: '$3',
|
||||||
|
borderBottomRightRadius: '$1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,10 +7,12 @@ import { SmallIcon } from '~components/SmallIcon'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
|
|
||||||
export interface RowButtonProps {
|
export interface RowButtonProps {
|
||||||
onSelect?: () => void
|
onClick?: React.MouseEventHandler<HTMLButtonElement>
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
kbd?: string
|
kbd?: string
|
||||||
|
variant?: 'wide'
|
||||||
|
isSponsor?: boolean
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
isWarning?: boolean
|
isWarning?: boolean
|
||||||
hasIndicator?: boolean
|
hasIndicator?: boolean
|
||||||
|
@ -20,12 +22,14 @@ export interface RowButtonProps {
|
||||||
export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(
|
export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
onSelect,
|
onClick,
|
||||||
isActive = false,
|
isActive = false,
|
||||||
isWarning = false,
|
isWarning = false,
|
||||||
hasIndicator = false,
|
hasIndicator = false,
|
||||||
hasArrow = false,
|
hasArrow = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
isSponsor = false,
|
||||||
|
variant,
|
||||||
kbd,
|
kbd,
|
||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
|
@ -38,8 +42,10 @@ export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(
|
||||||
bp={breakpoints}
|
bp={breakpoints}
|
||||||
isWarning={isWarning}
|
isWarning={isWarning}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
isSponsor={isSponsor}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onPointerDown={onSelect}
|
onClick={onClick}
|
||||||
|
variant={variant}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<StyledRowButtonInner>
|
<StyledRowButtonInner>
|
||||||
|
@ -66,13 +72,10 @@ export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(
|
||||||
const StyledRowButtonInner = styled('div', {
|
const StyledRowButtonInner = styled('div', {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
color: '$text',
|
|
||||||
fontFamily: '$ui',
|
|
||||||
fontWeight: 400,
|
|
||||||
fontSize: '$1',
|
|
||||||
backgroundColor: '$panel',
|
backgroundColor: '$panel',
|
||||||
borderRadius: '$2',
|
borderRadius: '$2',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
gap: '$1',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '0 $3',
|
padding: '0 $3',
|
||||||
|
@ -95,6 +98,10 @@ export const StyledRowButton = styled('button', {
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
|
color: '$text',
|
||||||
|
fontFamily: '$ui',
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: '$1',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
|
@ -112,17 +119,33 @@ export const StyledRowButton = styled('button', {
|
||||||
backgroundColor: '$hover',
|
backgroundColor: '$hover',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'& a': {
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: '$text',
|
||||||
|
},
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
bp: {
|
bp: {
|
||||||
mobile: {},
|
mobile: {},
|
||||||
small: {},
|
small: {},
|
||||||
},
|
},
|
||||||
|
variant: {
|
||||||
|
wide: {
|
||||||
|
gridColumn: '1 / span 4',
|
||||||
|
},
|
||||||
|
},
|
||||||
size: {
|
size: {
|
||||||
icon: {
|
icon: {
|
||||||
padding: '4px ',
|
padding: '4px ',
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
isSponsor: {
|
||||||
|
true: {
|
||||||
|
color: '#eb30a2',
|
||||||
|
},
|
||||||
|
false: {},
|
||||||
|
},
|
||||||
isWarning: {
|
isWarning: {
|
||||||
true: {
|
true: {
|
||||||
color: '$warn',
|
color: '$warn',
|
||||||
|
@ -132,7 +155,29 @@ export const StyledRowButton = styled('button', {
|
||||||
true: {
|
true: {
|
||||||
backgroundColor: '$hover',
|
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}`]: {
|
[`&:hover:not(:disabled) ${StyledRowButtonInner}`]: {
|
||||||
backgroundColor: '$hover',
|
backgroundColor: '$hover',
|
||||||
border: '1px solid $panel',
|
border: '1px solid $panel',
|
||||||
|
@ -142,5 +187,5 @@ export const StyledRowButton = styled('button', {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const SmallIcon = styled('div', {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: '$text',
|
color: 'currentColor',
|
||||||
|
|
||||||
'& svg': {
|
'& svg': {
|
||||||
height: 16,
|
height: 16,
|
||||||
|
|
|
@ -1,32 +1,52 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { breakpoints } from '~components/breakpoints'
|
import { breakpoints } from '~components/breakpoints'
|
||||||
import { Tooltip } from '~components/Tooltip'
|
import { Tooltip } from '~components/Tooltip'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
|
|
||||||
export interface ToolButtonProps {
|
export interface ToolButtonProps {
|
||||||
onClick?: () => void
|
onClick?: () => void
|
||||||
onSelect?: () => void
|
onSelect?: () => void
|
||||||
onDoubleClick?: () => void
|
onDoubleClick?: () => void
|
||||||
|
disabled?: boolean
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
isSponsor?: boolean
|
||||||
|
isToolLocked?: boolean
|
||||||
variant?: 'icon' | 'text' | 'circle' | 'primary'
|
variant?: 'icon' | 'text' | 'circle' | 'primary'
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
|
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 (
|
return (
|
||||||
<StyledToolButton
|
<StyledToolButton
|
||||||
ref={ref}
|
ref={ref}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
isSponsor={isSponsor}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
onPointerDown={onSelect}
|
onPointerDown={onSelect}
|
||||||
onDoubleClick={onDoubleClick}
|
onDoubleClick={onDoubleClick}
|
||||||
bp={breakpoints}
|
bp={breakpoints}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<StyledToolButtonInner>{children}</StyledToolButtonInner>
|
<StyledToolButtonInner>{children}</StyledToolButtonInner>
|
||||||
|
{isToolLocked && <ToolLockIndicator />}
|
||||||
</StyledToolButton>
|
</StyledToolButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -36,20 +56,30 @@ export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
|
||||||
|
|
||||||
interface ToolButtonWithTooltipProps extends ToolButtonProps {
|
interface ToolButtonWithTooltipProps extends ToolButtonProps {
|
||||||
label: string
|
label: string
|
||||||
|
isLocked?: boolean
|
||||||
kbd?: string
|
kbd?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolButtonWithTooltip({ label, kbd, ...rest }: ToolButtonWithTooltipProps) {
|
export function ToolButtonWithTooltip({
|
||||||
const { state } = useTLDrawContext()
|
label,
|
||||||
|
kbd,
|
||||||
|
isLocked,
|
||||||
|
...rest
|
||||||
|
}: ToolButtonWithTooltipProps) {
|
||||||
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const handleDoubleClick = React.useCallback(() => {
|
const handleDoubleClick = React.useCallback(() => {
|
||||||
console.log('double clicking')
|
app.toggleToolLock()
|
||||||
state.toggleToolLock()
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label={label[0].toUpperCase() + label.slice(1)} kbd={kbd}>
|
<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>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -112,6 +142,7 @@ export const StyledToolButton = styled('button', {
|
||||||
circle: {
|
circle: {
|
||||||
padding: '$2',
|
padding: '$2',
|
||||||
[`& ${StyledToolButtonInner}`]: {
|
[`& ${StyledToolButtonInner}`]: {
|
||||||
|
border: '1px solid $panelContrast',
|
||||||
borderRadius: '100%',
|
borderRadius: '100%',
|
||||||
boxShadow: '$panel',
|
boxShadow: '$panel',
|
||||||
},
|
},
|
||||||
|
@ -121,23 +152,17 @@ export const StyledToolButton = styled('button', {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isActive: {
|
isSponsor: {
|
||||||
true: {
|
true: {
|
||||||
[`${StyledToolButtonInner}`]: {
|
[`${StyledToolButtonInner}`]: {
|
||||||
backgroundColor: '$selected',
|
backgroundColor: '$sponsorContrast',
|
||||||
color: '$panelActive',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
false: {
|
|
||||||
[`&:hover:not(:disabled) ${StyledToolButtonInner}`]: {
|
|
||||||
backgroundColor: '$hover',
|
|
||||||
border: '1px solid $panel',
|
|
||||||
},
|
|
||||||
[`&:focus:not(:disabled) ${StyledToolButtonInner}`]: {
|
|
||||||
backgroundColor: '$hover',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
isActive: {
|
||||||
|
true: {},
|
||||||
|
false: {},
|
||||||
|
},
|
||||||
bp: {
|
bp: {
|
||||||
mobile: {},
|
mobile: {},
|
||||||
small: {},
|
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 * as React from 'react'
|
||||||
import { Tooltip } from '~components/Tooltip/Tooltip'
|
import { Tooltip } from '~components/Tooltip/Tooltip'
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import { AlignType, TLDrawSnapshot, DistributeType, StretchType } from '~types'
|
import { AlignType, TDSnapshot, DistributeType, StretchType } from '~types'
|
||||||
import {
|
import {
|
||||||
ArrowDownIcon,
|
ArrowDownIcon,
|
||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
|
@ -30,25 +30,24 @@ import {
|
||||||
import { DMContent } from '~components/DropdownMenu'
|
import { DMContent } from '~components/DropdownMenu'
|
||||||
import { Divider } from '~components/Divider'
|
import { Divider } from '~components/Divider'
|
||||||
import { TrashIcon } from '~components/icons'
|
import { TrashIcon } from '~components/icons'
|
||||||
import { IconButton } from '~components/IconButton'
|
|
||||||
import { ToolButton } from '~components/ToolButton'
|
import { ToolButton } from '~components/ToolButton'
|
||||||
|
|
||||||
const selectedShapesCountSelector = (s: TLDrawSnapshot) =>
|
const selectedShapesCountSelector = (s: TDSnapshot) =>
|
||||||
s.document.pageStates[s.appState.currentPageId].selectedIds.length
|
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 page = s.document.pages[s.appState.currentPageId]
|
||||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||||
return selectedIds.every((id) => page.shapes[id].isLocked)
|
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 page = s.document.pages[s.appState.currentPageId]
|
||||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||||
return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
|
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 page = s.document.pages[s.appState.currentPageId]
|
||||||
const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map(
|
const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map(
|
||||||
(id) => page.shapes[id]
|
(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]
|
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||||
return selectedIds.length > 0
|
return selectedIds.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasMultipleSelectionClickor = (s: TLDrawSnapshot) => {
|
const hasMultipleSelectionClickor = (s: TDSnapshot) => {
|
||||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||||
return selectedIds.length > 1
|
return selectedIds.length > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ActionButton(): JSX.Element {
|
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(() => {
|
const handleRotate = React.useCallback(() => {
|
||||||
state.rotate()
|
app.rotate()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleDuplicate = React.useCallback(() => {
|
const handleDuplicate = React.useCallback(() => {
|
||||||
state.duplicate()
|
app.duplicate()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleToggleLocked = React.useCallback(() => {
|
const handleToggleLocked = React.useCallback(() => {
|
||||||
state.toggleLocked()
|
app.toggleLocked()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleToggleAspectRatio = React.useCallback(() => {
|
const handleToggleAspectRatio = React.useCallback(() => {
|
||||||
state.toggleAspectRatioLocked()
|
app.toggleAspectRatioLocked()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleGroup = React.useCallback(() => {
|
const handleGroup = React.useCallback(() => {
|
||||||
state.group()
|
app.group()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleMoveToBack = React.useCallback(() => {
|
const handleMoveToBack = React.useCallback(() => {
|
||||||
state.moveToBack()
|
app.moveToBack()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleMoveBackward = React.useCallback(() => {
|
const handleMoveBackward = React.useCallback(() => {
|
||||||
state.moveBackward()
|
app.moveBackward()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleMoveForward = React.useCallback(() => {
|
const handleMoveForward = React.useCallback(() => {
|
||||||
state.moveForward()
|
app.moveForward()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleMoveToFront = React.useCallback(() => {
|
const handleMoveToFront = React.useCallback(() => {
|
||||||
state.moveToFront()
|
app.moveToFront()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleDelete = React.useCallback(() => {
|
const handleDelete = React.useCallback(() => {
|
||||||
state.delete()
|
app.delete()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignTop = React.useCallback(() => {
|
const alignTop = React.useCallback(() => {
|
||||||
state.align(AlignType.Top)
|
app.align(AlignType.Top)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignCenterVertical = React.useCallback(() => {
|
const alignCenterVertical = React.useCallback(() => {
|
||||||
state.align(AlignType.CenterVertical)
|
app.align(AlignType.CenterVertical)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignBottom = React.useCallback(() => {
|
const alignBottom = React.useCallback(() => {
|
||||||
state.align(AlignType.Bottom)
|
app.align(AlignType.Bottom)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const stretchVertically = React.useCallback(() => {
|
const stretchVertically = React.useCallback(() => {
|
||||||
state.stretch(StretchType.Vertical)
|
app.stretch(StretchType.Vertical)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const distributeVertically = React.useCallback(() => {
|
const distributeVertically = React.useCallback(() => {
|
||||||
state.distribute(DistributeType.Vertical)
|
app.distribute(DistributeType.Vertical)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignLeft = React.useCallback(() => {
|
const alignLeft = React.useCallback(() => {
|
||||||
state.align(AlignType.Left)
|
app.align(AlignType.Left)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignCenterHorizontal = React.useCallback(() => {
|
const alignCenterHorizontal = React.useCallback(() => {
|
||||||
state.align(AlignType.CenterHorizontal)
|
app.align(AlignType.CenterHorizontal)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const alignRight = React.useCallback(() => {
|
const alignRight = React.useCallback(() => {
|
||||||
state.align(AlignType.Right)
|
app.align(AlignType.Right)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const stretchHorizontally = React.useCallback(() => {
|
const stretchHorizontally = React.useCallback(() => {
|
||||||
state.stretch(StretchType.Horizontal)
|
app.stretch(StretchType.Horizontal)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const distributeHorizontally = React.useCallback(() => {
|
const distributeHorizontally = React.useCallback(() => {
|
||||||
state.distribute(DistributeType.Horizontal)
|
app.distribute(DistributeType.Horizontal)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const selectedShapesCount = useSelector(selectedShapesCountSelector)
|
const selectedShapesCount = app.useStore(selectedShapesCountSelector)
|
||||||
|
|
||||||
const hasTwoOrMore = selectedShapesCount > 1
|
const hasTwoOrMore = selectedShapesCount > 1
|
||||||
const hasThreeOrMore = selectedShapesCount > 2
|
const hasThreeOrMore = selectedShapesCount > 2
|
||||||
|
@ -177,99 +176,99 @@ export function ActionButton(): JSX.Element {
|
||||||
<DotsHorizontalIcon />
|
<DotsHorizontalIcon />
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DMContent>
|
<DMContent sideOffset={16}>
|
||||||
<>
|
<>
|
||||||
<ButtonsRow>
|
<ButtonsRow>
|
||||||
<IconButton disabled={!hasSelection} onClick={handleDuplicate}>
|
<ToolButton variant="icon" disabled={!hasSelection} onClick={handleDuplicate}>
|
||||||
<Tooltip label="Duplicate" kbd={`#D`}>
|
<Tooltip label="Duplicate" kbd={`#D`}>
|
||||||
<CopyIcon />
|
<CopyIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasSelection} onClick={handleRotate}>
|
<ToolButton disabled={!hasSelection} onClick={handleRotate}>
|
||||||
<Tooltip label="Rotate">
|
<Tooltip label="Rotate">
|
||||||
<RotateCounterClockwiseIcon />
|
<RotateCounterClockwiseIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasSelection} onClick={handleToggleLocked}>
|
<ToolButton disabled={!hasSelection} onClick={handleToggleLocked}>
|
||||||
<Tooltip label="Toogle Locked" kbd={`#L`}>
|
<Tooltip label="Toogle Locked" kbd={`#L`}>
|
||||||
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon opacity={0.4} />}
|
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon opacity={0.4} />}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasSelection} onClick={handleToggleAspectRatio}>
|
<ToolButton disabled={!hasSelection} onClick={handleToggleAspectRatio}>
|
||||||
<Tooltip label="Toogle Aspect Ratio Lock">
|
<Tooltip label="Toogle Aspect Ratio Lock">
|
||||||
<AspectRatioIcon opacity={isAllAspectLocked ? 1 : 0.4} />
|
<AspectRatioIcon opacity={isAllAspectLocked ? 1 : 0.4} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton
|
<ToolButton
|
||||||
disabled={!hasSelection || (!isAllGrouped && !hasMultipleSelection)}
|
disabled={!hasSelection || (!isAllGrouped && !hasMultipleSelection)}
|
||||||
onClick={handleGroup}
|
onClick={handleGroup}
|
||||||
>
|
>
|
||||||
<Tooltip label="Group" kbd={`#G`}>
|
<Tooltip label="Group" kbd={`#G`}>
|
||||||
<GroupIcon opacity={isAllGrouped ? 1 : 0.4} />
|
<GroupIcon opacity={isAllGrouped ? 1 : 0.4} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
</ButtonsRow>
|
</ButtonsRow>
|
||||||
<ButtonsRow>
|
<ButtonsRow>
|
||||||
<IconButton disabled={!hasSelection} onClick={handleMoveToBack}>
|
<ToolButton disabled={!hasSelection} onClick={handleMoveToBack}>
|
||||||
<Tooltip label="Move to Back" kbd={`#⇧[`}>
|
<Tooltip label="Move to Back" kbd={`#⇧[`}>
|
||||||
<PinBottomIcon />
|
<PinBottomIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasSelection} onClick={handleMoveBackward}>
|
<ToolButton disabled={!hasSelection} onClick={handleMoveBackward}>
|
||||||
<Tooltip label="Move Backward" kbd={`#[`}>
|
<Tooltip label="Move Backward" kbd={`#[`}>
|
||||||
<ArrowDownIcon />
|
<ArrowDownIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasSelection} onClick={handleMoveForward}>
|
<ToolButton disabled={!hasSelection} onClick={handleMoveForward}>
|
||||||
<Tooltip label="Move Forward" kbd={`#]`}>
|
<Tooltip label="Move Forward" kbd={`#]`}>
|
||||||
<ArrowUpIcon />
|
<ArrowUpIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasSelection} onClick={handleMoveToFront}>
|
<ToolButton disabled={!hasSelection} onClick={handleMoveToFront}>
|
||||||
<Tooltip label="More to Front" kbd={`#⇧]`}>
|
<Tooltip label="More to Front" kbd={`#⇧]`}>
|
||||||
<PinTopIcon />
|
<PinTopIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasSelection} onClick={handleDelete}>
|
<ToolButton disabled={!hasSelection} onClick={handleDelete}>
|
||||||
<Tooltip label="Delete" kbd="⌫">
|
<Tooltip label="Delete" kbd="⌫">
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
</ButtonsRow>
|
</ButtonsRow>
|
||||||
<Divider />
|
<Divider />
|
||||||
<ButtonsRow>
|
<ButtonsRow>
|
||||||
<IconButton disabled={!hasTwoOrMore} onClick={alignLeft}>
|
<ToolButton disabled={!hasTwoOrMore} onClick={alignLeft}>
|
||||||
<AlignLeftIcon />
|
<AlignLeftIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
|
<ToolButton disabled={!hasTwoOrMore} onClick={alignCenterHorizontal}>
|
||||||
<AlignCenterHorizontallyIcon />
|
<AlignCenterHorizontallyIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasTwoOrMore} onClick={alignRight}>
|
<ToolButton disabled={!hasTwoOrMore} onClick={alignRight}>
|
||||||
<AlignRightIcon />
|
<AlignRightIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
|
<ToolButton disabled={!hasTwoOrMore} onClick={stretchHorizontally}>
|
||||||
<StretchHorizontallyIcon />
|
<StretchHorizontallyIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
|
<ToolButton disabled={!hasThreeOrMore} onClick={distributeHorizontally}>
|
||||||
<SpaceEvenlyHorizontallyIcon />
|
<SpaceEvenlyHorizontallyIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
</ButtonsRow>
|
</ButtonsRow>
|
||||||
<ButtonsRow>
|
<ButtonsRow>
|
||||||
<IconButton disabled={!hasTwoOrMore} onClick={alignTop}>
|
<ToolButton disabled={!hasTwoOrMore} onClick={alignTop}>
|
||||||
<AlignTopIcon />
|
<AlignTopIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}>
|
<ToolButton disabled={!hasTwoOrMore} onClick={alignCenterVertical}>
|
||||||
<AlignCenterVerticallyIcon />
|
<AlignCenterVerticallyIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasTwoOrMore} onClick={alignBottom}>
|
<ToolButton disabled={!hasTwoOrMore} onClick={alignBottom}>
|
||||||
<AlignBottomIcon />
|
<AlignBottomIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasTwoOrMore} onClick={stretchVertically}>
|
<ToolButton disabled={!hasTwoOrMore} onClick={stretchVertically}>
|
||||||
<StretchVerticallyIcon />
|
<StretchVerticallyIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
<IconButton disabled={!hasThreeOrMore} onClick={distributeVertically}>
|
<ToolButton disabled={!hasThreeOrMore} onClick={distributeVertically}>
|
||||||
<SpaceEvenlyVerticallyIcon />
|
<SpaceEvenlyVerticallyIcon />
|
||||||
</IconButton>
|
</ToolButton>
|
||||||
</ButtonsRow>
|
</ButtonsRow>
|
||||||
</>
|
</>
|
||||||
</DMContent>
|
</DMContent>
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import type { TLDrawSnapshot } from '~types'
|
import type { TDSnapshot } from '~types'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { RowButton } from '~components/RowButton'
|
import { RowButton } from '~components/RowButton'
|
||||||
import { MenuContent } from '~components/MenuContent'
|
import { MenuContent } from '~components/MenuContent'
|
||||||
|
|
||||||
const isEmptyCanvasSelector = (s: TLDrawSnapshot) =>
|
const isEmptyCanvasSelector = (s: TDSnapshot) =>
|
||||||
Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
|
Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
|
||||||
s.appState.isEmptyCanvas
|
s.appState.isEmptyCanvas
|
||||||
|
|
||||||
export const BackToContent = React.memo(function BackToContent() {
|
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
|
if (!isEmptyCanvas) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BackToContentContainer>
|
<BackToContentContainer>
|
||||||
<RowButton onSelect={state.zoomToContent}>Back to content</RowButton>
|
<RowButton onClick={app.zoomToContent}>Back to content</RowButton>
|
||||||
</BackToContentContainer>
|
</BackToContentContainer>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Tooltip } from '~components/Tooltip'
|
import { Tooltip } from '~components/Tooltip'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { ToolButton } from '~components/ToolButton'
|
import { ToolButton } from '~components/ToolButton'
|
||||||
import { TrashIcon } from '~components/icons'
|
import { TrashIcon } from '~components/icons'
|
||||||
|
|
||||||
export function DeleteButton(): JSX.Element {
|
export function DeleteButton(): JSX.Element {
|
||||||
const { state } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const handleDelete = React.useCallback(() => {
|
const handleDelete = React.useCallback(() => {
|
||||||
state.delete()
|
app.delete()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label="Delete" kbd="7">
|
<Tooltip label="Delete" kbd="⌫">
|
||||||
<ToolButton variant="circle" onSelect={handleDelete}>
|
<ToolButton variant="circle" onSelect={handleDelete}>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons'
|
import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons'
|
||||||
import { Tooltip } from '~components/Tooltip'
|
import { Tooltip } from '~components/Tooltip'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { ToolButton } from '~components/ToolButton'
|
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 {
|
export function LockButton(): JSX.Element {
|
||||||
const { state, useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const isToolLocked = useSelector(isToolLockedSelector)
|
const isToolLocked = app.useStore(isToolLockedSelector)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip label="Lock Tool" kbd="7">
|
<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 />}
|
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
</Tooltip>
|
</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 * as React from 'react'
|
||||||
import {
|
import {
|
||||||
ArrowTopRightIcon,
|
ArrowTopRightIcon,
|
||||||
CircleIcon,
|
|
||||||
CursorArrowIcon,
|
CursorArrowIcon,
|
||||||
Pencil1Icon,
|
Pencil1Icon,
|
||||||
Pencil2Icon,
|
Pencil2Icon,
|
||||||
SquareIcon,
|
|
||||||
TextIcon,
|
TextIcon,
|
||||||
} from '@radix-ui/react-icons'
|
} from '@radix-ui/react-icons'
|
||||||
import { TLDrawSnapshot, TLDrawShapeType } from '~types'
|
import { TDSnapshot, TDShapeType } from '~types'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { ToolButtonWithTooltip } from '~components/ToolButton'
|
import { ToolButtonWithTooltip } from '~components/ToolButton'
|
||||||
import { Panel } from '~components/Panel'
|
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 {
|
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(() => {
|
const selectSelectTool = React.useCallback(() => {
|
||||||
state.selectTool('select')
|
app.selectTool('select')
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
|
const selectEraseTool = React.useCallback(() => {
|
||||||
|
app.selectTool('erase')
|
||||||
|
}, [app])
|
||||||
|
|
||||||
const selectDrawTool = React.useCallback(() => {
|
const selectDrawTool = React.useCallback(() => {
|
||||||
state.selectTool(TLDrawShapeType.Draw)
|
app.selectTool(TDShapeType.Draw)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const selectRectangleTool = React.useCallback(() => {
|
|
||||||
state.selectTool(TLDrawShapeType.Rectangle)
|
|
||||||
}, [state])
|
|
||||||
|
|
||||||
const selectEllipseTool = React.useCallback(() => {
|
|
||||||
state.selectTool(TLDrawShapeType.Ellipse)
|
|
||||||
}, [state])
|
|
||||||
|
|
||||||
const selectArrowTool = React.useCallback(() => {
|
const selectArrowTool = React.useCallback(() => {
|
||||||
state.selectTool(TLDrawShapeType.Arrow)
|
app.selectTool(TDShapeType.Arrow)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const selectTextTool = React.useCallback(() => {
|
const selectTextTool = React.useCallback(() => {
|
||||||
state.selectTool(TLDrawShapeType.Text)
|
app.selectTool(TDShapeType.Text)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const selectStickyTool = React.useCallback(() => {
|
const selectStickyTool = React.useCallback(() => {
|
||||||
state.selectTool(TLDrawShapeType.Sticky)
|
app.selectTool(TDShapeType.Sticky)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel side="center">
|
<Panel side="center">
|
||||||
|
@ -60,49 +59,43 @@ export const PrimaryTools = React.memo(function PrimaryTools(): JSX.Element {
|
||||||
</ToolButtonWithTooltip>
|
</ToolButtonWithTooltip>
|
||||||
<ToolButtonWithTooltip
|
<ToolButtonWithTooltip
|
||||||
kbd={'2'}
|
kbd={'2'}
|
||||||
label={TLDrawShapeType.Draw}
|
label={TDShapeType.Draw}
|
||||||
onClick={selectDrawTool}
|
onClick={selectDrawTool}
|
||||||
isActive={activeTool === TLDrawShapeType.Draw}
|
isActive={activeTool === TDShapeType.Draw}
|
||||||
>
|
>
|
||||||
<Pencil1Icon />
|
<Pencil1Icon />
|
||||||
</ToolButtonWithTooltip>
|
</ToolButtonWithTooltip>
|
||||||
<ToolButtonWithTooltip
|
<ToolButtonWithTooltip
|
||||||
kbd={'3'}
|
kbd={'3'}
|
||||||
label={TLDrawShapeType.Rectangle}
|
label={'eraser'}
|
||||||
onClick={selectRectangleTool}
|
onClick={selectEraseTool}
|
||||||
isActive={activeTool === TLDrawShapeType.Rectangle}
|
isActive={activeTool === 'erase'}
|
||||||
>
|
>
|
||||||
<SquareIcon />
|
<EraserIcon />
|
||||||
</ToolButtonWithTooltip>
|
</ToolButtonWithTooltip>
|
||||||
|
<ShapesMenu activeTool={activeTool} isToolLocked={isToolLocked} />
|
||||||
<ToolButtonWithTooltip
|
<ToolButtonWithTooltip
|
||||||
kbd={'4'}
|
kbd={'6'}
|
||||||
label={TLDrawShapeType.Ellipse}
|
label={TDShapeType.Arrow}
|
||||||
onClick={selectEllipseTool}
|
|
||||||
isActive={activeTool === TLDrawShapeType.Ellipse}
|
|
||||||
>
|
|
||||||
<CircleIcon />
|
|
||||||
</ToolButtonWithTooltip>
|
|
||||||
<ToolButtonWithTooltip
|
|
||||||
kbd={'5'}
|
|
||||||
label={TLDrawShapeType.Arrow}
|
|
||||||
onClick={selectArrowTool}
|
onClick={selectArrowTool}
|
||||||
isActive={activeTool === TLDrawShapeType.Arrow}
|
isLocked={isToolLocked}
|
||||||
|
isActive={activeTool === TDShapeType.Arrow}
|
||||||
>
|
>
|
||||||
<ArrowTopRightIcon />
|
<ArrowTopRightIcon />
|
||||||
</ToolButtonWithTooltip>
|
</ToolButtonWithTooltip>
|
||||||
<ToolButtonWithTooltip
|
<ToolButtonWithTooltip
|
||||||
kbd={'6'}
|
kbd={'7'}
|
||||||
label={TLDrawShapeType.Text}
|
label={TDShapeType.Text}
|
||||||
onClick={selectTextTool}
|
onClick={selectTextTool}
|
||||||
isActive={activeTool === TLDrawShapeType.Text}
|
isActive={activeTool === TDShapeType.Text}
|
||||||
>
|
>
|
||||||
<TextIcon />
|
<TextIcon />
|
||||||
</ToolButtonWithTooltip>
|
</ToolButtonWithTooltip>
|
||||||
<ToolButtonWithTooltip
|
<ToolButtonWithTooltip
|
||||||
kbd={'7'}
|
kbd={'8'}
|
||||||
label={TLDrawShapeType.Sticky}
|
label={TDShapeType.Sticky}
|
||||||
onClick={selectStickyTool}
|
onClick={selectStickyTool}
|
||||||
isActive={activeTool === TLDrawShapeType.Sticky}
|
isActive={activeTool === TDShapeType.Sticky}
|
||||||
>
|
>
|
||||||
<Pencil2Icon />
|
<Pencil2Icon />
|
||||||
</ToolButtonWithTooltip>
|
</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 * as React from 'react'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import type { TLDrawSnapshot } from '~types'
|
import type { TDSnapshot } from '~types'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import { breakpoints } from '~components/breakpoints'
|
import { breakpoints } from '~components/breakpoints'
|
||||||
|
|
||||||
const statusSelector = (s: TLDrawSnapshot) => s.appState.status
|
const statusSelector = (s: TDSnapshot) => s.appState.status
|
||||||
const activeToolSelector = (s: TLDrawSnapshot) => s.appState.activeTool
|
const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool
|
||||||
|
|
||||||
export function StatusBar(): JSX.Element | null {
|
export function StatusBar(): JSX.Element | null {
|
||||||
const { useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
const status = useSelector(statusSelector)
|
const status = app.useStore(statusSelector)
|
||||||
const activeTool = useSelector(activeToolSelector)
|
const activeTool = app.useStore(activeToolSelector)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledStatusBar bp={breakpoints}>
|
<StyledStatusBar bp={breakpoints}>
|
||||||
|
@ -24,7 +24,7 @@ export function StatusBar(): JSX.Element | null {
|
||||||
const StyledStatusBar = styled('div', {
|
const StyledStatusBar = styled('div', {
|
||||||
height: 40,
|
height: 40,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
borderTop: '1px solid $border',
|
borderTop: '1px solid $panelContrast',
|
||||||
gridArea: 'status',
|
gridArea: 'status',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
color: '$text',
|
color: '$text',
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import type { TLDrawSnapshot } from '~types'
|
import type { TDSnapshot } from '~types'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { StatusBar } from './StatusBar'
|
import { StatusBar } from './StatusBar'
|
||||||
import { BackToContent } from './BackToContent'
|
import { BackToContent } from './BackToContent'
|
||||||
import { PrimaryTools } from './PrimaryTools'
|
import { PrimaryTools } from './PrimaryTools'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { LockButton } from './LockButton'
|
|
||||||
import { DeleteButton } from './DeleteButton'
|
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 {
|
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 (
|
return (
|
||||||
<StyledToolsPanelContainer>
|
<StyledToolsPanelContainer>
|
||||||
|
@ -48,6 +47,7 @@ const StyledToolsPanelContainer = styled('div', {
|
||||||
gridTemplateRows: 'auto auto',
|
gridTemplateRows: 'auto auto',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
padding: '0',
|
padding: '0',
|
||||||
|
gap: '$4',
|
||||||
zIndex: 200,
|
zIndex: 200,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
'& > div > *': {
|
'& > div > *': {
|
||||||
|
|
|
@ -22,10 +22,10 @@ export function Tooltip({
|
||||||
}: TooltipProps): JSX.Element {
|
}: TooltipProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<RadixTooltip.Root>
|
<RadixTooltip.Root>
|
||||||
<RadixTooltip.Trigger asChild={true}>
|
<RadixTooltip.Trigger dir="ltr" asChild={true}>
|
||||||
<span>{children}</span>
|
<span>{children}</span>
|
||||||
</RadixTooltip.Trigger>
|
</RadixTooltip.Trigger>
|
||||||
<StyledContent side={side} sideOffset={8}>
|
<StyledContent dir="ltr" side={side} sideOffset={8}>
|
||||||
{label}
|
{label}
|
||||||
{kbdProp ? <Kbd variant="tooltip">{kbdProp}</Kbd> : null}
|
{kbdProp ? <Kbd variant="tooltip">{kbdProp}</Kbd> : null}
|
||||||
<StyledArrow />
|
<StyledArrow />
|
||||||
|
@ -38,8 +38,8 @@ const StyledContent = styled(RadixTooltip.Content, {
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
padding: '$3 $3 $3 $3',
|
padding: '$3 $3 $3 $3',
|
||||||
fontSize: '$1',
|
fontSize: '$1',
|
||||||
backgroundColor: '$tooltipBg',
|
backgroundColor: '$tooltip',
|
||||||
color: '$tooltipText',
|
color: '$tooltipContrast',
|
||||||
boxShadow: '$3',
|
boxShadow: '$3',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
@ -48,6 +48,6 @@ const StyledContent = styled(RadixTooltip.Content, {
|
||||||
})
|
})
|
||||||
|
|
||||||
const StyledArrow = styled(RadixTooltip.Arrow, {
|
const StyledArrow = styled(RadixTooltip.Arrow, {
|
||||||
fill: '$tooltipBg',
|
fill: '$tooltip',
|
||||||
margin: '0 8px',
|
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 * 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 * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { PreferencesMenu } from './PreferencesMenu'
|
import { PreferencesMenu } from './PreferencesMenu'
|
||||||
import { DMItem, DMContent, DMDivider, DMSubMenu, DMTriggerIcon } from '~components/DropdownMenu'
|
import { DMItem, DMContent, DMDivider, DMSubMenu, DMTriggerIcon } from '~components/DropdownMenu'
|
||||||
import { SmallIcon } from '~components/SmallIcon'
|
import { SmallIcon } from '~components/SmallIcon'
|
||||||
import { useFileSystemHandlers } from '~hooks'
|
import { useFileSystemHandlers } from '~hooks'
|
||||||
|
import { HeartIcon } from '~components/icons/HeartIcon'
|
||||||
|
import { preventEvent } from '~components/preventEvent'
|
||||||
|
|
||||||
interface MenuProps {
|
interface MenuProps {
|
||||||
|
showSponsorLink: boolean
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: MenuProps) {
|
||||||
const { state } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
||||||
|
|
||||||
const handleSignIn = React.useCallback(() => {
|
const handleSignIn = React.useCallback(() => {
|
||||||
state.callbacks.onSignIn?.(state)
|
app.callbacks.onSignIn?.(app)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleSignOut = React.useCallback(() => {
|
const handleSignOut = React.useCallback(() => {
|
||||||
state.callbacks.onSignOut?.(state)
|
app.callbacks.onSignOut?.(app)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleCut = React.useCallback(() => {
|
const handleCut = React.useCallback(() => {
|
||||||
state.cut()
|
app.cut()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleCopy = React.useCallback(() => {
|
const handleCopy = React.useCallback(() => {
|
||||||
state.copy()
|
app.copy()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handlePaste = React.useCallback(() => {
|
const handlePaste = React.useCallback(() => {
|
||||||
state.paste()
|
app.paste()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleCopySvg = React.useCallback(() => {
|
const handleCopySvg = React.useCallback(() => {
|
||||||
state.copySvg()
|
app.copySvg()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleCopyJson = React.useCallback(() => {
|
const handleCopyJson = React.useCallback(() => {
|
||||||
state.copyJson()
|
app.copyJson()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleSelectAll = React.useCallback(() => {
|
const handleSelectAll = React.useCallback(() => {
|
||||||
state.selectAll()
|
app.selectAll()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleselectNone = React.useCallback(() => {
|
const handleselectNone = React.useCallback(() => {
|
||||||
state.selectNone()
|
app.selectNone()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const showFileMenu =
|
const showFileMenu =
|
||||||
state.callbacks.onNewProject ||
|
app.callbacks.onNewProject ||
|
||||||
state.callbacks.onOpenProject ||
|
app.callbacks.onOpenProject ||
|
||||||
state.callbacks.onSaveProject ||
|
app.callbacks.onSaveProject ||
|
||||||
state.callbacks.onSaveProjectAs
|
app.callbacks.onSaveProjectAs
|
||||||
|
|
||||||
const showSignInOutMenu = state.callbacks.onSignIn || state.callbacks.onSignOut
|
const showSignInOutMenu = app.callbacks.onSignIn || app.callbacks.onSignOut || showSponsorLink
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root dir="ltr">
|
||||||
<DMTriggerIcon>
|
<DMTriggerIcon isSponsor={showSponsorLink}>
|
||||||
<HamburgerMenuIcon />
|
<HamburgerMenuIcon />
|
||||||
</DMTriggerIcon>
|
</DMTriggerIcon>
|
||||||
<DMContent variant="menu">
|
<DMContent variant="menu">
|
||||||
{showFileMenu && (
|
{showFileMenu && (
|
||||||
<DMSubMenu label="File...">
|
<DMSubMenu label="File...">
|
||||||
{state.callbacks.onNewProject && (
|
{app.callbacks.onNewProject && (
|
||||||
<DMItem onSelect={onNewProject} kbd="#N">
|
<DMItem onClick={onNewProject} kbd="#N">
|
||||||
New Project
|
New Project
|
||||||
</DMItem>
|
</DMItem>
|
||||||
)}
|
)}
|
||||||
{state.callbacks.onOpenProject && (
|
{app.callbacks.onOpenProject && (
|
||||||
<DMItem onSelect={onOpenProject} kbd="#O">
|
<DMItem onClick={onOpenProject} kbd="#O">
|
||||||
Open...
|
Open...
|
||||||
</DMItem>
|
</DMItem>
|
||||||
)}
|
)}
|
||||||
{state.callbacks.onSaveProject && (
|
{app.callbacks.onSaveProject && (
|
||||||
<DMItem onSelect={onSaveProject} kbd="#S">
|
<DMItem onClick={onSaveProject} kbd="#S">
|
||||||
Save
|
Save
|
||||||
</DMItem>
|
</DMItem>
|
||||||
)}
|
)}
|
||||||
{state.callbacks.onSaveProjectAs && (
|
{app.callbacks.onSaveProjectAs && (
|
||||||
<DMItem onSelect={onSaveProjectAs} kbd="⇧#S">
|
<DMItem onClick={onSaveProjectAs} kbd="⇧#S">
|
||||||
Save As...
|
Save As...
|
||||||
</DMItem>
|
</DMItem>
|
||||||
)}
|
)}
|
||||||
|
@ -93,42 +96,76 @@ export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<>
|
<>
|
||||||
<DMSubMenu label="Edit...">
|
<DMSubMenu label="Edit...">
|
||||||
<DMItem onSelect={state.undo} kbd="#Z">
|
<DMItem onSelect={preventEvent} onClick={app.undo} kbd="#Z">
|
||||||
Undo
|
Undo
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={state.redo} kbd="#⇧Z">
|
<DMItem onSelect={preventEvent} onClick={app.redo} kbd="#⇧Z">
|
||||||
Redo
|
Redo
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMDivider dir="ltr" />
|
<DMDivider dir="ltr" />
|
||||||
<DMItem onSelect={handleCut} kbd="#X">
|
<DMItem onSelect={preventEvent} onClick={handleCut} kbd="#X">
|
||||||
Cut
|
Cut
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={handleCopy} kbd="#C">
|
<DMItem onSelect={preventEvent} onClick={handleCopy} kbd="#C">
|
||||||
Copy
|
Copy
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={handlePaste} kbd="#V">
|
<DMItem onSelect={preventEvent} onClick={handlePaste} kbd="#V">
|
||||||
Paste
|
Paste
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMDivider dir="ltr" />
|
<DMDivider dir="ltr" />
|
||||||
<DMItem onSelect={handleCopySvg} kbd="#⇧C">
|
<DMItem onSelect={preventEvent} onClick={handleCopySvg} kbd="#⇧C">
|
||||||
Copy as SVG
|
Copy as SVG
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={handleCopyJson}>Copy as JSON</DMItem>
|
<DMItem onSelect={preventEvent} onClick={handleCopyJson}>
|
||||||
|
Copy as JSON
|
||||||
|
</DMItem>
|
||||||
<DMDivider dir="ltr" />
|
<DMDivider dir="ltr" />
|
||||||
<DMItem onSelect={handleSelectAll} kbd="#A">
|
<DMItem onSelect={preventEvent} onClick={handleSelectAll} kbd="#A">
|
||||||
Select All
|
Select All
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={handleselectNone}>Select None</DMItem>
|
<DMItem onSelect={preventEvent} onClick={handleselectNone}>
|
||||||
|
Select None
|
||||||
|
</DMItem>
|
||||||
</DMSubMenu>
|
</DMSubMenu>
|
||||||
<DMDivider dir="ltr" />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<a href="https://tldraw.com/r">
|
||||||
|
<DMItem>Create a Multiplayer Room</DMItem>
|
||||||
|
</a>
|
||||||
|
<DMDivider dir="ltr" />
|
||||||
<PreferencesMenu />
|
<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 && (
|
{showSignInOutMenu && (
|
||||||
<>
|
<>
|
||||||
<DMDivider dir="ltr" />{' '}
|
<DMDivider dir="ltr" />{' '}
|
||||||
{state.callbacks.onSignIn && <DMItem onSelect={handleSignIn}>Sign In</DMItem>}
|
{app.callbacks.onSignIn && <DMItem onSelect={handleSignIn}>Sign In</DMItem>}
|
||||||
{state.callbacks.onSignOut && (
|
{app.callbacks.onSignOut && (
|
||||||
<DMItem onSelect={handleSignOut}>
|
<DMItem onSelect={handleSignOut}>
|
||||||
Sign Out
|
Sign Out
|
||||||
<SmallIcon>
|
<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 { PlusIcon, CheckIcon } from '@radix-ui/react-icons'
|
||||||
import { PageOptionsDialog } from './PageOptionsDialog'
|
import { PageOptionsDialog } from './PageOptionsDialog'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import type { TLDrawSnapshot } from '~types'
|
import type { TDSnapshot } from '~types'
|
||||||
import { DMContent, DMDivider } from '~components/DropdownMenu'
|
import { DMContent, DMDivider } from '~components/DropdownMenu'
|
||||||
import { SmallIcon } from '~components/SmallIcon'
|
import { SmallIcon } from '~components/SmallIcon'
|
||||||
import { RowButton } from '~components/RowButton'
|
import { RowButton } from '~components/RowButton'
|
||||||
import { ToolButton } from '~components/ToolButton'
|
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))
|
Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||||
|
|
||||||
const currentPageNameSelector = (s: TLDrawSnapshot) =>
|
const currentPageNameSelector = (s: TDSnapshot) => s.document.pages[s.appState.currentPageId].name
|
||||||
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 {
|
export function PageMenu(): JSX.Element {
|
||||||
const { useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const rIsOpen = React.useRef(false)
|
const rIsOpen = React.useRef(false)
|
||||||
|
|
||||||
|
@ -43,7 +42,7 @@ export function PageMenu(): JSX.Element {
|
||||||
},
|
},
|
||||||
[setIsOpen]
|
[setIsOpen]
|
||||||
)
|
)
|
||||||
const currentPageName = useSelector(currentPageNameSelector)
|
const currentPageName = app.useStore(currentPageNameSelector)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root dir="ltr" open={isOpen} onOpenChange={handleOpenChange}>
|
<DropdownMenu.Root dir="ltr" open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
|
@ -58,27 +57,27 @@ export function PageMenu(): JSX.Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PageMenuContent({ onClose }: { onClose: () => void }) {
|
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(() => {
|
const handleCreatePage = React.useCallback(() => {
|
||||||
state.createPage()
|
app.createPage()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleChangePage = React.useCallback(
|
const handleChangePage = React.useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
onClose()
|
onClose()
|
||||||
state.changePage(id)
|
app.changePage(id)
|
||||||
},
|
},
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu.RadioGroup value={currentPageId} onValueChange={handleChangePage}>
|
<DropdownMenu.RadioGroup dir="ltr" value={currentPageId} onValueChange={handleChangePage}>
|
||||||
{sortedPages.map((page) => (
|
{sortedPages.map((page) => (
|
||||||
<ButtonWithOptions key={page.id}>
|
<ButtonWithOptions key={page.id}>
|
||||||
<DropdownMenu.RadioItem
|
<DropdownMenu.RadioItem
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as Dialog from '@radix-ui/react-alert-dialog'
|
import * as Dialog from '@radix-ui/react-alert-dialog'
|
||||||
import { MixerVerticalIcon } from '@radix-ui/react-icons'
|
import { MixerVerticalIcon } from '@radix-ui/react-icons'
|
||||||
import type { TLDrawSnapshot, TLDrawPage } from '~types'
|
import type { TDSnapshot, TDPage } from '~types'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import { Divider } from '~components/Divider'
|
import { Divider } from '~components/Divider'
|
||||||
|
@ -10,36 +10,36 @@ import { IconButton } from '~components/IconButton/IconButton'
|
||||||
import { SmallIcon } from '~components/SmallIcon'
|
import { SmallIcon } from '~components/SmallIcon'
|
||||||
import { breakpoints } from '~components/breakpoints'
|
import { breakpoints } from '~components/breakpoints'
|
||||||
|
|
||||||
const canDeleteSelector = (s: TLDrawSnapshot) => {
|
const canDeleteSelector = (s: TDSnapshot) => {
|
||||||
return Object.keys(s.document.pages).length > 1
|
return Object.keys(s.document.pages).length > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageOptionsDialogProps {
|
interface PageOptionsDialogProps {
|
||||||
page: TLDrawPage
|
page: TDPage
|
||||||
onOpen?: () => void
|
onOpen?: () => void
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
|
export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
|
||||||
const { state, useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = React.useState(false)
|
const [isOpen, setIsOpen] = React.useState(false)
|
||||||
|
|
||||||
const canDelete = useSelector(canDeleteSelector)
|
const canDelete = app.useStore(canDeleteSelector)
|
||||||
|
|
||||||
const rInput = React.useRef<HTMLInputElement>(null)
|
const rInput = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const handleDuplicate = React.useCallback(() => {
|
const handleDuplicate = React.useCallback(() => {
|
||||||
state.duplicatePage(page.id)
|
app.duplicatePage(page.id)
|
||||||
onClose?.()
|
onClose?.()
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleDelete = React.useCallback(() => {
|
const handleDelete = React.useCallback(() => {
|
||||||
if (window.confirm(`Are you sure you want to delete this page?`)) {
|
if (window.confirm(`Are you sure you want to delete this page?`)) {
|
||||||
state.deletePage(page.id)
|
app.deletePage(page.id)
|
||||||
onClose?.()
|
onClose?.()
|
||||||
}
|
}
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const handleOpenChange = React.useCallback(
|
const handleOpenChange = React.useCallback(
|
||||||
(isOpen: boolean) => {
|
(isOpen: boolean) => {
|
||||||
|
@ -50,7 +50,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[state, name]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
|
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
@ -60,7 +60,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr
|
||||||
// TODO: Replace with text input
|
// TODO: Replace with text input
|
||||||
function handleRename() {
|
function handleRename() {
|
||||||
const nextName = window.prompt('New name:', page.name)
|
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(() => {
|
React.useEffect(() => {
|
||||||
|
@ -82,7 +82,7 @@ export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogPr
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<StyledDialogOverlay />
|
<StyledDialogOverlay />
|
||||||
<StyledDialogContent onKeyDown={stopPropagation} onKeyUp={stopPropagation}>
|
<StyledDialogContent dir="ltr" onKeyDown={stopPropagation} onKeyUp={stopPropagation}>
|
||||||
<DialogAction onSelect={handleRename}>Rename</DialogAction>
|
<DialogAction onSelect={handleRename}>Rename</DialogAction>
|
||||||
<DialogAction onSelect={handleDuplicate}>Duplicate</DialogAction>
|
<DialogAction onSelect={handleDuplicate}>Duplicate</DialogAction>
|
||||||
<DialogAction disabled={!canDelete} onSelect={handleDelete}>
|
<DialogAction disabled={!canDelete} onSelect={handleDelete}>
|
||||||
|
@ -112,7 +112,6 @@ export const StyledDialogContent = styled(Dialog.Content, {
|
||||||
marginTop: '-5vh',
|
marginTop: '-5vh',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
backgroundColor: '$panel',
|
backgroundColor: '$panel',
|
||||||
border: '1px solid $panelBorder',
|
|
||||||
padding: '$0',
|
padding: '$0',
|
||||||
borderRadius: '$2',
|
borderRadius: '$2',
|
||||||
font: '$ui',
|
font: '$ui',
|
||||||
|
@ -132,9 +131,9 @@ export const StyledDialogOverlay = styled(Dialog.Overlay, {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
})
|
})
|
||||||
|
|
||||||
function DialogAction({ onSelect, ...rest }: RowButtonProps) {
|
function DialogAction({ ...rest }: RowButtonProps & { onSelect: (e: Event) => void }) {
|
||||||
return (
|
return (
|
||||||
<Dialog.Action asChild onClick={onSelect}>
|
<Dialog.Action asChild>
|
||||||
<RowButton {...rest} />
|
<RowButton {...rest} />
|
||||||
</Dialog.Action>
|
</Dialog.Action>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,42 +1,42 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/DropdownMenu'
|
import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/DropdownMenu'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import type { TLDrawSnapshot } from '~types'
|
import type { TDSnapshot } from '~types'
|
||||||
|
|
||||||
const settingsSelector = (s: TLDrawSnapshot) => s.settings
|
const settingsSelector = (s: TDSnapshot) => s.settings
|
||||||
|
|
||||||
export function PreferencesMenu() {
|
export function PreferencesMenu() {
|
||||||
const { state, useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const settings = useSelector(settingsSelector)
|
const settings = app.useStore(settingsSelector)
|
||||||
|
|
||||||
const toggleDebugMode = React.useCallback(() => {
|
const toggleDebugMode = React.useCallback(() => {
|
||||||
state.setSetting('isDebugMode', (v) => !v)
|
app.setSetting('isDebugMode', (v) => !v)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const toggleDarkMode = React.useCallback(() => {
|
const toggleDarkMode = React.useCallback(() => {
|
||||||
state.setSetting('isDarkMode', (v) => !v)
|
app.setSetting('isDarkMode', (v) => !v)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const toggleFocusMode = React.useCallback(() => {
|
const toggleFocusMode = React.useCallback(() => {
|
||||||
state.setSetting('isFocusMode', (v) => !v)
|
app.setSetting('isFocusMode', (v) => !v)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const toggleRotateHandle = React.useCallback(() => {
|
const toggleRotateHandle = React.useCallback(() => {
|
||||||
state.setSetting('showRotateHandles', (v) => !v)
|
app.setSetting('showRotateHandles', (v) => !v)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const toggleBoundShapesHandle = React.useCallback(() => {
|
const toggleBoundShapesHandle = React.useCallback(() => {
|
||||||
state.setSetting('showBindingHandles', (v) => !v)
|
app.setSetting('showBindingHandles', (v) => !v)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const toggleisSnapping = React.useCallback(() => {
|
const toggleisSnapping = React.useCallback(() => {
|
||||||
state.setSetting('isSnapping', (v) => !v)
|
app.setSetting('isSnapping', (v) => !v)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
const toggleCloneControls = React.useCallback(() => {
|
const toggleCloneControls = React.useCallback(() => {
|
||||||
state.setSetting('showCloneHandles', (v) => !v)
|
app.setSetting('showCloneHandles', (v) => !v)
|
||||||
}, [state])
|
}, [app])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DMSubMenu label="Preferences">
|
<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 { styled } from '~styles'
|
||||||
import { PageMenu } from './PageMenu'
|
import { PageMenu } from './PageMenu'
|
||||||
import { ZoomMenu } from './ZoomMenu'
|
import { ZoomMenu } from './ZoomMenu'
|
||||||
import { DashMenu } from './DashMenu'
|
import { StyleMenu } from './StyleMenu'
|
||||||
import { SizeMenu } from './SizeMenu'
|
|
||||||
import { FillCheckbox } from './FillCheckbox'
|
|
||||||
import { ColorMenu } from './ColorMenu'
|
|
||||||
import { Panel } from '~components/Panel'
|
import { Panel } from '~components/Panel'
|
||||||
|
|
||||||
interface TopPanelProps {
|
interface TopPanelProps {
|
||||||
|
@ -15,28 +12,29 @@ interface TopPanelProps {
|
||||||
showMenu: boolean
|
showMenu: boolean
|
||||||
showStyles: boolean
|
showStyles: boolean
|
||||||
showZoom: 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 (
|
return (
|
||||||
<StyledTopPanel>
|
<StyledTopPanel>
|
||||||
{(showMenu || showPages) && (
|
{(showMenu || showPages) && (
|
||||||
<Panel side="left">
|
<Panel side="left">
|
||||||
{showMenu && <Menu readOnly={readOnly} />}
|
{showMenu && <Menu showSponsorLink={showSponsorLink} readOnly={readOnly} />}
|
||||||
{showPages && <PageMenu />}
|
{showPages && <PageMenu />}
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
<StyledSpacer />
|
<StyledSpacer />
|
||||||
{(showStyles || showZoom) && (
|
{(showStyles || showZoom) && (
|
||||||
<Panel side="right">
|
<Panel side="right">
|
||||||
{showStyles && !readOnly && (
|
{showStyles && !readOnly && <StyleMenu />}
|
||||||
<>
|
|
||||||
<ColorMenu />
|
|
||||||
<SizeMenu />
|
|
||||||
<DashMenu />
|
|
||||||
<FillCheckbox />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{showZoom && <ZoomMenu />}
|
{showZoom && <ZoomMenu />}
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,43 +1,46 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import type { TLDrawSnapshot } from '~types'
|
import type { TDSnapshot } from '~types'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { DMItem, DMContent } from '~components/DropdownMenu'
|
import { DMItem, DMContent } from '~components/DropdownMenu'
|
||||||
import { ToolButton } from '~components/ToolButton'
|
import { ToolButton } from '~components/ToolButton'
|
||||||
|
import { preventEvent } from '~components/preventEvent'
|
||||||
|
|
||||||
const zoomSelector = (s: TLDrawSnapshot) =>
|
const zoomSelector = (s: TDSnapshot) => s.document.pageStates[s.appState.currentPageId].camera.zoom
|
||||||
s.document.pageStates[s.appState.currentPageId].camera.zoom
|
|
||||||
|
|
||||||
export function ZoomMenu() {
|
export const ZoomMenu = React.memo(function ZoomMenu() {
|
||||||
const { state, useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
const zoom = useSelector(zoomSelector)
|
|
||||||
|
const zoom = app.useStore(zoomSelector)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root dir="ltr">
|
||||||
<DropdownMenu.Trigger asChild>
|
<DropdownMenu.Trigger dir="ltr" asChild>
|
||||||
<FixedWidthToolButton variant="text">{Math.round(zoom * 100)}%</FixedWidthToolButton>
|
<FixedWidthToolButton onDoubleClick={app.resetZoom} variant="text">
|
||||||
|
{Math.round(zoom * 100)}%
|
||||||
|
</FixedWidthToolButton>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
<DMContent align="end">
|
<DMContent align="end">
|
||||||
<DMItem onSelect={state.zoomIn} kbd="#+">
|
<DMItem onSelect={preventEvent} onClick={app.zoomIn} kbd="#+">
|
||||||
Zoom In
|
Zoom In
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={state.zoomOut} kbd="#−">
|
<DMItem onSelect={preventEvent} onClick={app.zoomOut} kbd="#−">
|
||||||
Zoom Out
|
Zoom Out
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={state.resetZoom} kbd="⇧0">
|
<DMItem onSelect={preventEvent} onClick={app.resetZoom} kbd="⇧0">
|
||||||
To 100%
|
To 100%
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={state.zoomToFit} kbd="⇧1">
|
<DMItem onSelect={preventEvent} onClick={app.zoomToFit} kbd="⇧1">
|
||||||
To Fit
|
To Fit
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={state.zoomToSelection} kbd="⇧2">
|
<DMItem onSelect={preventEvent} onClick={app.zoomToSelection} kbd="⇧2">
|
||||||
To Selection
|
To Selection
|
||||||
</DMItem>
|
</DMItem>
|
||||||
</DMContent>
|
</DMContent>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const FixedWidthToolButton = styled(ToolButton, {
|
const FixedWidthToolButton = styled(ToolButton, {
|
||||||
minWidth: 56,
|
minWidth: 56,
|
||||||
|
|
|
@ -3,9 +3,11 @@ import * as React from 'react'
|
||||||
export function BoxIcon({
|
export function BoxIcon({
|
||||||
fill = 'none',
|
fill = 'none',
|
||||||
stroke = 'currentColor',
|
stroke = 'currentColor',
|
||||||
|
strokeWidth = 2,
|
||||||
}: {
|
}: {
|
||||||
fill?: string
|
fill?: string
|
||||||
stroke?: string
|
stroke?: string
|
||||||
|
strokeWidth?: number
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -13,10 +15,11 @@ export function BoxIcon({
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke={stroke}
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
fill={fill}
|
fill={fill}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
export function CircleIcon(
|
export function CircleIcon(
|
||||||
props: Pick<React.SVGProps<SVGSVGElement>, 'stroke' | 'fill'> & {
|
props: Pick<React.SVGProps<SVGSVGElement>, 'strokeWidth' | 'stroke' | 'fill'> & {
|
||||||
size: number
|
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"
|
rx="2"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
opacity=".3"
|
opacity=".9"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</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 './SizeSmallIcon'
|
||||||
export * from './SizeMediumIcon'
|
export * from './SizeMediumIcon'
|
||||||
export * from './SizeLargeIcon'
|
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 EMPTY_ARRAY = [] as any[]
|
||||||
export const SLOW_SPEED = 10
|
export const SLOW_SPEED = 10
|
||||||
export const VERY_SLOW_SPEED = 2.5
|
export const VERY_SLOW_SPEED = 2.5
|
||||||
|
export const GHOSTED_OPACITY = 0.3
|
||||||
|
export const DEAD_ZONE = 3
|
||||||
|
|
||||||
import type { Easing } from '~types'
|
import type { Easing } from '~types'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export * from './useKeyboardShortcuts'
|
export * from './useKeyboardShortcuts'
|
||||||
export * from './useTLDrawContext'
|
export * from './useTldrawApp'
|
||||||
export * from './useTheme'
|
export * from './useTheme'
|
||||||
export * from './useStylesheet'
|
export * from './useStylesheet'
|
||||||
export * from './useFileSystemHandlers'
|
export * from './useFileSystemHandlers'
|
||||||
|
|
|
@ -1,41 +1,41 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { TLDrawState } from '~state'
|
import type { TldrawApp } from '~state'
|
||||||
|
|
||||||
export function useFileSystem() {
|
export function useFileSystem() {
|
||||||
const promptSaveBeforeChange = React.useCallback(async (state: TLDrawState) => {
|
const promptSaveBeforeChange = React.useCallback(async (app: TldrawApp) => {
|
||||||
if (state.isDirty) {
|
if (app.isDirty) {
|
||||||
if (state.fileSystemHandle) {
|
if (app.fileSystemHandle) {
|
||||||
if (window.confirm('Do you want to save changes to your current project?')) {
|
if (window.confirm('Do you want to save changes to your current project?')) {
|
||||||
await state.saveProject()
|
await app.saveProject()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (window.confirm('Do you want to save your current project?')) {
|
if (window.confirm('Do you want to save your current project?')) {
|
||||||
await state.saveProject()
|
await app.saveProject()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onNewProject = React.useCallback(
|
const onNewProject = React.useCallback(
|
||||||
async (state: TLDrawState) => {
|
async (app: TldrawApp) => {
|
||||||
await promptSaveBeforeChange(state)
|
await promptSaveBeforeChange(app)
|
||||||
state.newProject()
|
app.newProject()
|
||||||
},
|
},
|
||||||
[promptSaveBeforeChange]
|
[promptSaveBeforeChange]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSaveProject = React.useCallback((state: TLDrawState) => {
|
const onSaveProject = React.useCallback((app: TldrawApp) => {
|
||||||
state.saveProject()
|
app.saveProject()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onSaveProjectAs = React.useCallback((state: TLDrawState) => {
|
const onSaveProjectAs = React.useCallback((app: TldrawApp) => {
|
||||||
state.saveProjectAs()
|
app.saveProjectAs()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onOpenProject = React.useCallback(
|
const onOpenProject = React.useCallback(
|
||||||
async (state: TLDrawState) => {
|
async (app: TldrawApp) => {
|
||||||
await promptSaveBeforeChange(state)
|
await promptSaveBeforeChange(app)
|
||||||
state.openProject()
|
app.openProject()
|
||||||
},
|
},
|
||||||
[promptSaveBeforeChange]
|
[promptSaveBeforeChange]
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,39 +1,39 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useTLDrawContext } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
|
|
||||||
export function useFileSystemHandlers() {
|
export function useFileSystemHandlers() {
|
||||||
const { state } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const onNewProject = React.useCallback(
|
const onNewProject = React.useCallback(
|
||||||
async (e?: KeyboardEvent) => {
|
async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||||
state.callbacks.onNewProject?.(state)
|
app.callbacks.onNewProject?.(app)
|
||||||
},
|
},
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSaveProject = React.useCallback(
|
const onSaveProject = React.useCallback(
|
||||||
(e?: KeyboardEvent) => {
|
(e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||||
state.callbacks.onSaveProject?.(state)
|
app.callbacks.onSaveProject?.(app)
|
||||||
},
|
},
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSaveProjectAs = React.useCallback(
|
const onSaveProjectAs = React.useCallback(
|
||||||
(e?: KeyboardEvent) => {
|
(e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||||
state.callbacks.onSaveProjectAs?.(state)
|
app.callbacks.onSaveProjectAs?.(app)
|
||||||
},
|
},
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onOpenProject = React.useCallback(
|
const onOpenProject = React.useCallback(
|
||||||
async (e?: KeyboardEvent) => {
|
async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||||
if (e && state.callbacks.onOpenProject) e.preventDefault()
|
if (e && app.callbacks.onOpenProject) e.preventDefault()
|
||||||
state.callbacks.onOpenProject?.(state)
|
app.callbacks.onOpenProject?.(app)
|
||||||
},
|
},
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { TLDrawShapeType } from '~types'
|
import { TDShapeType } from '~types'
|
||||||
import { useFileSystemHandlers, useTLDrawContext } from '~hooks'
|
import { useFileSystemHandlers, useTldrawApp } from '~hooks'
|
||||||
|
|
||||||
export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
const { state } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const canHandleEvent = React.useCallback(() => {
|
const canHandleEvent = React.useCallback(() => {
|
||||||
const elm = ref.current
|
const elm = ref.current
|
||||||
|
@ -16,63 +16,80 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'v,1',
|
'v,1',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.selectTool('select')
|
if (!canHandleEvent()) return
|
||||||
|
app.selectTool('select')
|
||||||
},
|
},
|
||||||
[state, ref.current]
|
[app, ref.current]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'd,2',
|
'd,2',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Draw)
|
if (!canHandleEvent()) return
|
||||||
|
app.selectTool(TDShapeType.Draw)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'r,3',
|
'e,3',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Rectangle)
|
if (!canHandleEvent()) return
|
||||||
|
app.selectTool('erase')
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'e,4',
|
'r,4',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Ellipse)
|
if (!canHandleEvent()) return
|
||||||
|
app.selectTool(TDShapeType.Rectangle)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'a,5',
|
'5',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Arrow)
|
if (!canHandleEvent()) return
|
||||||
|
app.selectTool(TDShapeType.Ellipse)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
't,6',
|
'a,6',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Text)
|
if (!canHandleEvent()) return
|
||||||
|
app.selectTool(TDShapeType.Arrow)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'n,7',
|
't,7',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.selectTool(TLDrawShapeType.Sticky)
|
if (!canHandleEvent()) return
|
||||||
|
app.selectTool(TDShapeType.Text)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'n,8',
|
||||||
|
() => {
|
||||||
|
if (!canHandleEvent()) return
|
||||||
|
app.selectTool(TDShapeType.Sticky)
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
/* ---------------------- Misc ---------------------- */
|
/* ---------------------- Misc ---------------------- */
|
||||||
|
@ -82,13 +99,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+shift+d,command+shift+d',
|
'ctrl+shift+d,command+shift+d',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
state.toggleDarkMode()
|
app.toggleDarkMode()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Focus Mode
|
// Focus Mode
|
||||||
|
@ -96,10 +112,11 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+.,command+.',
|
'ctrl+.,command+.',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.toggleFocusMode()
|
if (!canHandleEvent()) return
|
||||||
|
app.toggleFocusMode()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// File System
|
// File System
|
||||||
|
@ -109,43 +126,43 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+n,command+n',
|
'ctrl+n,command+n',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
onNewProject(e)
|
|
||||||
}
|
onNewProject(e)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+s,command+s',
|
'ctrl+s,command+s',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
onSaveProject(e)
|
|
||||||
}
|
onSaveProject(e)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+shift+s,command+shift+s',
|
'ctrl+shift+s,command+shift+s',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
onSaveProjectAs(e)
|
|
||||||
}
|
onSaveProjectAs(e)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+o,command+o',
|
'ctrl+o,command+o',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
onOpenProject(e)
|
|
||||||
}
|
onOpenProject(e)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Undo Redo
|
// Undo Redo
|
||||||
|
@ -153,31 +170,31 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+z,ctrl+z',
|
'command+z,ctrl+z',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
if (state.session) {
|
|
||||||
state.cancelSession()
|
if (app.session) {
|
||||||
} else {
|
app.cancelSession()
|
||||||
state.undo()
|
} else {
|
||||||
}
|
app.undo()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+shift-z,command+shift+z',
|
'ctrl+shift-z,command+shift+z',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
if (state.session) {
|
|
||||||
state.cancelSession()
|
if (app.session) {
|
||||||
} else {
|
app.cancelSession()
|
||||||
state.redo()
|
} else {
|
||||||
}
|
app.redo()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Undo Redo
|
// Undo Redo
|
||||||
|
@ -185,19 +202,21 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+u,ctrl+u',
|
'command+u,ctrl+u',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.undoSelect()
|
if (!canHandleEvent()) return
|
||||||
|
app.undoSelect()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+shift-u,command+shift+u',
|
'ctrl+shift-u,command+shift+u',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.redoSelect()
|
if (!canHandleEvent()) return
|
||||||
|
app.redoSelect()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
/* -------------------- Commands -------------------- */
|
/* -------------------- Commands -------------------- */
|
||||||
|
@ -207,52 +226,55 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+=,command+=',
|
'ctrl+=,command+=',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
state.zoomIn()
|
|
||||||
e.preventDefault()
|
app.zoomIn()
|
||||||
}
|
e.preventDefault()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+-,command+-',
|
'ctrl+-,command+-',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
state.zoomOut()
|
|
||||||
e.preventDefault()
|
app.zoomOut()
|
||||||
}
|
e.preventDefault()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+1',
|
'shift+1',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.zoomToFit()
|
if (!canHandleEvent()) return
|
||||||
|
app.zoomToFit()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+2',
|
'shift+2',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.zoomToSelection()
|
if (!canHandleEvent()) return
|
||||||
|
app.zoomToSelection()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+0',
|
'shift+0',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.resetZoom()
|
if (!canHandleEvent()) return
|
||||||
|
app.resetZoom()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Duplicate
|
// Duplicate
|
||||||
|
@ -260,13 +282,13 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+d,command+d',
|
'ctrl+d,command+d',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
state.duplicate()
|
|
||||||
e.preventDefault()
|
app.duplicate()
|
||||||
}
|
e.preventDefault()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Flip
|
// Flip
|
||||||
|
@ -274,19 +296,21 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+h',
|
'shift+h',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.flipHorizontal()
|
if (!canHandleEvent()) return
|
||||||
|
app.flipHorizontal()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+v',
|
'shift+v',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.flipVertical()
|
if (!canHandleEvent()) return
|
||||||
|
app.flipVertical()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cancel
|
// Cancel
|
||||||
|
@ -294,12 +318,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'escape',
|
'escape',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
state.cancel()
|
|
||||||
}
|
app.cancel()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delete
|
// Delete
|
||||||
|
@ -307,10 +331,11 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'backspace',
|
'backspace',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.delete()
|
if (!canHandleEvent()) return
|
||||||
|
app.delete()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Select All
|
// Select All
|
||||||
|
@ -318,10 +343,11 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+a,ctrl+a',
|
'command+a,ctrl+a',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.selectAll()
|
if (!canHandleEvent()) return
|
||||||
|
app.selectAll()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Nudge
|
// Nudge
|
||||||
|
@ -329,73 +355,91 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'up',
|
'up',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.nudge([0, -1], false)
|
if (!canHandleEvent()) return
|
||||||
|
app.nudge([0, -1], false)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'right',
|
'right',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.nudge([1, 0], false)
|
if (!canHandleEvent()) return
|
||||||
|
app.nudge([1, 0], false)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'down',
|
'down',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.nudge([0, 1], false)
|
if (!canHandleEvent()) return
|
||||||
|
app.nudge([0, 1], false)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'left',
|
'left',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.nudge([-1, 0], false)
|
if (!canHandleEvent()) return
|
||||||
|
app.nudge([-1, 0], false)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+up',
|
'shift+up',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.nudge([0, -1], true)
|
if (!canHandleEvent()) return
|
||||||
|
app.nudge([0, -1], true)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+right',
|
'shift+right',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.nudge([1, 0], true)
|
if (!canHandleEvent()) return
|
||||||
|
app.nudge([1, 0], true)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+down',
|
'shift+down',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.nudge([0, 1], true)
|
if (!canHandleEvent()) return
|
||||||
|
app.nudge([0, 1], true)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+left',
|
'shift+left',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.nudge([-1, 0], true)
|
if (!canHandleEvent()) return
|
||||||
|
app.nudge([-1, 0], true)
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'command+shift+l,ctrl+shift+l',
|
||||||
|
() => {
|
||||||
|
if (!canHandleEvent()) return
|
||||||
|
app.toggleLocked()
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Copy, Cut & Paste
|
// Copy, Cut & Paste
|
||||||
|
@ -403,28 +447,31 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+c,ctrl+c',
|
'command+c,ctrl+c',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.copy()
|
if (!canHandleEvent()) return
|
||||||
|
app.copy()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+x,ctrl+x',
|
'command+x,ctrl+x',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.cut()
|
if (!canHandleEvent()) return
|
||||||
|
app.cut()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+v,ctrl+v',
|
'command+v,ctrl+v',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.paste()
|
if (!canHandleEvent()) return
|
||||||
|
app.paste()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Group & Ungroup
|
// Group & Ungroup
|
||||||
|
@ -432,25 +479,25 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+g,ctrl+g',
|
'command+g,ctrl+g',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
state.group()
|
|
||||||
e.preventDefault()
|
app.group()
|
||||||
}
|
e.preventDefault()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+shift+g,ctrl+shift+g',
|
'command+shift+g,ctrl+shift+g',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
state.ungroup()
|
|
||||||
e.preventDefault()
|
app.ungroup()
|
||||||
}
|
e.preventDefault()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Move
|
// Move
|
||||||
|
@ -458,50 +505,53 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'[',
|
'[',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.moveBackward()
|
if (!canHandleEvent()) return
|
||||||
|
app.moveBackward()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
']',
|
']',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.moveForward()
|
if (!canHandleEvent()) return
|
||||||
|
app.moveForward()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+[',
|
'shift+[',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.moveToBack()
|
if (!canHandleEvent()) return
|
||||||
|
app.moveToBack()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'shift+]',
|
'shift+]',
|
||||||
() => {
|
() => {
|
||||||
if (canHandleEvent()) state.moveToFront()
|
if (!canHandleEvent()) return
|
||||||
|
app.moveToFront()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+shift+backspace',
|
'command+shift+backspace',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (canHandleEvent()) {
|
if (!canHandleEvent()) return
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (app.settings.isDebugMode) {
|
||||||
state.resetDocument()
|
app.resetDocument()
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
}
|
||||||
|
e.preventDefault()
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
[state]
|
[app]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@ import * as React from 'react'
|
||||||
|
|
||||||
const styles = new Map<string, HTMLStyleElement>()
|
const styles = new Map<string, HTMLStyleElement>()
|
||||||
|
|
||||||
const UID = `tldraw-fonts`
|
const UID = `Tldraw-fonts`
|
||||||
const CSS = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&display=swap')`
|
const CSS = `
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&display=swap')
|
||||||
|
`
|
||||||
|
|
||||||
export function useStylesheet() {
|
export function useStylesheet() {
|
||||||
React.useLayoutEffect(() => {
|
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 type { TDSnapshot, Theme } from '~types'
|
||||||
import { useTLDrawContext } from './useTLDrawContext'
|
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() {
|
export function useTheme() {
|
||||||
const { state, useSelector } = useTLDrawContext()
|
const app = useTldrawApp()
|
||||||
const theme = useSelector(themeSelector)
|
const theme = app.useStore(themeSelector)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme,
|
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 './types'
|
||||||
export * from './state/shapes'
|
export * from './state/shapes'
|
||||||
export { TLDrawState } from './state'
|
export { TldrawApp } from './state'
|
||||||
export { useFileSystem } from './hooks'
|
export { useFileSystem } from './hooks'
|
||||||
|
|
|
@ -1,36 +1,36 @@
|
||||||
import { TLBounds, TLTransformInfo, Utils, TLPageState } from '@tldraw/core'
|
import { TLBounds, TLTransformInfo, Utils, TLPageState } from '@tldraw/core'
|
||||||
import {
|
import {
|
||||||
TLDrawSnapshot,
|
TDSnapshot,
|
||||||
ShapeStyles,
|
ShapeStyles,
|
||||||
ShapesWithProp,
|
ShapesWithProp,
|
||||||
TLDrawShape,
|
TDShape,
|
||||||
TLDrawBinding,
|
TDBinding,
|
||||||
TLDrawPage,
|
TDPage,
|
||||||
TLDrawCommand,
|
TldrawCommand,
|
||||||
TLDrawPatch,
|
TldrawPatch,
|
||||||
TLDrawShapeType,
|
TDShapeType,
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import type { TLDrawShapeUtil } from './shapes/TLDrawShapeUtil'
|
import type { TDShapeUtil } from './shapes/TDShapeUtil'
|
||||||
import { getShapeUtils } from './shapes'
|
import { getShapeUtil } from './shapes'
|
||||||
|
|
||||||
export class TLDR {
|
export class TLDR {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
static getShapeUtils<T extends TLDrawShape>(shape: T): TLDrawShapeUtil<T>
|
static getShapeUtil<T extends TDShape>(shape: T): TDShapeUtil<T>
|
||||||
static getShapeUtils<T extends TLDrawShape>(shape: T | T['type']) {
|
static getShapeUtil<T extends TDShape>(shape: T | T['type']) {
|
||||||
return getShapeUtils<T>(shape)
|
return getShapeUtil<T>(shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSelectedShapes(data: TLDrawSnapshot, pageId: string) {
|
static getSelectedShapes(data: TDSnapshot, pageId: string) {
|
||||||
const page = TLDR.getPage(data, pageId)
|
const page = TLDR.getPage(data, pageId)
|
||||||
const selectedIds = TLDR.getSelectedIds(data, pageId)
|
const selectedIds = TLDR.getSelectedIds(data, pageId)
|
||||||
return selectedIds.map((id) => page.shapes[id])
|
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
|
const camera = TLDR.getPageState(data, data.appState.currentPageId).camera
|
||||||
return Vec.sub(Vec.div(point, camera.zoom), camera.point)
|
return Vec.sub(Vec.div(point, camera.zoom), camera.point)
|
||||||
}
|
}
|
||||||
|
@ -39,59 +39,59 @@ export class TLDR {
|
||||||
return Utils.clamp(zoom, 0.1, 5)
|
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]
|
return data.document.pages[pageId]
|
||||||
}
|
}
|
||||||
|
|
||||||
static getPageState(data: TLDrawSnapshot, pageId: string): TLPageState {
|
static getPageState(data: TDSnapshot, pageId: string): TLPageState {
|
||||||
return data.document.pageStates[pageId]
|
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
|
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)
|
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
|
return TLDR.getPageState(data, pageId).camera
|
||||||
}
|
}
|
||||||
|
|
||||||
static getShape<T extends TLDrawShape = TLDrawShape>(
|
static getShape<T extends TDShape = TDShape>(
|
||||||
data: TLDrawSnapshot,
|
data: TDSnapshot,
|
||||||
shapeId: string,
|
shapeId: string,
|
||||||
pageId: string
|
pageId: string
|
||||||
): T {
|
): T {
|
||||||
return TLDR.getPage(data, pageId).shapes[shapeId] as T
|
return TLDR.getPage(data, pageId).shapes[shapeId] as T
|
||||||
}
|
}
|
||||||
|
|
||||||
static getCenter<T extends TLDrawShape>(shape: T) {
|
static getCenter<T extends TDShape>(shape: T) {
|
||||||
return TLDR.getShapeUtils(shape).getCenter(shape)
|
return TLDR.getShapeUtil(shape).getCenter(shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getBounds<T extends TLDrawShape>(shape: T) {
|
static getBounds<T extends TDShape>(shape: T) {
|
||||||
return TLDR.getShapeUtils(shape).getBounds(shape)
|
return TLDR.getShapeUtil(shape).getBounds(shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getRotatedBounds<T extends TLDrawShape>(shape: T) {
|
static getRotatedBounds<T extends TDShape>(shape: T) {
|
||||||
return TLDR.getShapeUtils(shape).getRotatedBounds(shape)
|
return TLDR.getShapeUtil(shape).getRotatedBounds(shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSelectedBounds(data: TLDrawSnapshot): TLBounds {
|
static getSelectedBounds(data: TDSnapshot): TLBounds {
|
||||||
return Utils.getCommonBounds(
|
return Utils.getCommonBounds(
|
||||||
TLDR.getSelectedShapes(data, data.appState.currentPageId).map((shape) =>
|
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
|
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 page = TLDR.getPage(data, pageId)
|
||||||
// const pageState = TLDR.getPageState(data, data.appState.currentPageId)
|
// const pageState = TLDR.getPageState(data, data.appState.currentPageId)
|
||||||
// const shape = TLDR.getShape(data, id, pageId)
|
// const shape = TLDR.getShape(data, id, pageId)
|
||||||
|
@ -102,7 +102,7 @@ export class TLDR {
|
||||||
// : TLDR.getPointedId(data, shape.parentId, pageId)
|
// : 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 shape = TLDR.getShape(data, id, pageId)
|
||||||
// const { currentPageId } = data.appState
|
// const { currentPageId } = data.appState
|
||||||
// const { currentParentId, pointedId } = TLDR.getPageState(data, data.appState.currentPageId)
|
// const { currentParentId, pointedId } = TLDR.getPageState(data, data.appState.currentPageId)
|
||||||
|
@ -114,7 +114,7 @@ export class TLDR {
|
||||||
// : TLDR.getDrilledPointedId(data, shape.parentId, pageId)
|
// : 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 page = TLDR.getPage(data, pageId)
|
||||||
// const pageState = TLDR.getPageState(data, pageId)
|
// const pageState = TLDR.getPageState(data, pageId)
|
||||||
// const shape = TLDR.getShape(data, id, 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
|
// 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)
|
const shape = TLDR.getShape(data, id, pageId)
|
||||||
|
|
||||||
if (shape.children === undefined) return [id]
|
if (shape.children === undefined) return [id]
|
||||||
|
@ -142,16 +142,16 @@ export class TLDR {
|
||||||
|
|
||||||
// Get a deep array of unproxied shapes and their descendants
|
// Get a deep array of unproxied shapes and their descendants
|
||||||
static getSelectedBranchSnapshot<K>(
|
static getSelectedBranchSnapshot<K>(
|
||||||
data: TLDrawSnapshot,
|
data: TDSnapshot,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
fn: (shape: TLDrawShape) => K
|
fn: (shape: TDShape) => K
|
||||||
): ({ id: string } & K)[]
|
): ({ id: string } & K)[]
|
||||||
static getSelectedBranchSnapshot(data: TLDrawSnapshot, pageId: string): TLDrawShape[]
|
static getSelectedBranchSnapshot(data: TDSnapshot, pageId: string): TDShape[]
|
||||||
static getSelectedBranchSnapshot<K>(
|
static getSelectedBranchSnapshot<K>(
|
||||||
data: TLDrawSnapshot,
|
data: TDSnapshot,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
fn?: (shape: TLDrawShape) => K
|
fn?: (shape: TDShape) => K
|
||||||
): (TLDrawShape | K)[] {
|
): (TDShape | K)[] {
|
||||||
const page = TLDR.getPage(data, pageId)
|
const page = TLDR.getPage(data, pageId)
|
||||||
|
|
||||||
const copies = TLDR.getSelectedIds(data, pageId)
|
const copies = TLDR.getSelectedIds(data, pageId)
|
||||||
|
@ -167,17 +167,17 @@ export class TLDR {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a shallow array of unproxied shapes
|
// Get a shallow array of unproxied shapes
|
||||||
static getSelectedShapeSnapshot(data: TLDrawSnapshot, pageId: string): TLDrawShape[]
|
static getSelectedShapeSnapshot(data: TDSnapshot, pageId: string): TDShape[]
|
||||||
static getSelectedShapeSnapshot<K>(
|
static getSelectedShapeSnapshot<K>(
|
||||||
data: TLDrawSnapshot,
|
data: TDSnapshot,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
fn?: (shape: TLDrawShape) => K
|
fn?: (shape: TDShape) => K
|
||||||
): ({ id: string } & K)[]
|
): ({ id: string } & K)[]
|
||||||
static getSelectedShapeSnapshot<K>(
|
static getSelectedShapeSnapshot<K>(
|
||||||
data: TLDrawSnapshot,
|
data: TDSnapshot,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
fn?: (shape: TLDrawShape) => K
|
fn?: (shape: TDShape) => K
|
||||||
): (TLDrawShape | K)[] {
|
): (TDShape | K)[] {
|
||||||
const copies = TLDR.getSelectedShapes(data, pageId)
|
const copies = TLDR.getSelectedShapes(data, pageId)
|
||||||
.filter((shape) => !shape.isLocked)
|
.filter((shape) => !shape.isLocked)
|
||||||
.map(Utils.deepClone)
|
.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.
|
// 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.
|
// 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 page = TLDR.getPage(data, pageId)
|
||||||
|
|
||||||
const visited = new Set(ids)
|
const visited = new Set(ids)
|
||||||
|
@ -200,7 +200,7 @@ export class TLDR {
|
||||||
const shape = page.shapes[id]
|
const shape = page.shapes[id]
|
||||||
|
|
||||||
// Add descendant shapes
|
// Add descendant shapes
|
||||||
function collectDescendants(shape: TLDrawShape): void {
|
function collectDescendants(shape: TDShape): void {
|
||||||
if (shape.children === undefined) return
|
if (shape.children === undefined) return
|
||||||
shape.children
|
shape.children
|
||||||
.filter((childId) => !visited.has(childId))
|
.filter((childId) => !visited.has(childId))
|
||||||
|
@ -213,7 +213,7 @@ export class TLDR {
|
||||||
collectDescendants(shape)
|
collectDescendants(shape)
|
||||||
|
|
||||||
// Add asecendant shapes
|
// Add asecendant shapes
|
||||||
function collectAscendants(shape: TLDrawShape): void {
|
function collectAscendants(shape: TDShape): void {
|
||||||
const parentId = shape.parentId
|
const parentId = shape.parentId
|
||||||
if (parentId === page.id) return
|
if (parentId === page.id) return
|
||||||
if (visited.has(parentId)) return
|
if (visited.has(parentId)) return
|
||||||
|
@ -236,47 +236,47 @@ export class TLDR {
|
||||||
}
|
}
|
||||||
|
|
||||||
static updateBindings(
|
static updateBindings(
|
||||||
data: TLDrawSnapshot,
|
data: TDSnapshot,
|
||||||
id: string,
|
id: string,
|
||||||
beforeShapes: Record<string, Partial<TLDrawShape>> = {},
|
beforeShapes: Record<string, Partial<TDShape>> = {},
|
||||||
afterShapes: Record<string, Partial<TLDrawShape>> = {},
|
afterShapes: Record<string, Partial<TDShape>> = {},
|
||||||
pageId: string
|
pageId: string
|
||||||
): TLDrawSnapshot {
|
): TDSnapshot {
|
||||||
const page = { ...TLDR.getPage(data, pageId) }
|
const page = { ...TLDR.getPage(data, pageId) }
|
||||||
return Object.values(page.bindings)
|
return Object.values(page.bindings)
|
||||||
.filter((binding) => binding.fromId === id || binding.toId === id)
|
.filter((binding) => binding.fromId === id || binding.toId === id)
|
||||||
.reduce((cTLDrawSnapshot, binding) => {
|
.reduce((cTDSnapshot, binding) => {
|
||||||
if (!beforeShapes[binding.fromId]) {
|
if (!beforeShapes[binding.fromId]) {
|
||||||
beforeShapes[binding.fromId] = Utils.deepClone(
|
beforeShapes[binding.fromId] = Utils.deepClone(
|
||||||
TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId)
|
TLDR.getShape(cTDSnapshot, binding.fromId, pageId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!beforeShapes[binding.toId]) {
|
if (!beforeShapes[binding.toId]) {
|
||||||
beforeShapes[binding.toId] = Utils.deepClone(
|
beforeShapes[binding.toId] = Utils.deepClone(
|
||||||
TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId)
|
TLDR.getShape(cTDSnapshot, binding.toId, pageId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TLDR.onBindingChange(
|
TLDR.onBindingChange(
|
||||||
TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId),
|
TLDR.getShape(cTDSnapshot, binding.fromId, pageId),
|
||||||
binding,
|
binding,
|
||||||
TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId)
|
TLDR.getShape(cTDSnapshot, binding.toId, pageId)
|
||||||
)
|
)
|
||||||
|
|
||||||
afterShapes[binding.fromId] = Utils.deepClone(
|
afterShapes[binding.fromId] = Utils.deepClone(
|
||||||
TLDR.getShape(cTLDrawSnapshot, binding.fromId, pageId)
|
TLDR.getShape(cTDSnapshot, binding.fromId, pageId)
|
||||||
)
|
)
|
||||||
afterShapes[binding.toId] = Utils.deepClone(
|
afterShapes[binding.toId] = Utils.deepClone(
|
||||||
TLDR.getShape(cTLDrawSnapshot, binding.toId, pageId)
|
TLDR.getShape(cTDSnapshot, binding.toId, pageId)
|
||||||
)
|
)
|
||||||
|
|
||||||
return cTLDrawSnapshot
|
return cTDSnapshot
|
||||||
}, data)
|
}, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getLinkedShapes(
|
static getLinkedShapeIds(
|
||||||
data: TLDrawSnapshot,
|
data: TDSnapshot,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
direction: 'center' | 'left' | 'right',
|
direction: 'center' | 'left' | 'right',
|
||||||
includeArrows = true
|
includeArrows = true
|
||||||
|
@ -294,7 +294,7 @@ export class TLDR {
|
||||||
const arrows = new Set(
|
const arrows = new Set(
|
||||||
Object.values(page.shapes).filter((shape) => {
|
Object.values(page.shapes).filter((shape) => {
|
||||||
return (
|
return (
|
||||||
shape.type === TLDrawShapeType.Arrow &&
|
shape.type === TDShapeType.Arrow &&
|
||||||
(shape.handles.start.bindingId || shape.handles?.end.bindingId)
|
(shape.handles.start.bindingId || shape.handles?.end.bindingId)
|
||||||
)
|
)
|
||||||
}) as ArrowShape[]
|
}) as ArrowShape[]
|
||||||
|
@ -378,11 +378,11 @@ export class TLDR {
|
||||||
return Array.from(linkedIds.values())
|
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 page = data.document.pages[pageId]
|
||||||
const shape = page.shapes[id]
|
const shape = page.shapes[id]
|
||||||
|
|
||||||
let siblings: TLDrawShape[]
|
let siblings: TDShape[]
|
||||||
|
|
||||||
if (shape.parentId === page.id) {
|
if (shape.parentId === page.id) {
|
||||||
siblings = Object.values(page.shapes)
|
siblings = Object.values(page.shapes)
|
||||||
|
@ -409,27 +409,29 @@ export class TLDR {
|
||||||
/* Mutations */
|
/* 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(
|
return Object.fromEntries(
|
||||||
Object.keys(change).map((k) => [k, shape[k as keyof T]])
|
Object.keys(change).map((k) => [k, shape[k as keyof T]])
|
||||||
) as Partial<T>
|
) as Partial<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
static mutateShapes<T extends TLDrawShape>(
|
static mutateShapes<T extends TDShape>(
|
||||||
data: TLDrawSnapshot,
|
data: TDSnapshot,
|
||||||
ids: string[],
|
ids: string[],
|
||||||
fn: (shape: T, i: number) => Partial<T> | void,
|
fn: (shape: T, i: number) => Partial<T> | void,
|
||||||
pageId: string
|
pageId: string
|
||||||
): {
|
): {
|
||||||
before: Record<string, Partial<T>>
|
before: Record<string, Partial<T>>
|
||||||
after: Record<string, Partial<T>>
|
after: Record<string, Partial<T>>
|
||||||
data: TLDrawSnapshot
|
data: TDSnapshot
|
||||||
} {
|
} {
|
||||||
const beforeShapes: Record<string, Partial<T>> = {}
|
const beforeShapes: Record<string, Partial<T>> = {}
|
||||||
const afterShapes: Record<string, Partial<T>> = {}
|
const afterShapes: Record<string, Partial<T>> = {}
|
||||||
|
|
||||||
ids.forEach((id, i) => {
|
ids.forEach((id, i) => {
|
||||||
const shape = TLDR.getShape<T>(data, id, pageId)
|
const shape = TLDR.getShape<T>(data, id, pageId)
|
||||||
|
if (shape.isLocked) return
|
||||||
|
|
||||||
const change = fn(shape, i)
|
const change = fn(shape, i)
|
||||||
if (change) {
|
if (change) {
|
||||||
beforeShapes[id] = TLDR.getBeforeShape(shape, change)
|
beforeShapes[id] = TLDR.getBeforeShape(shape, change)
|
||||||
|
@ -446,8 +448,8 @@ export class TLDR {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const dataWithBindingChanges = ids.reduce<TLDrawSnapshot>((cTLDrawSnapshot, id) => {
|
const dataWithBindingChanges = ids.reduce<TDSnapshot>((cTDSnapshot, id) => {
|
||||||
return TLDR.updateBindings(cTLDrawSnapshot, id, beforeShapes, afterShapes, pageId)
|
return TLDR.updateBindings(cTDSnapshot, id, beforeShapes, afterShapes, pageId)
|
||||||
}, dataWithMutations)
|
}, dataWithMutations)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -457,17 +459,15 @@ export class TLDR {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static createShapes(data: TLDrawSnapshot, shapes: TLDrawShape[], pageId: string): TLDrawCommand {
|
static createShapes(data: TDSnapshot, shapes: TDShape[], pageId: string): TldrawCommand {
|
||||||
const before: TLDrawPatch = {
|
const before: TldrawPatch = {
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
[pageId]: {
|
[pageId]: {
|
||||||
shapes: {
|
shapes: {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
shapes.flatMap((shape) => {
|
shapes.flatMap((shape) => {
|
||||||
const results: [string, Partial<TLDrawShape> | undefined][] = [
|
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, undefined]]
|
||||||
[shape.id, undefined],
|
|
||||||
]
|
|
||||||
|
|
||||||
// If the shape is a child of another shape, also save that shape
|
// If the shape is a child of another shape, also save that shape
|
||||||
if (shape.parentId !== pageId) {
|
if (shape.parentId !== pageId) {
|
||||||
|
@ -485,7 +485,7 @@ export class TLDR {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const after: TLDrawPatch = {
|
const after: TldrawPatch = {
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
[pageId]: {
|
[pageId]: {
|
||||||
|
@ -493,9 +493,7 @@ export class TLDR {
|
||||||
shapes: {
|
shapes: {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
shapes.flatMap((shape) => {
|
shapes.flatMap((shape) => {
|
||||||
const results: [string, Partial<TLDrawShape> | undefined][] = [
|
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, shape]]
|
||||||
[shape.id, shape],
|
|
||||||
]
|
|
||||||
|
|
||||||
// If the shape is a child of a different shape, update its parent
|
// If the shape is a child of a different shape, update its parent
|
||||||
if (shape.parentId !== pageId) {
|
if (shape.parentId !== pageId) {
|
||||||
|
@ -521,10 +519,10 @@ export class TLDR {
|
||||||
}
|
}
|
||||||
|
|
||||||
static deleteShapes(
|
static deleteShapes(
|
||||||
data: TLDrawSnapshot,
|
data: TDSnapshot,
|
||||||
shapes: TLDrawShape[] | string[],
|
shapes: TDShape[] | string[],
|
||||||
pageId?: string
|
pageId?: string
|
||||||
): TLDrawCommand {
|
): TldrawCommand {
|
||||||
pageId = pageId ? pageId : data.appState.currentPageId
|
pageId = pageId ? pageId : data.appState.currentPageId
|
||||||
|
|
||||||
const page = TLDR.getPage(data, pageId)
|
const page = TLDR.getPage(data, pageId)
|
||||||
|
@ -532,9 +530,9 @@ export class TLDR {
|
||||||
const shapeIds =
|
const shapeIds =
|
||||||
typeof shapes[0] === 'string'
|
typeof shapes[0] === 'string'
|
||||||
? (shapes as 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: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
[pageId]: {
|
[pageId]: {
|
||||||
|
@ -543,7 +541,7 @@ export class TLDR {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
shapeIds.flatMap((id) => {
|
shapeIds.flatMap((id) => {
|
||||||
const shape = page.shapes[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 the shape is a child of another shape, also add that shape
|
||||||
if (shape.parentId !== pageId) {
|
if (shape.parentId !== pageId) {
|
||||||
|
@ -573,7 +571,7 @@ export class TLDR {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const after: TLDrawPatch = {
|
const after: TldrawPatch = {
|
||||||
document: {
|
document: {
|
||||||
pages: {
|
pages: {
|
||||||
[pageId]: {
|
[pageId]: {
|
||||||
|
@ -581,9 +579,7 @@ export class TLDR {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
shapeIds.flatMap((id) => {
|
shapeIds.flatMap((id) => {
|
||||||
const shape = page.shapes[id]
|
const shape = page.shapes[id]
|
||||||
const results: [string, Partial<TLDrawShape> | undefined][] = [
|
const results: [string, Partial<TDShape> | undefined][] = [[shape.id, undefined]]
|
||||||
[shape.id, undefined],
|
|
||||||
]
|
|
||||||
|
|
||||||
// If the shape is a child of a different shape, update its parent
|
// If the shape is a child of a different shape, update its parent
|
||||||
if (shape.parentId !== page.id) {
|
if (shape.parentId !== page.id) {
|
||||||
|
@ -612,16 +608,16 @@ export class TLDR {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static onSessionComplete<T extends TLDrawShape>(shape: T) {
|
static onSessionComplete<T extends TDShape>(shape: T) {
|
||||||
const delta = TLDR.getShapeUtils(shape).onSessionComplete?.(shape)
|
const delta = TLDR.getShapeUtil(shape).onSessionComplete?.(shape)
|
||||||
if (!delta) return shape
|
if (!delta) return shape
|
||||||
return { ...shape, ...delta }
|
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
|
if (!shape.children) return
|
||||||
|
|
||||||
const delta = TLDR.getShapeUtils(shape).onChildrenChange?.(
|
const delta = TLDR.getShapeUtil(shape).onChildrenChange?.(
|
||||||
shape,
|
shape,
|
||||||
shape.children.map((id) => TLDR.getShape(data, id, pageId))
|
shape.children.map((id) => TLDR.getShape(data, id, pageId))
|
||||||
)
|
)
|
||||||
|
@ -631,35 +627,27 @@ export class TLDR {
|
||||||
return { ...shape, ...delta }
|
return { ...shape, ...delta }
|
||||||
}
|
}
|
||||||
|
|
||||||
static onBindingChange<T extends TLDrawShape>(
|
static onBindingChange<T extends TDShape>(shape: T, binding: TDBinding, otherShape: TDShape) {
|
||||||
shape: T,
|
const delta = TLDR.getShapeUtil(shape).onBindingChange?.(
|
||||||
binding: TLDrawBinding,
|
|
||||||
otherShape: TLDrawShape
|
|
||||||
) {
|
|
||||||
const delta = TLDR.getShapeUtils(shape).onBindingChange?.(
|
|
||||||
shape,
|
shape,
|
||||||
binding,
|
binding,
|
||||||
otherShape,
|
otherShape,
|
||||||
TLDR.getShapeUtils(otherShape).getBounds(otherShape),
|
TLDR.getShapeUtil(otherShape).getBounds(otherShape),
|
||||||
TLDR.getShapeUtils(otherShape).getCenter(otherShape)
|
TLDR.getShapeUtil(otherShape).getCenter(otherShape)
|
||||||
)
|
)
|
||||||
if (!delta) return shape
|
if (!delta) return shape
|
||||||
|
|
||||||
return { ...shape, ...delta }
|
return { ...shape, ...delta }
|
||||||
}
|
}
|
||||||
|
|
||||||
static transform<T extends TLDrawShape>(shape: T, bounds: TLBounds, info: TLTransformInfo<T>) {
|
static transform<T extends TDShape>(shape: T, bounds: TLBounds, info: TLTransformInfo<T>) {
|
||||||
const delta = TLDR.getShapeUtils(shape).transform(shape, bounds, info)
|
const delta = TLDR.getShapeUtil(shape).transform(shape, bounds, info)
|
||||||
if (!delta) return shape
|
if (!delta) return shape
|
||||||
return { ...shape, ...delta }
|
return { ...shape, ...delta }
|
||||||
}
|
}
|
||||||
|
|
||||||
static transformSingle<T extends TLDrawShape>(
|
static transformSingle<T extends TDShape>(shape: T, bounds: TLBounds, info: TLTransformInfo<T>) {
|
||||||
shape: T,
|
const delta = TLDR.getShapeUtil(shape).transformSingle(shape, bounds, info)
|
||||||
bounds: TLBounds,
|
|
||||||
info: TLTransformInfo<T>
|
|
||||||
) {
|
|
||||||
const delta = TLDR.getShapeUtils(shape).transformSingle(shape, bounds, info)
|
|
||||||
if (!delta) return shape
|
if (!delta) return shape
|
||||||
return { ...shape, ...delta }
|
return { ...shape, ...delta }
|
||||||
}
|
}
|
||||||
|
@ -671,7 +659,7 @@ export class TLDR {
|
||||||
* @param origin the page point to rotate around.
|
* @param origin the page point to rotate around.
|
||||||
* @param rotation the amount to rotate the shape.
|
* @param rotation the amount to rotate the shape.
|
||||||
*/
|
*/
|
||||||
static getRotatedShapeMutation<T extends TLDrawShape>(
|
static getRotatedShapeMutation<T extends TDShape>(
|
||||||
shape: T, // in page space
|
shape: T, // in page space
|
||||||
center: number[], // in page space
|
center: number[], // in page space
|
||||||
origin: number[], // in page space (probably the center of common bounds)
|
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,
|
// of rotating the shape. Shapes with handles should never be rotated,
|
||||||
// because that makes a lot of other things incredible difficult.
|
// because that makes a lot of other things incredible difficult.
|
||||||
if (shape.handles !== undefined) {
|
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
|
// Base the change on a shape with the next point
|
||||||
{ ...shape, point: nextPoint },
|
{ ...shape, point: nextPoint },
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
|
@ -723,7 +711,7 @@ export class TLDR {
|
||||||
/* Parents */
|
/* 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)
|
const page = TLDR.getPage(data, pageId)
|
||||||
|
|
||||||
if (changedShapeIds.length === 0) return
|
if (changedShapeIds.length === 0) return
|
||||||
|
@ -747,7 +735,7 @@ export class TLDR {
|
||||||
TLDR.updateParents(data, pageId, parentToUpdateIds)
|
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 { currentStyle } = data.appState
|
||||||
|
|
||||||
const page = data.document.pages[pageId]
|
const page = data.document.pages[pageId]
|
||||||
|
@ -788,27 +776,23 @@ export class TLDR {
|
||||||
/* Bindings */
|
/* 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]
|
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)
|
const page = TLDR.getPage(data, pageId)
|
||||||
return Object.values(page.bindings)
|
return Object.values(page.bindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getBindableShapeIds(data: TLDrawSnapshot) {
|
static getBindableShapeIds(data: TDSnapshot) {
|
||||||
return TLDR.getShapes(data, data.appState.currentPageId)
|
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)
|
.sort((a, b) => b.childIndex - a.childIndex)
|
||||||
.map((shape) => shape.id)
|
.map((shape) => shape.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getBindingsWithShapeIds(
|
static getBindingsWithShapeIds(data: TDSnapshot, ids: string[], pageId: string): TDBinding[] {
|
||||||
data: TLDrawSnapshot,
|
|
||||||
ids: string[],
|
|
||||||
pageId: string
|
|
||||||
): TLDrawBinding[] {
|
|
||||||
return Array.from(
|
return Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
TLDR.getBindings(data, pageId).filter((binding) => {
|
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 changedShapeIds = new Set(ids)
|
||||||
|
|
||||||
const page = TLDR.getPage(data, pageId)
|
const page = TLDR.getPage(data, pageId)
|
||||||
|
@ -897,7 +881,7 @@ export class TLDR {
|
||||||
/* Groups */
|
/* Groups */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
static flattenShape = (data: TLDrawSnapshot, shape: TLDrawShape): TLDrawShape[] => {
|
static flattenShape = (data: TDSnapshot, shape: TDShape): TDShape[] => {
|
||||||
return [
|
return [
|
||||||
shape,
|
shape,
|
||||||
...(shape.children ?? [])
|
...(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)
|
return Object.values(data.document.pages[pageId].shapes)
|
||||||
.sort((a, b) => a.childIndex - b.childIndex)
|
.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)
|
const shapes = TLDR.getShapes(data, pageId)
|
||||||
return shapes.length === 0
|
return shapes.length === 0
|
||||||
? 1
|
? 1
|
||||||
|
@ -922,12 +906,23 @@ export class TLDR {
|
||||||
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
.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 */
|
/* Assertions */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
||||||
static assertShapeHasProperty<P extends keyof TLDrawShape>(
|
static assertShapeHasProperty<P extends keyof TDShape>(
|
||||||
shape: TLDrawShape,
|
shape: TDShape,
|
||||||
prop: P
|
prop: P
|
||||||
): asserts shape is ShapesWithProp<P> {
|
): asserts shape is ShapesWithProp<P> {
|
||||||
if (shape[prop] === undefined) {
|
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
|
// 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 [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"after": Object {
|
"after": Object {
|
||||||
|
@ -36,6 +99,7 @@ Array [
|
||||||
"color": "black",
|
"color": "black",
|
||||||
"dash": "draw",
|
"dash": "draw",
|
||||||
"isFilled": false,
|
"isFilled": false,
|
||||||
|
"scale": 1,
|
||||||
"size": "small",
|
"size": "small",
|
||||||
},
|
},
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
|
@ -96,6 +160,7 @@ Array [
|
||||||
"color": "black",
|
"color": "black",
|
||||||
"dash": "draw",
|
"dash": "draw",
|
||||||
"isFilled": false,
|
"isFilled": false,
|
||||||
|
"scale": 1,
|
||||||
"size": "small",
|
"size": "small",
|
||||||
},
|
},
|
||||||
"type": "rectangle",
|
"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 [
|
Array [
|
||||||
"rect1",
|
"rect1",
|
||||||
"rect2",
|
"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 Vec from '@tldraw/vec'
|
||||||
import { TLDrawState } from '~state'
|
import { mockDocument, TldrawTestApp } from '~test'
|
||||||
import { mockDocument, TLDrawStateUtils } from '~test'
|
import { AlignType, TDShapeType } from '~types'
|
||||||
import { AlignType, TLDrawShapeType } from '~types'
|
|
||||||
|
|
||||||
describe('Align command', () => {
|
describe('Align command', () => {
|
||||||
const state = new TLDrawState()
|
const app = new TldrawTestApp()
|
||||||
|
|
||||||
describe('when less than two shapes are selected', () => {
|
describe('when less than two shapes are selected', () => {
|
||||||
it('does nothing', () => {
|
it('does nothing', () => {
|
||||||
state.loadDocument(mockDocument).select('rect2')
|
app.loadDocument(mockDocument).select('rect2')
|
||||||
const initialState = state.state
|
const initialState = app.state
|
||||||
state.align(AlignType.Top)
|
app.align(AlignType.Top)
|
||||||
const currentState = state.state
|
const currentState = app.state
|
||||||
|
|
||||||
expect(currentState).toEqual(initialState)
|
expect(currentState).toEqual(initialState)
|
||||||
})
|
})
|
||||||
|
@ -19,86 +18,89 @@ describe('Align command', () => {
|
||||||
|
|
||||||
describe('when multiple shapes are selected', () => {
|
describe('when multiple shapes are selected', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state.loadDocument(mockDocument)
|
app.loadDocument(mockDocument)
|
||||||
state.selectAll()
|
app.selectAll()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does, undoes and redoes command', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
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', () => {
|
it('aligns center horizontal', () => {
|
||||||
state.align(AlignType.CenterHorizontal)
|
app.align(AlignType.CenterHorizontal)
|
||||||
|
|
||||||
expect(state.getShape('rect1').point).toEqual([50, 0])
|
expect(app.getShape('rect1').point).toEqual([50, 0])
|
||||||
expect(state.getShape('rect2').point).toEqual([50, 100])
|
expect(app.getShape('rect2').point).toEqual([50, 100])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('aligns center vertical', () => {
|
it('aligns center vertical', () => {
|
||||||
state.align(AlignType.CenterVertical)
|
app.align(AlignType.CenterVertical)
|
||||||
|
|
||||||
expect(state.getShape('rect1').point).toEqual([0, 50])
|
expect(app.getShape('rect1').point).toEqual([0, 50])
|
||||||
expect(state.getShape('rect2').point).toEqual([100, 50])
|
expect(app.getShape('rect2').point).toEqual([100, 50])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when aligning groups', () => {
|
describe('when aligning groups', () => {
|
||||||
it('aligns children', () => {
|
it('aligns children', () => {
|
||||||
const state = new TLDrawState()
|
const app = new TldrawTestApp()
|
||||||
.createShapes(
|
.createShapes(
|
||||||
{ id: 'rect1', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [100, 100] },
|
{ id: 'rect1', type: TDShapeType.Rectangle, point: [0, 0], size: [100, 100] },
|
||||||
{ id: 'rect2', type: TLDrawShapeType.Rectangle, point: [100, 100], size: [100, 100] },
|
{ id: 'rect2', type: TDShapeType.Rectangle, point: [100, 100], size: [100, 100] },
|
||||||
{ id: 'rect3', type: TLDrawShapeType.Rectangle, point: [200, 200], size: [100, 100] },
|
{ id: 'rect3', type: TDShapeType.Rectangle, point: [200, 200], size: [100, 100] },
|
||||||
{ id: 'rect4', type: TLDrawShapeType.Rectangle, point: [0, 0], size: [200, 200] }
|
{ id: 'rect4', type: TDShapeType.Rectangle, point: [0, 0], size: [200, 200] }
|
||||||
)
|
)
|
||||||
.group(['rect1', 'rect2'], 'groupA')
|
.group(['rect1', 'rect2'], 'groupA')
|
||||||
.select('rect3', 'rect4')
|
.select('rect3', 'rect4')
|
||||||
.align(AlignType.CenterVertical)
|
.align(AlignType.CenterVertical)
|
||||||
|
|
||||||
const p0 = state.getShape('rect4').point
|
const p0 = app.getShape('rect4').point
|
||||||
const p1 = state.getShape('rect3').point
|
const p1 = app.getShape('rect3').point
|
||||||
|
|
||||||
state.undo().delete(['rect4']).selectAll().align(AlignType.CenterVertical)
|
app
|
||||||
|
.undo()
|
||||||
new TLDrawStateUtils(state).expectShapesToBeAtPoints({
|
.delete(['rect4'])
|
||||||
rect1: p0,
|
.selectAll()
|
||||||
rect2: Vec.add(p0, [100, 100]),
|
.align(AlignType.CenterVertical)
|
||||||
rect3: p1,
|
.expectShapesToBeAtPoints({
|
||||||
})
|
rect1: p0,
|
||||||
|
rect2: Vec.add(p0, [100, 100]),
|
||||||
|
rect3: p1,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
import { AlignType, TLDrawCommand, TLDrawShapeType } from '~types'
|
import { AlignType, TldrawCommand, TDShapeType } from '~types'
|
||||||
import type { TLDrawSnapshot } from '~types'
|
import type { TDSnapshot } from '~types'
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
import Vec from '@tldraw/vec'
|
import Vec from '@tldraw/vec'
|
||||||
|
import type { TldrawApp } from '../../internal'
|
||||||
|
|
||||||
export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType): TLDrawCommand {
|
export function alignShapes(app: TldrawApp, ids: string[], type: AlignType): TldrawCommand {
|
||||||
const { currentPageId } = data.appState
|
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) => {
|
const boundsForShapes = initialShapes.map((shape) => {
|
||||||
return {
|
return {
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
point: [...shape.point],
|
point: [...shape.point],
|
||||||
bounds: TLDR.getShapeUtils(shape).getBounds(shape),
|
bounds: TLDR.getBounds(shape),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const commonBounds = Utils.getCommonBounds(boundsForShapes.map(({ bounds }) => bounds))
|
const commonBounds = Utils.getCommonBounds(boundsForShapes.map(({ bounds }) => bounds))
|
||||||
|
|
||||||
const midX = commonBounds.minX + commonBounds.width / 2
|
const midX = commonBounds.minX + commonBounds.width / 2
|
||||||
|
|
||||||
const midY = commonBounds.minY + commonBounds.height / 2
|
const midY = commonBounds.minY + commonBounds.height / 2
|
||||||
|
|
||||||
const deltaMap = Object.fromEntries(
|
const deltaMap = Object.fromEntries(
|
||||||
|
@ -44,7 +44,7 @@ export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType
|
||||||
)
|
)
|
||||||
|
|
||||||
const { before, after } = TLDR.mutateShapes(
|
const { before, after } = TLDR.mutateShapes(
|
||||||
data,
|
app.state,
|
||||||
ids,
|
ids,
|
||||||
(shape) => {
|
(shape) => {
|
||||||
if (!deltaMap[shape.id]) return shape
|
if (!deltaMap[shape.id]) return shape
|
||||||
|
@ -54,11 +54,11 @@ export function alignShapes(data: TLDrawSnapshot, ids: string[], type: AlignType
|
||||||
)
|
)
|
||||||
|
|
||||||
initialShapes.forEach((shape) => {
|
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!)
|
const delta = Vec.sub(after[shape.id].point!, before[shape.id].point!)
|
||||||
|
|
||||||
shape.children.forEach((id) => {
|
shape.children.forEach((id) => {
|
||||||
const child = TLDR.getShape(data, id, currentPageId)
|
const child = app.getShape(id)
|
||||||
before[child.id] = { point: child.point }
|
before[child.id] = { point: child.point }
|
||||||
after[child.id] = { point: Vec.add(child.point, delta) }
|
after[child.id] = { point: Vec.add(child.point, delta) }
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,32 +1,31 @@
|
||||||
import { TLDrawState } from '~state'
|
import { mockDocument, TldrawTestApp } from '~test'
|
||||||
import { mockDocument } from '~test'
|
|
||||||
|
|
||||||
describe('Change page command', () => {
|
describe('Change page command', () => {
|
||||||
const state = new TLDrawState()
|
const app = new TldrawTestApp()
|
||||||
|
|
||||||
it('does, undoes and redoes command', () => {
|
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 {
|
return {
|
||||||
id: 'change_page',
|
id: 'change_page',
|
||||||
before: {
|
before: {
|
||||||
appState: {
|
appState: {
|
||||||
currentPageId: data.appState.currentPageId,
|
currentPageId: app.currentPageId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
after: {
|
after: {
|
||||||
|
|
|
@ -1,34 +1,33 @@
|
||||||
import { TLDrawState } from '~state'
|
import { mockDocument, TldrawTestApp } from '~test'
|
||||||
import { mockDocument } from '~test'
|
|
||||||
|
|
||||||
describe('Create page command', () => {
|
describe('Create page command', () => {
|
||||||
const state = new TLDrawState()
|
const app = new TldrawTestApp()
|
||||||
|
|
||||||
it('does, undoes and redoes command', () => {
|
it('does, undoes and redoes command', () => {
|
||||||
state.loadDocument(mockDocument)
|
app.loadDocument(mockDocument)
|
||||||
|
|
||||||
const initialId = state.page.id
|
const initialId = app.page.id
|
||||||
const initialPageState = state.pageState
|
const initialPageState = app.pageState
|
||||||
|
|
||||||
state.createPage()
|
app.createPage()
|
||||||
|
|
||||||
const nextId = state.page.id
|
const nextId = app.page.id
|
||||||
const nextPageState = state.pageState
|
const nextPageState = app.pageState
|
||||||
|
|
||||||
expect(Object.keys(state.document.pages).length).toBe(2)
|
expect(Object.keys(app.document.pages).length).toBe(2)
|
||||||
expect(state.page.id).toBe(nextId)
|
expect(app.page.id).toBe(nextId)
|
||||||
expect(state.pageState).toEqual(nextPageState)
|
expect(app.pageState).toEqual(nextPageState)
|
||||||
|
|
||||||
state.undo()
|
app.undo()
|
||||||
|
|
||||||
expect(Object.keys(state.document.pages).length).toBe(1)
|
expect(Object.keys(app.document.pages).length).toBe(1)
|
||||||
expect(state.page.id).toBe(initialId)
|
expect(app.page.id).toBe(initialId)
|
||||||
expect(state.pageState).toEqual(initialPageState)
|
expect(app.pageState).toEqual(initialPageState)
|
||||||
|
|
||||||
state.redo()
|
app.redo()
|
||||||
|
|
||||||
expect(Object.keys(state.document.pages).length).toBe(2)
|
expect(Object.keys(app.document.pages).length).toBe(2)
|
||||||
expect(state.page.id).toBe(nextId)
|
expect(app.page.id).toBe(nextId)
|
||||||
expect(state.pageState).toEqual(nextPageState)
|
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 { Utils, TLPageState } from '@tldraw/core'
|
||||||
|
import type { TldrawApp } from '~state'
|
||||||
|
|
||||||
export function createPage(
|
export function createPage(
|
||||||
data: TLDrawSnapshot,
|
app: TldrawApp,
|
||||||
center: number[],
|
center: number[],
|
||||||
pageId = Utils.uniqueId()
|
pageId = Utils.uniqueId()
|
||||||
): TLDrawCommand {
|
): TldrawCommand {
|
||||||
const { currentPageId } = data.appState
|
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)
|
(a, b) => (b.childIndex || 0) - (a.childIndex || 0)
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
|
@ -17,7 +18,7 @@ export function createPage(
|
||||||
// TODO: Iterate the name better
|
// TODO: Iterate the name better
|
||||||
const nextName = `New Page`
|
const nextName = `New Page`
|
||||||
|
|
||||||
const page: TLDrawPage = {
|
const page: TDPage = {
|
||||||
id: pageId,
|
id: pageId,
|
||||||
name: nextName,
|
name: nextName,
|
||||||
childIndex: nextChildIndex,
|
childIndex: nextChildIndex,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue