tldraw zero - package shuffle (#1710)
This PR moves code between our packages so that: - @tldraw/editor is a “core” library with the engine and canvas but no shapes, tools, or other things - @tldraw/tldraw contains everything particular to the experience we’ve built for tldraw At first look, this might seem like a step away from customization and configuration, however I believe it greatly increases the configuration potential of the @tldraw/editor while also providing a more accurate reflection of what configuration options actually exist for @tldraw/tldraw. ## Library changes @tldraw/editor re-exports its dependencies and @tldraw/tldraw re-exports @tldraw/editor. - users of @tldraw/editor WITHOUT @tldraw/tldraw should almost always only import things from @tldraw/editor. - users of @tldraw/tldraw should almost always only import things from @tldraw/tldraw. - @tldraw/polyfills is merged into @tldraw/editor - @tldraw/indices is merged into @tldraw/editor - @tldraw/primitives is merged mostly into @tldraw/editor, partially into @tldraw/tldraw - @tldraw/file-format is merged into @tldraw/tldraw - @tldraw/ui is merged into @tldraw/tldraw Many (many) utils and other code is moved from the editor to tldraw. For example, embeds now are entirely an feature of @tldraw/tldraw. The only big chunk of code left in core is related to arrow handling. ## API Changes The editor can now be used without tldraw's assets. We load them in @tldraw/tldraw instead, so feel free to use whatever fonts or images or whatever that you like with the editor. All tools and shapes (except for the `Group` shape) are moved to @tldraw/tldraw. This includes the `select` tool. You should use the editor with at least one tool, however, so you now also need to send in an `initialState` prop to the Editor / <TldrawEditor> component indicating which state the editor should begin in. The `components` prop now also accepts `SelectionForeground`. The complex selection component that we use for tldraw is moved to @tldraw/tldraw. The default component is quite basic but can easily be replaced via the `components` prop. We pass down our tldraw-flavored SelectionFg via `components`. Likewise with the `Scribble` component: the `DefaultScribble` no longer uses our freehand tech and is a simple path instead. We pass down the tldraw-flavored scribble via `components`. The `ExternalContentManager` (`Editor.externalContentManager`) is removed and replaced with a mapping of types to handlers. - Register new content handlers with `Editor.registerExternalContentHandler`. - Register new asset creation handlers (for files and URLs) with `Editor.registerExternalAssetHandler` ### Change Type - [x] `major` — Breaking change ### Test Plan - [x] Unit Tests - [x] End to end tests ### Release Notes - [@tldraw/editor] lots, wip - [@tldraw/ui] gone, merged to tldraw/tldraw - [@tldraw/polyfills] gone, merged to tldraw/editor - [@tldraw/primitives] gone, merged to tldraw/editor / tldraw/tldraw - [@tldraw/indices] gone, merged to tldraw/editor - [@tldraw/file-format] gone, merged to tldraw/tldraw --------- Co-authored-by: alex <alex@dytry.ch>
This commit is contained in:
parent
43a0dd83f8
commit
b7d9c8684c
618 changed files with 8939 additions and 11666 deletions
|
@ -76,5 +76,11 @@ module.exports = {
|
||||||
'import/no-internal-modules': 'off',
|
'import/no-internal-modules': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// files: ['packages/tldraw/src/test/**/*'],
|
||||||
|
// rules: {
|
||||||
|
// 'import/no-internal-modules': 'off',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,15 +82,10 @@ This repository's contents is divided across four primary sections:
|
||||||
|
|
||||||
- `assets`: a library for working with tldraw's fonts and translations
|
- `assets`: a library for working with tldraw's fonts and translations
|
||||||
- `editor`: the tldraw editor
|
- `editor`: the tldraw editor
|
||||||
- `file-format`: a library for working with tldraw's `.tldr` file format
|
|
||||||
- `indices`: a library for working with tldraw's indices
|
|
||||||
- `polyfills`: a collection of polyfills used by tldraw
|
|
||||||
- `primitives`: low-level primitives for working with vectors and geometry
|
|
||||||
- `state`: a signals library, also known as signia
|
- `state`: a signals library, also known as signia
|
||||||
- `store`: an in-memory reactive database
|
- `store`: an in-memory reactive database
|
||||||
- `tldraw`: the main tldraw package containing both the editor and the UI
|
- `tldraw`: the main tldraw package containing both the editor and the UI
|
||||||
- `tlschema`: shape definitions and migrations
|
- `tlschema`: shape definitions and migrations
|
||||||
- `ui`: the editor's user interface
|
|
||||||
- `utils`: low-level data utilities shared by other libraries
|
- `utils`: low-level data utilities shared by other libraries
|
||||||
- `validate`: a validation library used for run-time validation
|
- `validate`: a validation library used for run-time validation
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export async function setupPage(page: PlaywrightTestArgs['page']) {
|
||||||
await page.goto('http://localhost:5420/end-to-end')
|
await page.goto('http://localhost:5420/end-to-end')
|
||||||
await page.waitForSelector('.tl-canvas')
|
await page.waitForSelector('.tl-canvas')
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
editor.setAnimationSpeed(0)
|
editor.animationSpeed = 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,11 @@
|
||||||
import test, { Page, expect } from '@playwright/test'
|
import test, { expect } from '@playwright/test'
|
||||||
import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
|
import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
|
||||||
import { assert } from '@tldraw/utils'
|
|
||||||
import { rename, writeFile } from 'fs/promises'
|
import { rename, writeFile } from 'fs/promises'
|
||||||
import { setupPage } from '../shared-e2e'
|
import { setupPage } from '../shared-e2e'
|
||||||
|
|
||||||
let page: Page
|
|
||||||
declare const editor: Editor
|
declare const editor: Editor
|
||||||
|
|
||||||
test.describe('Export snapshots', () => {
|
test.describe('Export snapshots', () => {
|
||||||
test.beforeAll(async ({ browser }) => {
|
|
||||||
page = await browser.newPage()
|
|
||||||
})
|
|
||||||
test.beforeEach(async () => {
|
|
||||||
await setupPage(page)
|
|
||||||
})
|
|
||||||
|
|
||||||
const snapshots = {} as Record<string, TLShapePartial[]>
|
const snapshots = {} as Record<string, TLShapePartial[]>
|
||||||
|
|
||||||
for (const fill of ['none', 'semi', 'solid', 'pattern']) {
|
for (const fill of ['none', 'semi', 'solid', 'pattern']) {
|
||||||
|
@ -172,7 +163,9 @@ test.describe('Export snapshots', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, shapes] of Object.entries(snapshots)) {
|
for (const [name, shapes] of Object.entries(snapshots)) {
|
||||||
test(`Exports with ${name}`, async () => {
|
test(`Exports with ${name}`, async ({ browser }) => {
|
||||||
|
const page = await browser.newPage()
|
||||||
|
await setupPage(page)
|
||||||
await page.evaluate((shapes) => {
|
await page.evaluate((shapes) => {
|
||||||
editor
|
editor
|
||||||
.updateInstanceState({ exportBackground: false })
|
.updateInstanceState({ exportBackground: false })
|
||||||
|
@ -188,17 +181,17 @@ test.describe('Export snapshots', () => {
|
||||||
await page.click('[data-testid="menu-item.export-as-svg"]')
|
await page.click('[data-testid="menu-item.export-as-svg"]')
|
||||||
|
|
||||||
const download = await downloadEvent
|
const download = await downloadEvent
|
||||||
const path = await download.path()
|
const path = (await download.path()) as string
|
||||||
assert(path)
|
// assert(path)
|
||||||
await rename(path, path + '.svg')
|
await rename(path, path + '.svg')
|
||||||
await writeFile(
|
await writeFile(
|
||||||
path + '.html',
|
path + '.html',
|
||||||
`
|
`
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<img src="${path}.svg" />
|
<img src="${path}.svg" />
|
||||||
`,
|
`,
|
||||||
'utf-8'
|
'utf-8'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -327,3 +327,32 @@ test.describe('Keyboard Shortcuts', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Delete bug', () => {
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
page = await browser.newPage()
|
||||||
|
await setupPage(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('delete bug without drag', async () => {
|
||||||
|
await page.keyboard.press('r')
|
||||||
|
await page.mouse.click(100, 100)
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||||
|
name: 'delete-shapes',
|
||||||
|
data: { source: 'kbd' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('delete bug with drag', async () => {
|
||||||
|
await page.keyboard.press('r')
|
||||||
|
await page.mouse.move(100, 100)
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.up()
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||||
|
name: 'delete-shapes',
|
||||||
|
data: { source: 'kbd' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -37,10 +37,7 @@
|
||||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||||
"@playwright/test": "^1.35.1",
|
"@playwright/test": "^1.35.1",
|
||||||
"@tldraw/assets": "workspace:*",
|
"@tldraw/assets": "workspace:*",
|
||||||
"@tldraw/state": "workspace:*",
|
|
||||||
"@tldraw/tldraw": "workspace:*",
|
"@tldraw/tldraw": "workspace:*",
|
||||||
"@tldraw/utils": "workspace:*",
|
|
||||||
"@tldraw/validate": "workspace:*",
|
|
||||||
"@vercel/analytics": "^1.0.1",
|
"@vercel/analytics": "^1.0.1",
|
||||||
"lazyrepo": "0.0.0-alpha.27",
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|
|
@ -2,24 +2,30 @@ import { Tldraw, TLEditorComponents } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
|
|
||||||
const components: Partial<TLEditorComponents> = {
|
const components: Partial<TLEditorComponents> = {
|
||||||
Brush: ({ brush }) => (
|
Brush: function MyBrush({ brush }) {
|
||||||
<rect
|
return (
|
||||||
className="tl-brush"
|
<svg className="tl-overlays__item">
|
||||||
stroke="red"
|
<rect
|
||||||
fill="none"
|
className="tl-brush"
|
||||||
width={Math.max(1, brush.w)}
|
stroke="red"
|
||||||
height={Math.max(1, brush.h)}
|
fill="none"
|
||||||
transform={`translate(${brush.x},${brush.y})`}
|
width={Math.max(1, brush.w)}
|
||||||
/>
|
height={Math.max(1, brush.h)}
|
||||||
),
|
transform={`translate(${brush.x},${brush.y})`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
},
|
||||||
Scribble: ({ scribble, opacity, color }) => {
|
Scribble: ({ scribble, opacity, color }) => {
|
||||||
return (
|
return (
|
||||||
<polyline
|
<svg className="tl-overlays__item">
|
||||||
points={scribble.points.map((p) => `${p.x},${p.y}`).join(' ')}
|
<polyline
|
||||||
stroke={color ?? 'black'}
|
points={scribble.points.map((p) => `${p.x},${p.y}`).join(' ')}
|
||||||
opacity={opacity ?? '1'}
|
stroke={color ?? 'black'}
|
||||||
fill="none"
|
opacity={opacity ?? '1'}
|
||||||
/>
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
SnapLine: null,
|
SnapLine: null,
|
||||||
|
|
|
@ -8,6 +8,12 @@ const MOVING_CURSOR_SPEED = 0.25 // 0 is stopped, 1 is full send
|
||||||
const MOVING_CURSOR_RADIUS = 100
|
const MOVING_CURSOR_RADIUS = 100
|
||||||
const CURSOR_CHAT_MESSAGE = 'Hey, I think this is just great.'
|
const CURSOR_CHAT_MESSAGE = 'Hey, I think this is just great.'
|
||||||
|
|
||||||
|
// Note:
|
||||||
|
// Almost all of the information below is calculated automatically by helpers in the editor.
|
||||||
|
// For a more realistic implementation, see the yjs example in this examples folder. If anything,
|
||||||
|
// this example should be used to understand the data model and test designs, not as a reference
|
||||||
|
// for how to implement user presence.
|
||||||
|
|
||||||
export default function UserPresenceExample() {
|
export default function UserPresenceExample() {
|
||||||
const rRaf = useRef<any>(-1)
|
const rRaf = useRef<any>(-1)
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { Tldraw, createTLStore, defaultShapes } from '@tldraw/tldraw'
|
import { Tldraw, createTLStore, defaultShapeUtils, throttle } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import { throttle } from '@tldraw/utils'
|
|
||||||
import { useLayoutEffect, useState } from 'react'
|
import { useLayoutEffect, useState } from 'react'
|
||||||
|
|
||||||
const PERSISTENCE_KEY = 'example-3'
|
const PERSISTENCE_KEY = 'example-3'
|
||||||
|
|
||||||
export default function PersistenceExample() {
|
export default function PersistenceExample() {
|
||||||
const [store] = useState(() => createTLStore({ shapes: defaultShapes }))
|
const [store] = useState(() => createTLStore({ shapeUtils: defaultShapeUtils }))
|
||||||
const [loadingState, setLoadingState] = useState<
|
const [loadingState, setLoadingState] = useState<
|
||||||
{ status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string }
|
{ status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string }
|
||||||
>({
|
>({
|
||||||
|
|
|
@ -4,12 +4,11 @@ import {
|
||||||
DefaultColorStyle,
|
DefaultColorStyle,
|
||||||
HTMLContainer,
|
HTMLContainer,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
|
T,
|
||||||
TLBaseShape,
|
TLBaseShape,
|
||||||
TLDefaultColorStyle,
|
TLDefaultColorStyle,
|
||||||
defineShape,
|
|
||||||
getDefaultColorTheme,
|
getDefaultColorTheme,
|
||||||
} from '@tldraw/tldraw'
|
} from '@tldraw/tldraw'
|
||||||
import { T } from '@tldraw/validate'
|
|
||||||
|
|
||||||
// Define a style that can be used across multiple shapes.
|
// Define a style that can be used across multiple shapes.
|
||||||
// The ID (myApp:filter) must be globally unique, so we recommend prefixing it with a namespace.
|
// The ID (myApp:filter) must be globally unique, so we recommend prefixing it with a namespace.
|
||||||
|
@ -33,6 +32,15 @@ export type CardShape = TLBaseShape<
|
||||||
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
|
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
|
||||||
static override type = 'card' as const
|
static override type = 'card' as const
|
||||||
|
|
||||||
|
static override props = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
// You can re-use tldraw built-in styles...
|
||||||
|
color: DefaultColorStyle,
|
||||||
|
// ...or your own custom styles.
|
||||||
|
filter: MyFilterStyle,
|
||||||
|
}
|
||||||
|
|
||||||
override isAspectRatioLocked = (_shape: CardShape) => false
|
override isAspectRatioLocked = (_shape: CardShape) => false
|
||||||
override canResize = (_shape: CardShape) => true
|
override canResize = (_shape: CardShape) => true
|
||||||
override canBind = (_shape: CardShape) => true
|
override canBind = (_shape: CardShape) => true
|
||||||
|
@ -87,17 +95,12 @@ export class CardShapeTool extends BaseBoxShapeTool {
|
||||||
static override id = 'card'
|
static override id = 'card'
|
||||||
static override initial = 'idle'
|
static override initial = 'idle'
|
||||||
override shapeType = 'card'
|
override shapeType = 'card'
|
||||||
}
|
props = {
|
||||||
|
|
||||||
export const CardShape = defineShape('card', {
|
|
||||||
util: CardShapeUtil,
|
|
||||||
// to use a style prop, you need to describe all the props in your shape.
|
|
||||||
props: {
|
|
||||||
w: T.number,
|
w: T.number,
|
||||||
h: T.number,
|
h: T.number,
|
||||||
// You can re-use tldraw built-in styles...
|
// You can re-use tldraw built-in styles...
|
||||||
color: DefaultColorStyle,
|
color: DefaultColorStyle,
|
||||||
// ...or your own custom styles.
|
// ...or your own custom styles.
|
||||||
filter: MyFilterStyle,
|
filter: MyFilterStyle,
|
||||||
},
|
}
|
||||||
})
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Tldraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import { CardShape, CardShapeTool } from './CardShape'
|
import { CardShapeTool, CardShapeUtil } from './CardShape'
|
||||||
import { FilterStyleUi } from './FilterStyleUi'
|
import { FilterStyleUi } from './FilterStyleUi'
|
||||||
import { uiOverrides } from './ui-overrides'
|
import { uiOverrides } from './ui-overrides'
|
||||||
|
|
||||||
const shapes = [CardShape]
|
const customShapeUtils = [CardShapeUtil]
|
||||||
|
const customTools = [CardShapeTool]
|
||||||
|
|
||||||
export default function CustomStylesExample() {
|
export default function CustomStylesExample() {
|
||||||
return (
|
return (
|
||||||
|
@ -12,8 +13,8 @@ export default function CustomStylesExample() {
|
||||||
<Tldraw
|
<Tldraw
|
||||||
autoFocus
|
autoFocus
|
||||||
persistenceKey="custom-styles-example"
|
persistenceKey="custom-styles-example"
|
||||||
shapes={shapes}
|
shapeUtils={customShapeUtils}
|
||||||
tools={[CardShapeTool]}
|
tools={customTools}
|
||||||
overrides={uiOverrides}
|
overrides={uiOverrides}
|
||||||
>
|
>
|
||||||
<FilterStyleUi />
|
<FilterStyleUi />
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { track } from '@tldraw/state'
|
import { track, useEditor } from '@tldraw/tldraw'
|
||||||
import { useEditor } from '@tldraw/tldraw'
|
|
||||||
import { MyFilterStyle } from './CardShape'
|
import { MyFilterStyle } from './CardShape'
|
||||||
|
|
||||||
export const FilterStyleUi = track(function FilterStyleUi() {
|
export const FilterStyleUi = track(function FilterStyleUi() {
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const uiOverrides: TLUiOverrides = {
|
||||||
kbd: 'c',
|
kbd: 'c',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
editor.setSelectedTool('card')
|
editor.setCurrentTool('card')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return tools
|
return tools
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { defineShape } from '@tldraw/tldraw'
|
|
||||||
import { CardShapeUtil } from './CardShapeUtil'
|
|
||||||
import { cardShapeMigrations } from './card-shape-migrations'
|
|
||||||
import { cardShapeProps } from './card-shape-props'
|
|
||||||
|
|
||||||
// A custom shape is a bundle of a shape util, a tool, and props
|
|
||||||
export const CardShape = defineShape('card', {
|
|
||||||
// A utility class
|
|
||||||
util: CardShapeUtil,
|
|
||||||
// A tool that is used to create and edit the shape (optional)
|
|
||||||
// A validation schema for the shape's props (optional)
|
|
||||||
props: cardShapeProps,
|
|
||||||
// Migrations for upgrading shapes (optional)
|
|
||||||
migrations: cardShapeMigrations,
|
|
||||||
})
|
|
|
@ -7,6 +7,8 @@ import {
|
||||||
resizeBox,
|
resizeBox,
|
||||||
} from '@tldraw/tldraw'
|
} from '@tldraw/tldraw'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { cardShapeMigrations } from './card-shape-migrations'
|
||||||
|
import { cardShapeProps } from './card-shape-props'
|
||||||
import { ICardShape } from './card-shape-types'
|
import { ICardShape } from './card-shape-types'
|
||||||
|
|
||||||
// A utility class for the card shape. This is where you define
|
// A utility class for the card shape. This is where you define
|
||||||
|
@ -15,6 +17,10 @@ import { ICardShape } from './card-shape-types'
|
||||||
|
|
||||||
export class CardShapeUtil extends ShapeUtil<ICardShape> {
|
export class CardShapeUtil extends ShapeUtil<ICardShape> {
|
||||||
static override type = 'card' as const
|
static override type = 'card' as const
|
||||||
|
// A validation schema for the shape's props (optional)
|
||||||
|
static override props = cardShapeProps
|
||||||
|
// Migrations for upgrading shapes (optional)
|
||||||
|
static override migrations = cardShapeMigrations
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
override isAspectRatioLocked = (_shape: ICardShape) => false
|
override isAspectRatioLocked = (_shape: ICardShape) => false
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { DefaultColorStyle, ShapeProps, StyleProp } from '@tldraw/tldraw'
|
import { DefaultColorStyle, ShapeProps, StyleProp, T } from '@tldraw/tldraw'
|
||||||
import { T } from '@tldraw/validate'
|
|
||||||
import { ICardShape } from './card-shape-types'
|
import { ICardShape } from './card-shape-types'
|
||||||
|
|
||||||
export const WeightStyle = StyleProp.defineEnum('myApp:weight', {
|
export const WeightStyle = StyleProp.defineEnum('myApp:weight', {
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
import { Tldraw } from '@tldraw/tldraw'
|
import { Tldraw } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import { CardShapeTool } from './CardShape/CardShapeTool'
|
import { CardShapeTool } from './CardShape/CardShapeTool'
|
||||||
import { customShapes } from './custom-shapes'
|
import { CardShapeUtil } from './CardShape/CardShapeUtil'
|
||||||
import { uiOverrides } from './ui-overrides'
|
import { uiOverrides } from './ui-overrides'
|
||||||
|
|
||||||
|
const customShapeUtils = [CardShapeUtil]
|
||||||
|
const customTools = [CardShapeTool]
|
||||||
|
|
||||||
export default function CustomConfigExample() {
|
export default function CustomConfigExample() {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<Tldraw
|
<Tldraw
|
||||||
autoFocus
|
autoFocus
|
||||||
// Pass in the array of custom shape definitions
|
// Pass in the array of custom shape classes
|
||||||
shapes={customShapes}
|
shapeUtils={customShapeUtils}
|
||||||
// Pass in the array of custom tools
|
// Pass in the array of custom tool classes
|
||||||
tools={[CardShapeTool]}
|
tools={customTools}
|
||||||
// Pass in any overrides to the user interface
|
// Pass in any overrides to the user interface
|
||||||
overrides={uiOverrides}
|
overrides={uiOverrides}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { CardShape } from './CardShape/CardShape'
|
|
||||||
|
|
||||||
export const customShapes = [CardShape]
|
|
|
@ -12,7 +12,7 @@ export const uiOverrides: TLUiOverrides = {
|
||||||
kbd: 'c',
|
kbd: 'c',
|
||||||
readonlyOk: false,
|
readonlyOk: false,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
editor.setSelectedTool('card')
|
editor.setCurrentTool('card')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return tools
|
return tools
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { track } from '@tldraw/state'
|
import { Canvas, Tldraw, track, useEditor } from '@tldraw/tldraw'
|
||||||
import { Canvas, TldrawEditor, defaultShapes, defaultTools, useEditor } from '@tldraw/tldraw'
|
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import './custom-ui.css'
|
import './custom-ui.css'
|
||||||
|
@ -7,10 +6,10 @@ import './custom-ui.css'
|
||||||
export default function CustomUiExample() {
|
export default function CustomUiExample() {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<TldrawEditor shapes={defaultShapes} tools={defaultTools} autoFocus>
|
<Tldraw hideUi autoFocus>
|
||||||
<Canvas />
|
<Canvas />
|
||||||
<CustomUi />
|
<CustomUi />
|
||||||
</TldrawEditor>
|
</Tldraw>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -24,6 +23,22 @@ const CustomUi = track(() => {
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
case 'Backspace': {
|
case 'Backspace': {
|
||||||
editor.deleteShapes()
|
editor.deleteShapes()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'v': {
|
||||||
|
editor.setCurrentTool('select')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'e': {
|
||||||
|
editor.setCurrentTool('eraser')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'x':
|
||||||
|
case 'p':
|
||||||
|
case 'b':
|
||||||
|
case 'd': {
|
||||||
|
editor.setCurrentTool('draw')
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,21 +55,21 @@ const CustomUi = track(() => {
|
||||||
<button
|
<button
|
||||||
className="custom-button"
|
className="custom-button"
|
||||||
data-isactive={editor.currentToolId === 'select'}
|
data-isactive={editor.currentToolId === 'select'}
|
||||||
onClick={() => editor.setSelectedTool('select')}
|
onClick={() => editor.setCurrentTool('select')}
|
||||||
>
|
>
|
||||||
Select
|
Select
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="custom-button"
|
className="custom-button"
|
||||||
data-isactive={editor.currentToolId === 'draw'}
|
data-isactive={editor.currentToolId === 'draw'}
|
||||||
onClick={() => editor.setSelectedTool('draw')}
|
onClick={() => editor.setCurrentTool('draw')}
|
||||||
>
|
>
|
||||||
Pencil
|
Pencil
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="custom-button"
|
className="custom-button"
|
||||||
data-isactive={editor.currentToolId === 'eraser'}
|
data-isactive={editor.currentToolId === 'eraser'}
|
||||||
onClick={() => editor.setSelectedTool('eraser')}
|
onClick={() => editor.setCurrentTool('eraser')}
|
||||||
>
|
>
|
||||||
Eraser
|
Eraser
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
TldrawEditor,
|
TldrawEditor,
|
||||||
TldrawUi,
|
TldrawUi,
|
||||||
defaultShapes,
|
defaultShapeUtils,
|
||||||
defaultTools,
|
defaultTools,
|
||||||
} from '@tldraw/tldraw'
|
} from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
|
@ -12,7 +12,8 @@ export default function ExplodedExample() {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<TldrawEditor
|
<TldrawEditor
|
||||||
shapes={defaultShapes}
|
initialState="select"
|
||||||
|
shapeUtils={defaultShapeUtils}
|
||||||
tools={defaultTools}
|
tools={defaultTools}
|
||||||
autoFocus
|
autoFocus
|
||||||
persistenceKey="exploded-example"
|
persistenceKey="exploded-example"
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { createShapeId, Tldraw, TLShapePartial } from '@tldraw/tldraw'
|
import { createShapeId, Tldraw, TLShapePartial } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import { ErrorShape } from './ErrorShape'
|
import { ErrorShape, ErrorShapeUtil } from './ErrorShape'
|
||||||
|
|
||||||
const shapes = [ErrorShape]
|
const shapes = [ErrorShapeUtil]
|
||||||
|
|
||||||
export default function ErrorBoundaryExample() {
|
export default function ErrorBoundaryExample() {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<Tldraw
|
<Tldraw
|
||||||
shapes={shapes}
|
shapeUtils={shapes}
|
||||||
tools={[]}
|
tools={[]}
|
||||||
components={{
|
components={{
|
||||||
ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>, // use a custom error fallback for shapes
|
ShapeErrorFallback: ({ error }) => <div>Shape error! {String(error)}</div>, // use a custom error fallback for shapes
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { BaseBoxShapeUtil, TLBaseShape, defineShape } from '@tldraw/tldraw'
|
import { BaseBoxShapeUtil, TLBaseShape } from '@tldraw/tldraw'
|
||||||
|
|
||||||
export type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }>
|
export type ErrorShape = TLBaseShape<'error', { w: number; h: number; message: string }>
|
||||||
|
|
||||||
export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
|
export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
|
||||||
static override type = 'error' as const
|
static override type = 'error' as const
|
||||||
override type = 'error' as const
|
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
return { message: 'Error!', w: 100, h: 100 }
|
return { message: 'Error!', w: 100, h: 100 }
|
||||||
|
@ -16,5 +15,3 @@ export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
|
||||||
throw new Error(`Error shape indicator!`)
|
throw new Error(`Error shape indicator!`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ErrorShape = defineShape('error', { util: ErrorShapeUtil })
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { getAssetUrlsByMetaUrl } from '@tldraw/assets/urls'
|
import { getAssetUrlsByMetaUrl } from '@tldraw/assets/urls'
|
||||||
import {
|
import { DefaultErrorFallback, ErrorBoundary, setDefaultUiAssetUrls } from '@tldraw/tldraw'
|
||||||
DefaultErrorFallback,
|
import { setDefaultEditorAssetUrls } from '@tldraw/tldraw/src/lib/utils/assetUrls'
|
||||||
ErrorBoundary,
|
|
||||||
setDefaultEditorAssetUrls,
|
|
||||||
setDefaultUiAssetUrls,
|
|
||||||
} from '@tldraw/tldraw'
|
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
||||||
|
@ -28,6 +24,7 @@ import HideUiExample from './9-hide-ui/HideUiExample'
|
||||||
import ExamplesTldrawLogo from './ExamplesTldrawLogo'
|
import ExamplesTldrawLogo from './ExamplesTldrawLogo'
|
||||||
import { ListLink } from './components/ListLink'
|
import { ListLink } from './components/ListLink'
|
||||||
import EndToEnd from './end-to-end/end-to-end'
|
import EndToEnd from './end-to-end/end-to-end'
|
||||||
|
import OnlyEditorExample from './only-editor/OnlyEditor'
|
||||||
import YjsExample from './yjs/YjsExample'
|
import YjsExample from './yjs/YjsExample'
|
||||||
|
|
||||||
// This example is only used for end to end tests
|
// This example is only used for end to end tests
|
||||||
|
@ -50,6 +47,11 @@ export const allExamples: Example[] = [
|
||||||
path: '/develop',
|
path: '/develop',
|
||||||
element: <ExampleBasic />,
|
element: <ExampleBasic />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Collaboration (with Yjs)',
|
||||||
|
path: '/yjs',
|
||||||
|
element: <YjsExample />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Editor API',
|
title: 'Editor API',
|
||||||
path: '/api',
|
path: '/api',
|
||||||
|
@ -120,11 +122,6 @@ export const allExamples: Example[] = [
|
||||||
path: '/persistence',
|
path: '/persistence',
|
||||||
element: <PersistenceExample />,
|
element: <PersistenceExample />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Custom styles',
|
|
||||||
path: '/yjs',
|
|
||||||
element: <YjsExample />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Custom styles',
|
title: 'Custom styles',
|
||||||
path: '/custom-styles',
|
path: '/custom-styles',
|
||||||
|
@ -135,6 +132,11 @@ export const allExamples: Example[] = [
|
||||||
path: '/shape-meta',
|
path: '/shape-meta',
|
||||||
element: <ShapeMetaExample />,
|
element: <ShapeMetaExample />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Only editor',
|
||||||
|
path: '/only-editor',
|
||||||
|
element: <OnlyEditorExample />,
|
||||||
|
},
|
||||||
// not listed
|
// not listed
|
||||||
{
|
{
|
||||||
path: '/end-to-end',
|
path: '/end-to-end',
|
||||||
|
|
60
apps/examples/src/only-editor/MicroSelectTool.ts
Normal file
60
apps/examples/src/only-editor/MicroSelectTool.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { StateNode, TLEventHandlers, createShapeId } from '@tldraw/tldraw'
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is a very small example of a state node that implements a "select" tool.
|
||||||
|
|
||||||
|
The state handles two events: onPointerDown and onDoubleClick.
|
||||||
|
|
||||||
|
When the user points down on the canvas, it deselects all shapes; and when
|
||||||
|
they point a shape it selects that shape. When the user double clicks on the
|
||||||
|
canvas, it creates a new shape; and when they double click on a shape, it
|
||||||
|
deletes that shape.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class MicroSelectTool extends StateNode {
|
||||||
|
static override id = 'select'
|
||||||
|
|
||||||
|
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||||
|
const { editor } = this
|
||||||
|
|
||||||
|
switch (info.target) {
|
||||||
|
case 'canvas': {
|
||||||
|
editor.selectNone()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'shape': {
|
||||||
|
editor.select(info.shape.id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
|
||||||
|
const { editor } = this
|
||||||
|
|
||||||
|
if (info.phase !== 'up') return
|
||||||
|
|
||||||
|
switch (info.target) {
|
||||||
|
case 'canvas': {
|
||||||
|
const { currentPagePoint } = editor.inputs
|
||||||
|
editor.createShapes([
|
||||||
|
{
|
||||||
|
id: createShapeId(),
|
||||||
|
type: 'box',
|
||||||
|
x: currentPagePoint.x - 50,
|
||||||
|
y: currentPagePoint.y - 50,
|
||||||
|
props: {
|
||||||
|
w: 100,
|
||||||
|
h: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'shape': {
|
||||||
|
editor.deleteShapes([info.shape.id])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
apps/examples/src/only-editor/MiniBoxShape.tsx
Normal file
31
apps/examples/src/only-editor/MiniBoxShape.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape } from '@tldraw/tldraw'
|
||||||
|
|
||||||
|
export type MiniBoxShape = TLBaseShape<'box', { w: number; h: number; color: string }>
|
||||||
|
|
||||||
|
export class MiniBoxShapeUtil extends BaseBoxShapeUtil<MiniBoxShape> {
|
||||||
|
static override type = 'box'
|
||||||
|
|
||||||
|
override getDefaultProps(): MiniBoxShape['props'] {
|
||||||
|
return { w: 100, h: 100, color: '#efefef' }
|
||||||
|
}
|
||||||
|
|
||||||
|
component(shape: MiniBoxShape) {
|
||||||
|
return (
|
||||||
|
<HTMLContainer>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: shape.props.w,
|
||||||
|
height: shape.props.h,
|
||||||
|
border: '1px solid black',
|
||||||
|
backgroundColor: shape.props.color,
|
||||||
|
pointerEvents: 'all',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: MiniBoxShape) {
|
||||||
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
|
}
|
||||||
|
}
|
122
apps/examples/src/only-editor/MiniSelectTool.ts
Normal file
122
apps/examples/src/only-editor/MiniSelectTool.ts
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
import { StateNode, TLEventHandlers, TLUnknownShape, createShapeId } from '@tldraw/tldraw'
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is a bigger example of a state node that implements a "select" tool.
|
||||||
|
|
||||||
|
The state has three children: idle, pointing, and dragging. Only one child
|
||||||
|
state can be "active" at a time. The parent state's initial active state is
|
||||||
|
"idle". Certain events received by the child states will cause the parent
|
||||||
|
state to transition to another child state, making that state active instead.
|
||||||
|
|
||||||
|
Note that when `transition()` is called, the parent state will call the new
|
||||||
|
active state(s)'s `onEnter` method with the second argument passed to the
|
||||||
|
transition method. This is useful for passing data between states.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class IdleState extends StateNode {
|
||||||
|
static override id = 'idle'
|
||||||
|
|
||||||
|
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||||
|
const { editor } = this
|
||||||
|
|
||||||
|
switch (info.target) {
|
||||||
|
case 'canvas': {
|
||||||
|
editor.selectNone()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'selection': {
|
||||||
|
this.parent.transition('pointing', info)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'shape': {
|
||||||
|
if (editor.inputs.shiftKey) {
|
||||||
|
editor.select(...editor.selectedIds, info.shape.id)
|
||||||
|
} else {
|
||||||
|
if (!editor.isSelected(info.shape.id)) {
|
||||||
|
editor.select(info.shape.id)
|
||||||
|
}
|
||||||
|
this.parent.transition('pointing', info)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
|
||||||
|
const { editor } = this
|
||||||
|
|
||||||
|
if (info.phase !== 'up') return
|
||||||
|
|
||||||
|
switch (info.target) {
|
||||||
|
case 'canvas': {
|
||||||
|
const { currentPagePoint } = editor.inputs
|
||||||
|
editor.createShapes([
|
||||||
|
{
|
||||||
|
id: createShapeId(),
|
||||||
|
type: 'box',
|
||||||
|
x: currentPagePoint.x - 50,
|
||||||
|
y: currentPagePoint.y - 50,
|
||||||
|
props: {
|
||||||
|
w: 100,
|
||||||
|
h: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'shape': {
|
||||||
|
editor.deleteShapes([info.shape.id])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PointingState extends StateNode {
|
||||||
|
static override id = 'pointing'
|
||||||
|
|
||||||
|
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
|
||||||
|
this.parent.transition('idle', info)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
|
||||||
|
if (this.editor.inputs.isDragging) {
|
||||||
|
this.parent.transition('dragging', { shapes: [...this.editor.selectedShapes] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DraggingState extends StateNode {
|
||||||
|
static override id = 'dragging'
|
||||||
|
|
||||||
|
private initialDraggingShapes = [] as TLUnknownShape[]
|
||||||
|
|
||||||
|
override onEnter = (info: { shapes: TLUnknownShape[] }) => {
|
||||||
|
this.initialDraggingShapes = info.shapes
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
|
||||||
|
this.parent.transition('idle', info)
|
||||||
|
}
|
||||||
|
|
||||||
|
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
|
||||||
|
const { initialDraggingShapes } = this
|
||||||
|
const { originPagePoint, currentPagePoint } = this.editor.inputs
|
||||||
|
|
||||||
|
this.editor.updateShapes(
|
||||||
|
initialDraggingShapes.map((shape) => {
|
||||||
|
return {
|
||||||
|
...shape,
|
||||||
|
x: shape.x + (currentPagePoint.x - originPagePoint.x),
|
||||||
|
y: shape.y + (currentPagePoint.y - originPagePoint.y),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MiniSelectTool extends StateNode {
|
||||||
|
static override id = 'select'
|
||||||
|
static override children = () => [IdleState, PointingState, DraggingState]
|
||||||
|
static override initial = 'idle'
|
||||||
|
}
|
51
apps/examples/src/only-editor/OnlyEditor.tsx
Normal file
51
apps/examples/src/only-editor/OnlyEditor.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
|
||||||
|
import { Editor, PositionedOnCanvas, TldrawEditor, createShapeId, track } from '@tldraw/editor'
|
||||||
|
import '@tldraw/editor/editor.css'
|
||||||
|
import { MiniBoxShapeUtil } from './MiniBoxShape'
|
||||||
|
import { MiniSelectTool } from './MiniSelectTool'
|
||||||
|
|
||||||
|
const myTools = [MiniSelectTool]
|
||||||
|
const myShapeUtils = [MiniBoxShapeUtil]
|
||||||
|
|
||||||
|
export default function OnlyEditorExample() {
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<TldrawEditor
|
||||||
|
autoFocus
|
||||||
|
tools={myTools}
|
||||||
|
shapeUtils={myShapeUtils}
|
||||||
|
initialState="select"
|
||||||
|
onMount={(editor: Editor) => {
|
||||||
|
editor
|
||||||
|
.selectAll()
|
||||||
|
.deleteShapes()
|
||||||
|
.createShapes([
|
||||||
|
{
|
||||||
|
id: createShapeId(),
|
||||||
|
type: 'box',
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Background: BackgroundComponent,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This one will move with the camera, just like shapes do.
|
||||||
|
*/
|
||||||
|
const BackgroundComponent = track(() => {
|
||||||
|
return (
|
||||||
|
<PositionedOnCanvas x={16} y={16}>
|
||||||
|
<p>Double click to create shapes.</p>
|
||||||
|
<p>Click or Shift+Click to select shapes.</p>
|
||||||
|
<p>Click and drag to move shapes.</p>
|
||||||
|
</PositionedOnCanvas>
|
||||||
|
)
|
||||||
|
})
|
|
@ -1,5 +1,4 @@
|
||||||
import { track } from '@tldraw/state'
|
import { Tldraw, track, useEditor } from '@tldraw/tldraw'
|
||||||
import { Tldraw, useEditor } from '@tldraw/tldraw'
|
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import { useYjsStore } from './useYjsStore'
|
import { useYjsStore } from './useYjsStore'
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import { computed, react, transact } from '@tldraw/state'
|
|
||||||
import {
|
import {
|
||||||
DocumentRecordType,
|
DocumentRecordType,
|
||||||
InstancePresenceRecordType,
|
InstancePresenceRecordType,
|
||||||
PageRecordType,
|
PageRecordType,
|
||||||
|
TLAnyShapeUtilConstructor,
|
||||||
TLDocument,
|
TLDocument,
|
||||||
TLInstancePresence,
|
TLInstancePresence,
|
||||||
TLPageId,
|
TLPageId,
|
||||||
TLRecord,
|
TLRecord,
|
||||||
TLShapeInfo,
|
|
||||||
TLStoreWithStatus,
|
TLStoreWithStatus,
|
||||||
|
computed,
|
||||||
createPresenceStateDerivation,
|
createPresenceStateDerivation,
|
||||||
createTLStore,
|
createTLStore,
|
||||||
defaultShapes,
|
defaultShapeUtils,
|
||||||
getUserPreferences,
|
getUserPreferences,
|
||||||
|
react,
|
||||||
} from '@tldraw/tldraw'
|
} from '@tldraw/tldraw'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { WebsocketProvider } from 'y-websocket'
|
import { WebsocketProvider } from 'y-websocket'
|
||||||
|
@ -21,14 +22,16 @@ import * as Y from 'yjs'
|
||||||
export function useYjsStore({
|
export function useYjsStore({
|
||||||
roomId = 'example',
|
roomId = 'example',
|
||||||
hostUrl = process.env.NODE_ENV === 'development' ? 'ws://localhost:1234' : 'wss://demos.yjs.dev',
|
hostUrl = process.env.NODE_ENV === 'development' ? 'ws://localhost:1234' : 'wss://demos.yjs.dev',
|
||||||
shapes = [],
|
shapeUtils = [],
|
||||||
}: Partial<{
|
}: Partial<{
|
||||||
hostUrl: string
|
hostUrl: string
|
||||||
roomId: string
|
roomId: string
|
||||||
version: number
|
version: number
|
||||||
shapes?: TLShapeInfo[]
|
shapeUtils: TLAnyShapeUtilConstructor[]
|
||||||
}>) {
|
}>) {
|
||||||
const [store] = useState(() => createTLStore({ shapes: [...defaultShapes, ...shapes] }))
|
const [store] = useState(() =>
|
||||||
|
createTLStore({ shapeUtils: [...defaultShapeUtils, ...shapeUtils] })
|
||||||
|
)
|
||||||
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({ status: 'loading' })
|
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({ status: 'loading' })
|
||||||
|
|
||||||
const { doc, room, yRecords } = useMemo(() => {
|
const { doc, room, yRecords } = useMemo(() => {
|
||||||
|
@ -75,7 +78,7 @@ export function useYjsStore({
|
||||||
// is empty, initialize the yjs doc with the default store records.
|
// is empty, initialize the yjs doc with the default store records.
|
||||||
if (yRecords.size === 0) {
|
if (yRecords.size === 0) {
|
||||||
// Create the initial store records
|
// Create the initial store records
|
||||||
transact(() => {
|
Y.transact(doc, () => {
|
||||||
store.clear()
|
store.clear()
|
||||||
store.put([
|
store.put([
|
||||||
DocumentRecordType.create({
|
DocumentRecordType.create({
|
||||||
|
@ -97,7 +100,7 @@ export function useYjsStore({
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Replace the store records with the yjs doc records
|
// Replace the store records with the yjs doc records
|
||||||
transact(() => {
|
Y.transact(doc, () => {
|
||||||
store.clear()
|
store.clear()
|
||||||
store.put([...yRecords.values()])
|
store.put([...yRecords.values()])
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,11 +34,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tldraw/assets": "workspace:*",
|
"@tldraw/assets": "workspace:*",
|
||||||
"@tldraw/editor": "workspace:*",
|
|
||||||
"@tldraw/file-format": "workspace:*",
|
|
||||||
"@tldraw/tldraw": "workspace:*",
|
"@tldraw/tldraw": "workspace:*",
|
||||||
"@tldraw/ui": "workspace:*",
|
|
||||||
"@tldraw/utils": "workspace:*",
|
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@types/node": "^17.0.14",
|
"@types/node": "^17.0.14",
|
||||||
"@types/react": "^18.0.24",
|
"@types/react": "^18.0.24",
|
||||||
|
@ -50,7 +46,6 @@
|
||||||
"esbuild": "^0.18.3",
|
"esbuild": "^0.18.3",
|
||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"lazyrepo": "0.0.0-alpha.27",
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
"nanoid": "4.0.2",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import esbuild from 'esbuild'
|
import esbuild from 'esbuild'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import fse from 'fs-extra'
|
import fse, { exists } from 'fs-extra'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { logEnv } from '../../vscode-script-utils/cli'
|
import { logEnv } from './cli'
|
||||||
import { exists, getDirname } from '../../vscode-script-utils/path'
|
import { getDirname } from './path'
|
||||||
|
|
||||||
const rootDir = getDirname(import.meta.url, '../')
|
const rootDir = getDirname(import.meta.url, '../')
|
||||||
const log = logEnv('editor')
|
const log = logEnv('editor')
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import esbuild from 'esbuild'
|
import esbuild from 'esbuild'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import fse from 'fs-extra'
|
import fse, { exists } from 'fs-extra'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { logEnv } from '../../vscode-script-utils/cli'
|
import { logEnv } from './cli'
|
||||||
import { copyEditor } from '../../vscode-script-utils/helpers'
|
import { copyEditor } from './helpers'
|
||||||
import { exists, getDirname } from '../../vscode-script-utils/path'
|
import { getDirname } from './path'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
const rootDir = getDirname(import.meta.url, '../')
|
const rootDir = getDirname(import.meta.url, '../')
|
||||||
|
|
|
@ -3,7 +3,7 @@ import fse from 'fs-extra'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { exists, getDirname } from './path'
|
import { exists, getDirname } from './path'
|
||||||
|
|
||||||
const vscodeDir = getDirname(import.meta.url, '../')
|
const vscodeDir = getDirname(import.meta.url, '../../')
|
||||||
|
|
||||||
export async function copyEditor({ log }: { log: (opts: any) => void }) {
|
export async function copyEditor({ log }: { log: (opts: any) => void }) {
|
||||||
const editorRoot = join(vscodeDir, 'editor')
|
const editorRoot = join(vscodeDir, 'editor')
|
|
@ -1,13 +1,15 @@
|
||||||
import { useEditor } from '@tldraw/editor'
|
import {
|
||||||
import { parseAndLoadDocument, serializeTldrawJson } from '@tldraw/file-format'
|
debounce,
|
||||||
import { useDefaultHelpers } from '@tldraw/ui'
|
parseAndLoadDocument,
|
||||||
import { debounce } from '@tldraw/utils'
|
serializeTldrawJson,
|
||||||
|
useDefaultHelpers,
|
||||||
|
useEditor,
|
||||||
|
} from '@tldraw/tldraw'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import '../public/index.css'
|
|
||||||
import { vscode } from './utils/vscode'
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import type { VscodeMessage } from '../../messages'
|
import type { VscodeMessage } from '../../messages'
|
||||||
|
import '../public/index.css'
|
||||||
|
import { vscode } from './utils/vscode'
|
||||||
|
|
||||||
export const ChangeResponder = () => {
|
export const ChangeResponder = () => {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { useEditor } from '@tldraw/editor'
|
import { parseAndLoadDocument, useDefaultHelpers, useEditor } from '@tldraw/tldraw'
|
||||||
import { parseAndLoadDocument } from '@tldraw/file-format'
|
|
||||||
import { useDefaultHelpers } from '@tldraw/ui'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { vscode } from './utils/vscode'
|
import { vscode } from './utils/vscode'
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
import {
|
|
||||||
Canvas,
|
|
||||||
Editor,
|
|
||||||
ErrorBoundary,
|
|
||||||
TldrawEditor,
|
|
||||||
defaultShapes,
|
|
||||||
defaultTools,
|
|
||||||
setRuntimeOverrides,
|
|
||||||
} from '@tldraw/editor'
|
|
||||||
import { linksUiOverrides } from './utils/links'
|
import { linksUiOverrides } from './utils/links'
|
||||||
// eslint-disable-next-line import/no-internal-modules
|
// eslint-disable-next-line import/no-internal-modules
|
||||||
import '@tldraw/editor/editor.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
import { ContextMenu, TLUiMenuSchema, TldrawUi } from '@tldraw/ui'
|
|
||||||
// eslint-disable-next-line import/no-internal-modules
|
|
||||||
import '@tldraw/ui/ui.css'
|
|
||||||
// eslint-disable-next-line import/no-internal-modules
|
// eslint-disable-next-line import/no-internal-modules
|
||||||
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
|
import { getAssetUrlsByImport } from '@tldraw/assets/imports'
|
||||||
|
import {
|
||||||
|
Editor,
|
||||||
|
ErrorBoundary,
|
||||||
|
TLUiMenuSchema,
|
||||||
|
Tldraw,
|
||||||
|
defaultShapeTools,
|
||||||
|
defaultShapeUtils,
|
||||||
|
setRuntimeOverrides,
|
||||||
|
} from '@tldraw/tldraw'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { VscodeMessage } from '../../messages'
|
import { VscodeMessage } from '../../messages'
|
||||||
import '../public/index.css'
|
import '../public/index.css'
|
||||||
|
@ -128,26 +125,22 @@ function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerPro
|
||||||
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
|
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
|
||||||
|
|
||||||
const handleMount = useCallback((editor: Editor) => {
|
const handleMount = useCallback((editor: Editor) => {
|
||||||
editor.externalContentManager.createAssetFromUrl = onCreateAssetFromUrl
|
editor.registerExternalAssetHandler('url', onCreateAssetFromUrl)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TldrawEditor
|
<Tldraw
|
||||||
shapes={defaultShapes}
|
shapeUtils={defaultShapeUtils}
|
||||||
tools={defaultTools}
|
tools={defaultShapeTools}
|
||||||
assetUrls={assetUrls}
|
assetUrls={assetUrls}
|
||||||
persistenceKey={uri}
|
persistenceKey={uri}
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
|
overrides={[menuOverrides, linksUiOverrides]}
|
||||||
autoFocus
|
autoFocus
|
||||||
>
|
>
|
||||||
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
||||||
<TldrawUi assetUrls={assetUrls} overrides={[menuOverrides, linksUiOverrides]}>
|
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
|
||||||
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
|
<ChangeResponder />
|
||||||
<ChangeResponder />
|
</Tldraw>
|
||||||
<ContextMenu>
|
|
||||||
<Canvas />
|
|
||||||
</ContextMenu>
|
|
||||||
</TldrawUi>
|
|
||||||
</TldrawEditor>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import { AssetRecordType, Editor, TLAsset, truncateStringWithEllipsis } from '@tldraw/editor'
|
import { AssetRecordType, TLAsset, TLExternalAssetContent, getHashForString } from '@tldraw/tldraw'
|
||||||
import { getHashForString } from '@tldraw/utils'
|
|
||||||
import { rpc } from './rpc'
|
import { rpc } from './rpc'
|
||||||
|
|
||||||
export async function onCreateAssetFromUrl(editor: Editor, url: string): Promise<TLAsset> {
|
export const truncateStringWithEllipsis = (str: string, maxLength: number) => {
|
||||||
|
return str.length <= maxLength ? str : str.substring(0, maxLength - 3) + '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function onCreateAssetFromUrl({
|
||||||
|
url,
|
||||||
|
}: TLExternalAssetContent & { type: 'url' }): Promise<TLAsset> {
|
||||||
try {
|
try {
|
||||||
// First, try to get the data from vscode
|
// First, try to get the data from vscode
|
||||||
const meta = await rpc('vscode:bookmark', { url })
|
const meta = await rpc('vscode:bookmark', { url })
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { menuGroup, menuItem, TLUiOverrides } from '@tldraw/ui'
|
import { menuGroup, menuItem, TLUiOverrides } from '@tldraw/tldraw'
|
||||||
import { openUrl } from './openUrl'
|
import { openUrl } from './openUrl'
|
||||||
|
|
||||||
export const GITHUB_URL = 'https://github.com/tldraw/tldraw'
|
export const GITHUB_URL = 'https://github.com/tldraw/tldraw'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { nanoid } from 'nanoid'
|
import { uniqueId } from '@tldraw/tldraw'
|
||||||
import type { VscodeMessagePairs } from '../../../messages'
|
import type { VscodeMessagePairs } from '../../../messages'
|
||||||
import { vscode } from './vscode'
|
import { vscode } from './vscode'
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export function rpc(
|
||||||
type ErrorType = VscodeMessagePairs[typeof id]['error']
|
type ErrorType = VscodeMessagePairs[typeof id]['error']
|
||||||
|
|
||||||
const type = (id + '/request') as RequestType['type']
|
const type = (id + '/request') as RequestType['type']
|
||||||
const uuid = nanoid()
|
const uuid = uniqueId()
|
||||||
return new Promise<ResponseType['data']>((resolve, reject) => {
|
return new Promise<ResponseType['data']>((resolve, reject) => {
|
||||||
const inMessage = {
|
const inMessage = {
|
||||||
uuid,
|
uuid,
|
||||||
|
|
|
@ -22,11 +22,6 @@
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
"include": ["src", "../messages", "scripts", "../vscode-script-utils"],
|
"include": ["src", "../messages", "scripts"],
|
||||||
"references": [
|
"references": [{ "path": "../../../packages/tldraw" }]
|
||||||
{ "path": "../../../packages/file-format" },
|
|
||||||
{ "path": "../../../packages/ui" },
|
|
||||||
{ "path": "../../../packages/editor" },
|
|
||||||
{ "path": "../../../packages/utils" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,8 +131,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tldraw/editor": "workspace:*",
|
"@tldraw/editor": "workspace:*",
|
||||||
"@tldraw/file-format": "workspace:*",
|
"@tldraw/tldraw": "workspace:*",
|
||||||
"@tldraw/store": "workspace:*",
|
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@types/node-fetch": "^2.6.2",
|
"@types/node-fetch": "^2.6.2",
|
||||||
"@types/vscode": "^1.75.1",
|
"@types/vscode": "^1.75.1",
|
||||||
|
@ -152,7 +151,6 @@
|
||||||
},
|
},
|
||||||
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296",
|
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "4.0.2",
|
|
||||||
"node-fetch": "^2.0.0"
|
"node-fetch": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import esbuild from 'esbuild'
|
import esbuild from 'esbuild'
|
||||||
import { logEnv } from '../../vscode-script-utils/cli'
|
import { logEnv } from './cli'
|
||||||
import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers'
|
import { copyEditor, removeDistDirectory } from './helpers'
|
||||||
|
|
||||||
const log = logEnv('extension')
|
const log = logEnv('extension')
|
||||||
|
|
||||||
|
|
58
apps/vscode/extension/scripts/cli.ts
Normal file
58
apps/vscode/extension/scripts/cli.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const displayRelative = (from: string, to: string) => {
|
||||||
|
const outpath = path.relative(from, to)
|
||||||
|
if (!outpath.match(/^\./)) {
|
||||||
|
return `./${outpath}`
|
||||||
|
}
|
||||||
|
return outpath
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogDef =
|
||||||
|
| { cmd: 'remove'; env: string; args: { target: string } }
|
||||||
|
| { cmd: 'copy'; env: string; args: { source: string; dest: string } }
|
||||||
|
| { cmd: 'esbuild'; env: string; args: { entryPoints: string[] } }
|
||||||
|
| { cmd: 'esbuild:success'; env: string; args: any }
|
||||||
|
| { cmd: 'esbuild:error'; env: string; args: { error: string } }
|
||||||
|
| { cmd: 'esbuild:serve'; env: string; args: { host: string; port: number | string } }
|
||||||
|
|
||||||
|
export function log(def: LogDef) {
|
||||||
|
const printStderr = (icon: string, cmd: string, ...args: unknown[]) => {
|
||||||
|
console.error(`${icon} [${def.env ?? 'unknown'}/${cmd}]`, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def.cmd === 'remove') {
|
||||||
|
const { target } = def.args
|
||||||
|
printStderr('🗑 ', 'remove', displayRelative(process.cwd(), target))
|
||||||
|
} else if (def.cmd === 'copy') {
|
||||||
|
const { source, dest } = def.args
|
||||||
|
printStderr(
|
||||||
|
'🏠',
|
||||||
|
'copy',
|
||||||
|
`${displayRelative(process.cwd(), source)} -> ${displayRelative(process.cwd(), dest)}`
|
||||||
|
)
|
||||||
|
} else if (def.cmd === 'esbuild') {
|
||||||
|
printStderr(
|
||||||
|
'🤖',
|
||||||
|
'esbuild',
|
||||||
|
`${def.args.entryPoints.map((pathname) => displayRelative(process.cwd(), pathname))}`
|
||||||
|
)
|
||||||
|
} else if (def.cmd === 'esbuild:success') {
|
||||||
|
printStderr('✅', `esbuild`, `build successful (${new Date().toISOString()})`)
|
||||||
|
} else if (def.cmd === 'esbuild:error') {
|
||||||
|
printStderr(`❌`, `esbuild`, `error`)
|
||||||
|
console.error(def.args.error)
|
||||||
|
} else if (def.cmd === 'esbuild:serve') {
|
||||||
|
const { host = 'localhost', port } = def.args
|
||||||
|
printStderr(`🌎`, `esbuild`, `serving <http://${host}:${port}>`)
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
printStderr(`❓`, def.cmd, JSON.stringify(def.args))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logEnv(env: string) {
|
||||||
|
return (opts: any) => {
|
||||||
|
log({ ...opts, env })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import esbuild from 'esbuild'
|
import esbuild from 'esbuild'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { logEnv } from '../../vscode-script-utils/cli'
|
import { logEnv } from './cli'
|
||||||
import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers'
|
import { copyEditor, removeDistDirectory } from './helpers'
|
||||||
import { getDirname } from '../../vscode-script-utils/path'
|
import { getDirname } from './path'
|
||||||
|
|
||||||
const rootDir = getDirname(import.meta.url, '../')
|
const rootDir = getDirname(import.meta.url, '../')
|
||||||
const log = logEnv('extension')
|
const log = logEnv('extension')
|
||||||
|
|
25
apps/vscode/extension/scripts/helpers.ts
Normal file
25
apps/vscode/extension/scripts/helpers.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import fse from 'fs-extra'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { exists, getDirname } from './path'
|
||||||
|
|
||||||
|
const vscodeDir = getDirname(import.meta.url, '../../')
|
||||||
|
|
||||||
|
export async function copyEditor({ log }: { log: (opts: any) => void }) {
|
||||||
|
const editorRoot = join(vscodeDir, 'editor')
|
||||||
|
const extensionRoot = join(vscodeDir, 'extension')
|
||||||
|
|
||||||
|
const source = join(editorRoot, 'dist')
|
||||||
|
const dest = join(extensionRoot, 'editor')
|
||||||
|
|
||||||
|
log({ cmd: 'copy', args: { source, dest } })
|
||||||
|
await fse.copy(source, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeDistDirectory({ log }: { log: (opts: any) => void }) {
|
||||||
|
const target = join(vscodeDir, 'extension', 'dist')
|
||||||
|
if (await exists(target)) {
|
||||||
|
log({ cmd: 'remove', args: { target } })
|
||||||
|
await fs.promises.rm(target, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
16
apps/vscode/extension/scripts/path.ts
Normal file
16
apps/vscode/extension/scripts/path.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export function getDirname(metaUrl: string, targetPath: string) {
|
||||||
|
const dirname = path.dirname(metaUrl.replace('file://', ''))
|
||||||
|
return path.normalize(path.join(dirname, targetPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exists(targetFolder: string) {
|
||||||
|
try {
|
||||||
|
await fs.promises.access(targetFolder)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { TldrawFile } from '@tldraw/file-format'
|
import { TldrawFile } from '@tldraw/tldraw'
|
||||||
import * as vscode from 'vscode'
|
import * as vscode from 'vscode'
|
||||||
import { defaultFileContents, fileExists, loadFile } from './file'
|
import { defaultFileContents, fileExists, loadFile } from './file'
|
||||||
import { nicelog } from './utils'
|
import { nicelog } from './utils'
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { nanoid } from 'nanoid'
|
import { uniqueId } from '@tldraw/tldraw'
|
||||||
import * as vscode from 'vscode'
|
import * as vscode from 'vscode'
|
||||||
import { TLDrawDocument } from './TldrawDocument'
|
import { TLDrawDocument } from './TldrawDocument'
|
||||||
import { GlobalStateKeys, WebViewMessageHandler } from './WebViewMessageHandler'
|
import { GlobalStateKeys, WebViewMessageHandler } from './WebViewMessageHandler'
|
||||||
// @ts-ignore
|
|
||||||
|
|
||||||
export class TldrawWebviewManager {
|
export class TldrawWebviewManager {
|
||||||
private disposables: vscode.Disposable[] = []
|
private disposables: vscode.Disposable[] = []
|
||||||
|
@ -15,7 +14,7 @@ export class TldrawWebviewManager {
|
||||||
) {
|
) {
|
||||||
let userId = context.globalState.get(GlobalStateKeys.UserId)
|
let userId = context.globalState.get(GlobalStateKeys.UserId)
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
userId = 'user:' + nanoid()
|
userId = 'user:' + uniqueId()
|
||||||
context.globalState.update(GlobalStateKeys.UserId, userId)
|
context.globalState.update(GlobalStateKeys.UserId, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { UnknownRecord } from '@tldraw/store'
|
|
||||||
import { isEqual } from 'lodash'
|
import { isEqual } from 'lodash'
|
||||||
import fetch from 'node-fetch'
|
import fetch from 'node-fetch'
|
||||||
import * as vscode from 'vscode'
|
import * as vscode from 'vscode'
|
||||||
import { TLDrawDocument } from './TldrawDocument'
|
import { TLDrawDocument } from './TldrawDocument'
|
||||||
import { loadFile } from './file'
|
import { loadFile } from './file'
|
||||||
|
|
||||||
|
import { UnknownRecord } from '@tldraw/tldraw'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import type { VscodeMessage } from '../../messages'
|
import type { VscodeMessage } from '../../messages'
|
||||||
import { nicelog } from './utils'
|
import { nicelog } from './utils'
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import { createTLStore, defaultShapes } from '@tldraw/editor'
|
import { TldrawFile, createTLStore, defaultShapeUtils } from '@tldraw/tldraw'
|
||||||
import { TldrawFile } from '@tldraw/file-format'
|
|
||||||
import * as vscode from 'vscode'
|
import * as vscode from 'vscode'
|
||||||
import { nicelog } from './utils'
|
import { nicelog } from './utils'
|
||||||
|
|
||||||
export const defaultFileContents: TldrawFile = {
|
export const defaultFileContents: TldrawFile = {
|
||||||
tldrawFileFormatVersion: 1,
|
tldrawFileFormatVersion: 1,
|
||||||
schema: createTLStore({ shapes: defaultShapes }).schema.serialize(),
|
schema: createTLStore({ shapeUtils: defaultShapeUtils }).schema.serialize(),
|
||||||
records: [],
|
records: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileContentWithErrors: TldrawFile = {
|
export const fileContentWithErrors: TldrawFile = {
|
||||||
tldrawFileFormatVersion: 1,
|
tldrawFileFormatVersion: 1,
|
||||||
schema: createTLStore({ shapes: defaultShapes }).schema.serialize(),
|
schema: createTLStore({ shapeUtils: defaultShapeUtils }).schema.serialize(),
|
||||||
records: [{ typeName: 'shape', id: null } as any],
|
records: [{ typeName: 'shape', id: null } as any],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,6 @@
|
||||||
"skipDefaultLibCheck": true,
|
"skipDefaultLibCheck": true,
|
||||||
"experimentalDecorators": true
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
"include": ["src", "../messages", "scripts", "../vscode-script-utils"],
|
"include": ["src", "../messages", "scripts"],
|
||||||
"references": [{ "path": "../../../packages/file-format" }]
|
"references": [{ "path": "../../../packages/tldraw" }]
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ const MyCustomShapes = [MyCardShape]
|
||||||
export default function () {
|
export default function () {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0 }}>
|
<div style={{ position: 'fixed', inset: 0 }}>
|
||||||
<Tldraw shapes={MyCustomShapes}/>
|
<Tldraw shapeUtils={MyCustomShapes}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ The [`defineShape`](/gen/editor/defineShape) function can also be used to includ
|
||||||
export default function () {
|
export default function () {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0 }}>
|
<div style={{ position: 'fixed', inset: 0 }}>
|
||||||
<Tldraw shapes={MyCustomShapes} onMount={editor => {
|
<Tldraw shapeUtils={MyCustomShapes} onMount={editor => {
|
||||||
editor.createShapes([{ type: "card" }])
|
editor.createShapes([{ type: "card" }])
|
||||||
}}/>
|
}}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -94,5 +94,8 @@
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@microsoft/api-extractor@^7.35.4": "patch:@microsoft/api-extractor@npm%3A7.35.4#./.yarn/patches/@microsoft-api-extractor-npm-7.35.4-5f4f0357b4.patch"
|
"@microsoft/api-extractor@^7.35.4": "patch:@microsoft/api-extractor@npm%3A7.35.4#./.yarn/patches/@microsoft-api-extractor-npm-7.35.4-5f4f0357b4.patch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"svgo": "^3.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -245,14 +245,11 @@ input,
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tlui-following {
|
.tl-positioned {
|
||||||
display: block;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0px;
|
top: 0px;
|
||||||
border-width: 2px;
|
left: 0px;
|
||||||
border-style: solid;
|
transform-origin: top left;
|
||||||
z-index: 9999999;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------- Background ------------------- */
|
/* ------------------- Background ------------------- */
|
||||||
|
|
|
@ -45,24 +45,21 @@
|
||||||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tldraw/indices": "workspace:*",
|
|
||||||
"@tldraw/primitives": "workspace:*",
|
|
||||||
"@tldraw/state": "workspace:*",
|
"@tldraw/state": "workspace:*",
|
||||||
"@tldraw/store": "workspace:*",
|
"@tldraw/store": "workspace:*",
|
||||||
"@tldraw/tlschema": "workspace:*",
|
"@tldraw/tlschema": "workspace:*",
|
||||||
"@tldraw/utils": "workspace:*",
|
"@tldraw/utils": "workspace:*",
|
||||||
"@tldraw/validate": "workspace:*",
|
"@tldraw/validate": "workspace:*",
|
||||||
"@types/canvas-size": "^1.2.0",
|
"@types/core-js": "^2.5.5",
|
||||||
"@use-gesture/react": "^10.2.27",
|
"@use-gesture/react": "^10.2.27",
|
||||||
"canvas-size": "^1.2.6",
|
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"escape-string-regexp": "^5.0.0",
|
"core-js": "^3.31.1",
|
||||||
"eventemitter3": "^4.0.7",
|
"eventemitter3": "^4.0.7",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"is-plain-object": "^5.0.0",
|
"is-plain-object": "^5.0.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"lodash.uniq": "^4.5.0",
|
"lodash.uniq": "^4.5.0",
|
||||||
"nanoid": "4.0.2"
|
"nanoid": "^3.3.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
@ -95,7 +92,7 @@
|
||||||
"^.+\\.*.css$"
|
"^.+\\.*.css$"
|
||||||
],
|
],
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!(nanoid|escape-string-regexp)/)"
|
"node_modules/(?!(nanoid)/)"
|
||||||
],
|
],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"^~(.*)": "<rootDir>/src/$1",
|
"^~(.*)": "<rootDir>/src/$1",
|
||||||
|
|
|
@ -1,23 +1,30 @@
|
||||||
// Important! don't move this tlschema re-export to lib/index.ts, doing so causes esbuild to produce
|
// Important! don't move this tlschema re-export to lib/index.ts, doing so causes esbuild to produce
|
||||||
// incorrect output. https://github.com/evanw/esbuild/issues/1737
|
// incorrect output. https://github.com/evanw/esbuild/issues/1737
|
||||||
|
|
||||||
// eslint-disable-next-line local/no-export-star
|
|
||||||
export * from '@tldraw/indices'
|
|
||||||
export {
|
export {
|
||||||
|
EMPTY_ARRAY,
|
||||||
atom,
|
atom,
|
||||||
computed,
|
computed,
|
||||||
react,
|
react,
|
||||||
track,
|
track,
|
||||||
|
transact,
|
||||||
|
transaction,
|
||||||
useComputed,
|
useComputed,
|
||||||
useQuickReactor,
|
useQuickReactor,
|
||||||
useReactor,
|
useReactor,
|
||||||
useValue,
|
useValue,
|
||||||
whyAmIRunning,
|
whyAmIRunning,
|
||||||
|
type Atom,
|
||||||
|
type Signal,
|
||||||
} from '@tldraw/state'
|
} from '@tldraw/state'
|
||||||
export { defineMigrations } from '@tldraw/store'
|
// eslint-disable-next-line local/no-export-star
|
||||||
|
export * from '@tldraw/store'
|
||||||
// eslint-disable-next-line local/no-export-star
|
// eslint-disable-next-line local/no-export-star
|
||||||
export * from '@tldraw/tlschema'
|
export * from '@tldraw/tlschema'
|
||||||
export { getHashForString } from '@tldraw/utils'
|
// eslint-disable-next-line local/no-export-star
|
||||||
|
export * from '@tldraw/utils'
|
||||||
|
// eslint-disable-next-line local/no-export-star
|
||||||
|
export * from '@tldraw/validate'
|
||||||
export {
|
export {
|
||||||
ErrorScreen,
|
ErrorScreen,
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
|
@ -26,20 +33,16 @@ export {
|
||||||
type TldrawEditorBaseProps,
|
type TldrawEditorBaseProps,
|
||||||
type TldrawEditorProps,
|
type TldrawEditorProps,
|
||||||
} from './lib/TldrawEditor'
|
} from './lib/TldrawEditor'
|
||||||
export {
|
|
||||||
defaultEditorAssetUrls,
|
|
||||||
setDefaultEditorAssetUrls,
|
|
||||||
type TLEditorAssetUrls,
|
|
||||||
} from './lib/assetUrls'
|
|
||||||
export { Canvas } from './lib/components/Canvas'
|
export { Canvas } from './lib/components/Canvas'
|
||||||
export { DefaultErrorFallback } from './lib/components/DefaultErrorFallback'
|
|
||||||
export {
|
export {
|
||||||
ErrorBoundary,
|
ErrorBoundary,
|
||||||
OptionalErrorBoundary,
|
OptionalErrorBoundary,
|
||||||
type TLErrorBoundaryProps,
|
type TLErrorBoundaryProps,
|
||||||
} from './lib/components/ErrorBoundary'
|
} from './lib/components/ErrorBoundary'
|
||||||
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
|
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
|
||||||
|
export { PositionedOnCanvas } from './lib/components/PositionedOnCanvas'
|
||||||
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
|
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
|
||||||
|
export { DefaultErrorFallback } from './lib/components/default-components/DefaultErrorFallback'
|
||||||
export {
|
export {
|
||||||
TAB_ID,
|
TAB_ID,
|
||||||
createSessionStateSnapshotSignal,
|
createSessionStateSnapshotSignal,
|
||||||
|
@ -60,39 +63,35 @@ export {
|
||||||
type TLStoreOptions,
|
type TLStoreOptions,
|
||||||
} from './lib/config/createTLStore'
|
} from './lib/config/createTLStore'
|
||||||
export { createTLUser } from './lib/config/createTLUser'
|
export { createTLUser } from './lib/config/createTLUser'
|
||||||
export { coreShapes, defaultShapes } from './lib/config/defaultShapes'
|
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
|
||||||
export { defaultTools } from './lib/config/defaultTools'
|
|
||||||
export { defineShape, type TLShapeInfo } from './lib/config/defineShape'
|
|
||||||
export {
|
export {
|
||||||
ANIMATION_MEDIUM_MS,
|
ANIMATION_MEDIUM_MS,
|
||||||
ANIMATION_SHORT_MS,
|
ANIMATION_SHORT_MS,
|
||||||
|
CAMERA_SLIDE_FRICTION,
|
||||||
DEFAULT_ANIMATION_OPTIONS,
|
DEFAULT_ANIMATION_OPTIONS,
|
||||||
DOUBLE_CLICK_DURATION,
|
DOUBLE_CLICK_DURATION,
|
||||||
DRAG_DISTANCE,
|
DRAG_DISTANCE,
|
||||||
GRID_INCREMENT,
|
GRID_INCREMENT,
|
||||||
GRID_STEPS,
|
GRID_STEPS,
|
||||||
HAND_TOOL_FRICTION,
|
|
||||||
HASH_PATTERN_ZOOM_NAMES,
|
HASH_PATTERN_ZOOM_NAMES,
|
||||||
MAJOR_NUDGE_FACTOR,
|
MAJOR_NUDGE_FACTOR,
|
||||||
MAX_ASSET_HEIGHT,
|
|
||||||
MAX_ASSET_WIDTH,
|
|
||||||
MAX_PAGES,
|
MAX_PAGES,
|
||||||
MAX_SHAPES_PER_PAGE,
|
MAX_SHAPES_PER_PAGE,
|
||||||
MAX_ZOOM,
|
MAX_ZOOM,
|
||||||
MINOR_NUDGE_FACTOR,
|
MINOR_NUDGE_FACTOR,
|
||||||
MIN_ZOOM,
|
MIN_ZOOM,
|
||||||
MULTI_CLICK_DURATION,
|
MULTI_CLICK_DURATION,
|
||||||
REMOVE_SYMBOL,
|
|
||||||
RICH_TYPES,
|
|
||||||
SVG_PADDING,
|
SVG_PADDING,
|
||||||
ZOOMS,
|
ZOOMS,
|
||||||
} from './lib/constants'
|
} from './lib/constants'
|
||||||
export { Editor, type TLAnimationOptions, type TLEditorOptions } from './lib/editor/Editor'
|
export { Editor, type TLAnimationOptions, type TLEditorOptions } from './lib/editor/Editor'
|
||||||
export {
|
export {
|
||||||
ExternalContentManager as PlopManager,
|
SnapManager,
|
||||||
type TLExternalContent,
|
type GapsSnapLine,
|
||||||
} from './lib/editor/managers/ExternalContentManager'
|
type PointsSnapLine,
|
||||||
export { ScribbleManager } from './lib/editor/managers/ScribbleManager'
|
type SnapLine,
|
||||||
|
type SnapPoint,
|
||||||
|
} from './lib/editor/managers/SnapManager'
|
||||||
export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseBoxShapeUtil'
|
export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseBoxShapeUtil'
|
||||||
export {
|
export {
|
||||||
ShapeUtil,
|
ShapeUtil,
|
||||||
|
@ -117,38 +116,25 @@ export {
|
||||||
type TLOnTranslateStartHandler,
|
type TLOnTranslateStartHandler,
|
||||||
type TLResizeInfo,
|
type TLResizeInfo,
|
||||||
type TLResizeMode,
|
type TLResizeMode,
|
||||||
|
type TLShapeUtilCanvasSvgDef,
|
||||||
type TLShapeUtilConstructor,
|
type TLShapeUtilConstructor,
|
||||||
type TLShapeUtilFlag,
|
type TLShapeUtilFlag,
|
||||||
} from './lib/editor/shapes/ShapeUtil'
|
} from './lib/editor/shapes/ShapeUtil'
|
||||||
export { ArrowShape } from './lib/editor/shapes/arrow/ArrowShape'
|
|
||||||
export { ArrowShapeUtil } from './lib/editor/shapes/arrow/ArrowShapeUtil'
|
|
||||||
export { BookmarkShape } from './lib/editor/shapes/bookmark/BookmarkShape'
|
|
||||||
export { BookmarkShapeUtil } from './lib/editor/shapes/bookmark/BookmarkShapeUtil'
|
|
||||||
export { DrawShape } from './lib/editor/shapes/draw/DrawShape'
|
|
||||||
export { DrawShapeUtil } from './lib/editor/shapes/draw/DrawShapeUtil'
|
|
||||||
export { EmbedShape } from './lib/editor/shapes/embed/EmbedShape'
|
|
||||||
export { EmbedShapeUtil } from './lib/editor/shapes/embed/EmbedShapeUtil'
|
|
||||||
export { FrameShape } from './lib/editor/shapes/frame/FrameShape'
|
|
||||||
export { FrameShapeUtil } from './lib/editor/shapes/frame/FrameShapeUtil'
|
|
||||||
export { GeoShape } from './lib/editor/shapes/geo/GeoShape'
|
|
||||||
export { GeoShapeUtil } from './lib/editor/shapes/geo/GeoShapeUtil'
|
|
||||||
export { GroupShape } from './lib/editor/shapes/group/GroupShape'
|
|
||||||
export { GroupShapeUtil } from './lib/editor/shapes/group/GroupShapeUtil'
|
export { GroupShapeUtil } from './lib/editor/shapes/group/GroupShapeUtil'
|
||||||
export { HighlightShape } from './lib/editor/shapes/highlight/HighlightShape'
|
export { getArrowheadPathForType } from './lib/editor/shapes/shared/arrow/arrowheads'
|
||||||
export { HighlightShapeUtil } from './lib/editor/shapes/highlight/HighlightShapeUtil'
|
export {
|
||||||
export { ImageShape } from './lib/editor/shapes/image/ImageShape'
|
getCurvedArrowHandlePath,
|
||||||
export { ImageShapeUtil } from './lib/editor/shapes/image/ImageShapeUtil'
|
getSolidCurvedArrowPath,
|
||||||
export { LineShape } from './lib/editor/shapes/line/LineShape'
|
} from './lib/editor/shapes/shared/arrow/curved-arrow'
|
||||||
export { LineShapeUtil, getSplineForLineShape } from './lib/editor/shapes/line/LineShapeUtil'
|
export { getArrowTerminalsInArrowSpace } from './lib/editor/shapes/shared/arrow/shared'
|
||||||
export { NoteShape } from './lib/editor/shapes/note/NoteShape'
|
export {
|
||||||
export { NoteShapeUtil } from './lib/editor/shapes/note/NoteShapeUtil'
|
getSolidStraightArrowPath,
|
||||||
|
getStraightArrowHandlePath,
|
||||||
|
} from './lib/editor/shapes/shared/arrow/straight-arrow'
|
||||||
export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
|
export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
|
||||||
export { TextShape } from './lib/editor/shapes/text/TextShape'
|
|
||||||
export { INDENT, TextShapeUtil } from './lib/editor/shapes/text/TextShapeUtil'
|
|
||||||
export { VideoShape } from './lib/editor/shapes/video/VideoShape'
|
|
||||||
export { VideoShapeUtil } from './lib/editor/shapes/video/VideoShapeUtil'
|
|
||||||
export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
|
export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
|
||||||
export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'
|
export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'
|
||||||
|
export { type SvgExportContext, type SvgExportDef } from './lib/editor/types/SvgExportContext'
|
||||||
export { type TLContent } from './lib/editor/types/clipboard-types'
|
export { type TLContent } from './lib/editor/types/clipboard-types'
|
||||||
export { type TLEventMap, type TLEventMapHandler } from './lib/editor/types/emit-types'
|
export { type TLEventMap, type TLEventMapHandler } from './lib/editor/types/emit-types'
|
||||||
export {
|
export {
|
||||||
|
@ -184,6 +170,10 @@ export {
|
||||||
type UiEvent,
|
type UiEvent,
|
||||||
type UiEventType,
|
type UiEventType,
|
||||||
} from './lib/editor/types/event-types'
|
} from './lib/editor/types/event-types'
|
||||||
|
export {
|
||||||
|
type TLExternalAssetContent,
|
||||||
|
type TLExternalContent,
|
||||||
|
} from './lib/editor/types/external-content'
|
||||||
export {
|
export {
|
||||||
type TLCommand,
|
type TLCommand,
|
||||||
type TLCommandHandler,
|
type TLCommandHandler,
|
||||||
|
@ -192,83 +182,126 @@ export {
|
||||||
} from './lib/editor/types/history-types'
|
} from './lib/editor/types/history-types'
|
||||||
export { type RequiredKeys } from './lib/editor/types/misc-types'
|
export { type RequiredKeys } from './lib/editor/types/misc-types'
|
||||||
export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types'
|
export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types'
|
||||||
export { normalizeWheel } from './lib/hooks/shared'
|
|
||||||
export { useContainer } from './lib/hooks/useContainer'
|
export { useContainer } from './lib/hooks/useContainer'
|
||||||
|
export { getCursor } from './lib/hooks/useCursor'
|
||||||
export { useEditor } from './lib/hooks/useEditor'
|
export { useEditor } from './lib/hooks/useEditor'
|
||||||
export type { TLEditorComponents } from './lib/hooks/useEditorComponents'
|
export type { TLEditorComponents } from './lib/hooks/useEditorComponents'
|
||||||
|
export { useIsCropping } from './lib/hooks/useIsCropping'
|
||||||
|
export { useIsEditing } from './lib/hooks/useIsEditing'
|
||||||
export { useLocalStore } from './lib/hooks/useLocalStore'
|
export { useLocalStore } from './lib/hooks/useLocalStore'
|
||||||
export { usePeerIds } from './lib/hooks/usePeerIds'
|
export { usePeerIds } from './lib/hooks/usePeerIds'
|
||||||
export { usePresence } from './lib/hooks/usePresence'
|
export { usePresence } from './lib/hooks/usePresence'
|
||||||
|
export { useSelectionEvents } from './lib/hooks/useSelectionEvents'
|
||||||
export { useTLStore } from './lib/hooks/useTLStore'
|
export { useTLStore } from './lib/hooks/useTLStore'
|
||||||
|
export { useTransform } from './lib/hooks/useTransform'
|
||||||
|
export {
|
||||||
|
Box2d,
|
||||||
|
ROTATE_CORNER_TO_SELECTION_CORNER,
|
||||||
|
rotateSelectionHandle,
|
||||||
|
type RotateCorner,
|
||||||
|
type SelectionCorner,
|
||||||
|
type SelectionEdge,
|
||||||
|
type SelectionHandle,
|
||||||
|
} from './lib/primitives/Box2d'
|
||||||
|
export { Matrix2d, type Matrix2dModel } from './lib/primitives/Matrix2d'
|
||||||
|
export { Vec2d, type VecLike } from './lib/primitives/Vec2d'
|
||||||
|
export { EASINGS } from './lib/primitives/easings'
|
||||||
|
export {
|
||||||
|
intersectLineSegmentPolygon,
|
||||||
|
intersectLineSegmentPolyline,
|
||||||
|
intersectPolygonPolygon,
|
||||||
|
linesIntersect,
|
||||||
|
polygonsIntersect,
|
||||||
|
} from './lib/primitives/intersect'
|
||||||
|
export {
|
||||||
|
EPSILON,
|
||||||
|
PI,
|
||||||
|
PI2,
|
||||||
|
SIN,
|
||||||
|
TAU,
|
||||||
|
angleDelta,
|
||||||
|
approximately,
|
||||||
|
areAnglesCompatible,
|
||||||
|
average,
|
||||||
|
canonicalizeRotation,
|
||||||
|
clamp,
|
||||||
|
clampRadians,
|
||||||
|
degreesToRadians,
|
||||||
|
getArcLength,
|
||||||
|
getPointOnCircle,
|
||||||
|
getPolygonVertices,
|
||||||
|
getStarBounds,
|
||||||
|
getSweep,
|
||||||
|
isAngleBetween,
|
||||||
|
isSafeFloat,
|
||||||
|
lerpAngles,
|
||||||
|
longAngleDist,
|
||||||
|
perimeterOfEllipse,
|
||||||
|
pointInBounds,
|
||||||
|
pointInCircle,
|
||||||
|
pointInEllipse,
|
||||||
|
pointInPolygon,
|
||||||
|
pointInPolyline,
|
||||||
|
pointInRect,
|
||||||
|
pointNearToLineSegment,
|
||||||
|
pointNearToPolyline,
|
||||||
|
precise,
|
||||||
|
radiansToDegrees,
|
||||||
|
rangeIntersection,
|
||||||
|
shortAngleDist,
|
||||||
|
snapAngle,
|
||||||
|
toDomPrecision,
|
||||||
|
toFixed,
|
||||||
|
toPrecision,
|
||||||
|
} from './lib/primitives/utils'
|
||||||
export {
|
export {
|
||||||
ReadonlySharedStyleMap,
|
ReadonlySharedStyleMap,
|
||||||
SharedStyleMap,
|
SharedStyleMap,
|
||||||
type SharedStyle,
|
type SharedStyle,
|
||||||
} from './lib/utils/SharedStylesMap'
|
} from './lib/utils/SharedStylesMap'
|
||||||
export { WeakMapCache } from './lib/utils/WeakMapCache'
|
export { WeakMapCache } from './lib/utils/WeakMapCache'
|
||||||
export {
|
export { dataUrlToFile } from './lib/utils/assets'
|
||||||
ACCEPTED_ASSET_TYPE,
|
|
||||||
ACCEPTED_IMG_TYPE,
|
|
||||||
ACCEPTED_VID_TYPE,
|
|
||||||
containBoxSize,
|
|
||||||
dataUrlToFile,
|
|
||||||
getFileMetaData,
|
|
||||||
getImageSizeFromSrc,
|
|
||||||
getMediaAssetFromFile,
|
|
||||||
getResizedImageDataUrl,
|
|
||||||
getValidHttpURLList,
|
|
||||||
getVideoSizeFromSrc,
|
|
||||||
isImage,
|
|
||||||
isSvgText,
|
|
||||||
isValidHttpURL,
|
|
||||||
} from './lib/utils/assets'
|
|
||||||
export {
|
|
||||||
checkFlag,
|
|
||||||
fileToBase64,
|
|
||||||
getIncrementedName,
|
|
||||||
isSerializable,
|
|
||||||
isValidUrl,
|
|
||||||
snapToGrid,
|
|
||||||
uniqueId,
|
|
||||||
} from './lib/utils/data'
|
|
||||||
export { debugFlags, featureFlags, type DebugFlag } from './lib/utils/debug-flags'
|
export { debugFlags, featureFlags, type DebugFlag } from './lib/utils/debug-flags'
|
||||||
export {
|
export {
|
||||||
getRotatedBoxShadow,
|
|
||||||
loopToHtmlElement,
|
loopToHtmlElement,
|
||||||
preventDefault,
|
preventDefault,
|
||||||
releasePointerCapture,
|
releasePointerCapture,
|
||||||
setPointerCapture,
|
setPointerCapture,
|
||||||
truncateStringWithEllipsis,
|
stopEventPropagation,
|
||||||
usePrefersReducedMotion,
|
|
||||||
} from './lib/utils/dom'
|
} from './lib/utils/dom'
|
||||||
|
export { getIncrementedName } from './lib/utils/getIncrementedName'
|
||||||
|
export { getPointerInfo } from './lib/utils/getPointerInfo'
|
||||||
|
export { getSvgPathFromPoints } from './lib/utils/getSvgPathFromPoints'
|
||||||
|
export { hardResetEditor } from './lib/utils/hardResetEditor'
|
||||||
|
export { normalizeWheel } from './lib/utils/normalizeWheel'
|
||||||
|
export { png } from './lib/utils/png'
|
||||||
|
export { refreshPage } from './lib/utils/refreshPage'
|
||||||
export {
|
export {
|
||||||
getEmbedInfo,
|
getIndexAbove,
|
||||||
getEmbedInfoUnsafely,
|
getIndexBelow,
|
||||||
matchEmbedUrl,
|
getIndexBetween,
|
||||||
matchUrl,
|
getIndices,
|
||||||
type TLEmbedResult,
|
getIndicesAbove,
|
||||||
} from './lib/utils/embeds'
|
getIndicesBelow,
|
||||||
|
getIndicesBetween,
|
||||||
|
sortByIndex,
|
||||||
|
} from './lib/utils/reordering/reordering'
|
||||||
export {
|
export {
|
||||||
downloadDataURLAsFile,
|
applyRotationToSnapshotShapes,
|
||||||
getSvgAsDataUrl,
|
getRotationSnapshot,
|
||||||
getSvgAsDataUrlSync,
|
type TLRotationSnapshot,
|
||||||
getSvgAsImage,
|
} from './lib/utils/rotation'
|
||||||
getSvgAsString,
|
|
||||||
getTextBoundingBox,
|
|
||||||
type TLCopyType,
|
|
||||||
type TLExportType,
|
|
||||||
} from './lib/utils/export'
|
|
||||||
export { hardResetEditor } from './lib/utils/hard-reset'
|
|
||||||
export { isAnimated, isGIF } from './lib/utils/is-gif-animated'
|
|
||||||
export { refreshPage } from './lib/utils/refresh-page'
|
|
||||||
export { runtime, setRuntimeOverrides } from './lib/utils/runtime'
|
export { runtime, setRuntimeOverrides } from './lib/utils/runtime'
|
||||||
export {
|
|
||||||
blobAsString,
|
|
||||||
correctSpacesToNbsp,
|
|
||||||
dataTransferItemAsString,
|
|
||||||
defaultEmptyAs,
|
|
||||||
} from './lib/utils/string'
|
|
||||||
export { getPointerInfo, getSvgPathFromStroke, getSvgPathFromStrokePoints } from './lib/utils/svg'
|
|
||||||
export { type TLStoreWithStatus } from './lib/utils/sync/StoreWithStatus'
|
export { type TLStoreWithStatus } from './lib/utils/sync/StoreWithStatus'
|
||||||
export { hardReset } from './lib/utils/sync/hardReset'
|
export { hardReset } from './lib/utils/sync/hardReset'
|
||||||
|
export { uniq } from './lib/utils/uniq'
|
||||||
|
export { uniqueId } from './lib/utils/uniqueId'
|
||||||
export { openWindow } from './lib/utils/window-open'
|
export { openWindow } from './lib/utils/window-open'
|
||||||
|
|
||||||
|
/** @polyfills */
|
||||||
|
|
||||||
|
import 'core-js/stable/array/at'
|
||||||
|
import 'core-js/stable/array/flat'
|
||||||
|
import 'core-js/stable/array/flat-map'
|
||||||
|
import 'core-js/stable/string/at'
|
||||||
|
import 'core-js/stable/string/replace-all'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SerializedStore, Store } from '@tldraw/store'
|
import { SerializedStore, Store } from '@tldraw/store'
|
||||||
import { TLRecord, TLStore } from '@tldraw/tlschema'
|
import { TLRecord, TLStore } from '@tldraw/tlschema'
|
||||||
import { RecursivePartial, Required, annotateError } from '@tldraw/utils'
|
import { Required, annotateError } from '@tldraw/utils'
|
||||||
import React, {
|
import React, {
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
@ -10,11 +10,11 @@ import React, {
|
||||||
useSyncExternalStore,
|
useSyncExternalStore,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
import { TLEditorAssetUrls, useDefaultEditorAssetsWithOverrides } from './assetUrls'
|
import { Canvas } from './components/Canvas'
|
||||||
import { DefaultErrorFallback } from './components/DefaultErrorFallback'
|
|
||||||
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
||||||
|
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
|
||||||
import { TLUser, createTLUser } from './config/createTLUser'
|
import { TLUser, createTLUser } from './config/createTLUser'
|
||||||
import { AnyTLShapeInfo } from './config/defineShape'
|
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
|
||||||
import { Editor } from './editor/Editor'
|
import { Editor } from './editor/Editor'
|
||||||
import { TLStateNodeConstructor } from './editor/tools/StateNode'
|
import { TLStateNodeConstructor } from './editor/tools/StateNode'
|
||||||
import { ContainerProvider, useContainer } from './hooks/useContainer'
|
import { ContainerProvider, useContainer } from './hooks/useContainer'
|
||||||
|
@ -29,7 +29,6 @@ import {
|
||||||
import { useEvent } from './hooks/useEvent'
|
import { useEvent } from './hooks/useEvent'
|
||||||
import { useForceUpdate } from './hooks/useForceUpdate'
|
import { useForceUpdate } from './hooks/useForceUpdate'
|
||||||
import { useLocalStore } from './hooks/useLocalStore'
|
import { useLocalStore } from './hooks/useLocalStore'
|
||||||
import { usePreloadAssets } from './hooks/usePreloadAssets'
|
|
||||||
import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix'
|
import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix'
|
||||||
import { useZoomCss } from './hooks/useZoomCss'
|
import { useZoomCss } from './hooks/useZoomCss'
|
||||||
import { TLStoreWithStatus } from './utils/sync/StoreWithStatus'
|
import { TLStoreWithStatus } from './utils/sync/StoreWithStatus'
|
||||||
|
@ -65,20 +64,15 @@ export interface TldrawEditorBaseProps {
|
||||||
children?: any
|
children?: any
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of shapes definitions to make available to the editor.
|
* An array of shape utils to use in the editor.
|
||||||
*/
|
*/
|
||||||
shapes?: readonly AnyTLShapeInfo[]
|
shapeUtils?: readonly TLAnyShapeUtilConstructor[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of tools to add to the editor's state chart.
|
* An array of tools to add to the editor's state chart.
|
||||||
*/
|
*/
|
||||||
tools?: readonly TLStateNodeConstructor[]
|
tools?: readonly TLStateNodeConstructor[]
|
||||||
|
|
||||||
/**
|
|
||||||
* Urls for the editor to find fonts and other assets.
|
|
||||||
*/
|
|
||||||
assetUrls?: RecursivePartial<TLEditorAssetUrls>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to automatically focus the editor when it mounts.
|
* Whether to automatically focus the editor when it mounts.
|
||||||
*/
|
*/
|
||||||
|
@ -93,6 +87,11 @@ export interface TldrawEditorBaseProps {
|
||||||
* Called when the editor has mounted.
|
* Called when the editor has mounted.
|
||||||
*/
|
*/
|
||||||
onMount?: TLOnMountHandler
|
onMount?: TLOnMountHandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The editor's initial state (usually the id of the first active tool).
|
||||||
|
*/
|
||||||
|
initialState?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -113,7 +112,7 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_SHAPES_ARRAY = [] as const
|
const EMPTY_SHAPE_UTILS_ARRAY = [] as const
|
||||||
const EMPTY_TOOLS_ARRAY = [] as const
|
const EMPTY_TOOLS_ARRAY = [] as const
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -133,7 +132,7 @@ export const TldrawEditor = memo(function TldrawEditor({
|
||||||
// defaults applied in @tldraw/tldraw.
|
// defaults applied in @tldraw/tldraw.
|
||||||
const withDefaults = {
|
const withDefaults = {
|
||||||
...rest,
|
...rest,
|
||||||
shapes: rest.shapes ?? EMPTY_SHAPES_ARRAY,
|
shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY,
|
||||||
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
|
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,12 +166,12 @@ export const TldrawEditor = memo(function TldrawEditor({
|
||||||
})
|
})
|
||||||
|
|
||||||
function TldrawEditorWithOwnStore(
|
function TldrawEditorWithOwnStore(
|
||||||
props: Required<TldrawEditorProps & { store: undefined; user: TLUser }, 'shapes' | 'tools'>
|
props: Required<TldrawEditorProps & { store: undefined; user: TLUser }, 'shapeUtils' | 'tools'>
|
||||||
) {
|
) {
|
||||||
const { defaultName, initialData, shapes, persistenceKey, sessionId, user } = props
|
const { defaultName, initialData, shapeUtils, persistenceKey, sessionId, user } = props
|
||||||
|
|
||||||
const syncedStore = useLocalStore({
|
const syncedStore = useLocalStore({
|
||||||
shapes,
|
shapeUtils,
|
||||||
initialData,
|
initialData,
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
sessionId,
|
sessionId,
|
||||||
|
@ -186,7 +185,10 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
|
||||||
store,
|
store,
|
||||||
user,
|
user,
|
||||||
...rest
|
...rest
|
||||||
}: Required<TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser }, 'shapes' | 'tools'>) {
|
}: Required<
|
||||||
|
TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser },
|
||||||
|
'shapeUtils' | 'tools'
|
||||||
|
>) {
|
||||||
const container = useContainer()
|
const container = useContainer()
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
@ -225,16 +227,16 @@ function TldrawEditorWithReadyStore({
|
||||||
children,
|
children,
|
||||||
store,
|
store,
|
||||||
tools,
|
tools,
|
||||||
shapes,
|
shapeUtils,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
user,
|
user,
|
||||||
assetUrls,
|
initialState,
|
||||||
}: Required<
|
}: Required<
|
||||||
TldrawEditorProps & {
|
TldrawEditorProps & {
|
||||||
store: TLStore
|
store: TLStore
|
||||||
user: TLUser
|
user: TLUser
|
||||||
},
|
},
|
||||||
'shapes' | 'tools'
|
'shapeUtils' | 'tools'
|
||||||
>) {
|
>) {
|
||||||
const { ErrorFallback } = useEditorComponents()
|
const { ErrorFallback } = useEditorComponents()
|
||||||
const container = useContainer()
|
const container = useContainer()
|
||||||
|
@ -243,10 +245,11 @@ function TldrawEditorWithReadyStore({
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const editor = new Editor({
|
const editor = new Editor({
|
||||||
store,
|
store,
|
||||||
shapes,
|
shapeUtils,
|
||||||
tools,
|
tools,
|
||||||
getContainer: () => container,
|
getContainer: () => container,
|
||||||
user,
|
user,
|
||||||
|
initialState,
|
||||||
})
|
})
|
||||||
;(window as any).app = editor
|
;(window as any).app = editor
|
||||||
;(window as any).editor = editor
|
;(window as any).editor = editor
|
||||||
|
@ -255,10 +258,10 @@ function TldrawEditorWithReadyStore({
|
||||||
return () => {
|
return () => {
|
||||||
editor.dispose()
|
editor.dispose()
|
||||||
}
|
}
|
||||||
}, [container, shapes, tools, store, user])
|
}, [container, shapeUtils, tools, store, user, initialState])
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
if (editor && autoFocus) editor.focus()
|
if (editor && autoFocus) editor.isFocused = true
|
||||||
}, [editor, autoFocus])
|
}, [editor, autoFocus])
|
||||||
|
|
||||||
const onMountEvent = useEvent((editor: Editor) => {
|
const onMountEvent = useEvent((editor: Editor) => {
|
||||||
|
@ -288,17 +291,6 @@ function TldrawEditorWithReadyStore({
|
||||||
() => editor?.crashingError ?? null
|
() => editor?.crashingError ?? null
|
||||||
)
|
)
|
||||||
|
|
||||||
const assets = useDefaultEditorAssetsWithOverrides(assetUrls)
|
|
||||||
const { done: preloadingComplete, error: preloadingError } = usePreloadAssets(assets)
|
|
||||||
|
|
||||||
if (preloadingError) {
|
|
||||||
return <ErrorScreen>Could not load assets. Please refresh the page.</ErrorScreen>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preloadingComplete) {
|
|
||||||
return <LoadingScreen>Loading assets...</LoadingScreen>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -311,7 +303,7 @@ function TldrawEditorWithReadyStore({
|
||||||
// document in the event of an error to reassure them that their work is
|
// document in the event of an error to reassure them that their work is
|
||||||
// not lost.
|
// not lost.
|
||||||
<OptionalErrorBoundary
|
<OptionalErrorBoundary
|
||||||
fallback={ErrorFallback}
|
fallback={ErrorFallback as any}
|
||||||
onError={(error) =>
|
onError={(error) =>
|
||||||
editor.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })
|
editor.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })
|
||||||
}
|
}
|
||||||
|
@ -334,7 +326,7 @@ function Layout({ children }: { children: any }) {
|
||||||
useSafariFocusOutFix()
|
useSafariFocusOutFix()
|
||||||
useForceUpdate()
|
useForceUpdate()
|
||||||
|
|
||||||
return children
|
return children ?? <Canvas />
|
||||||
}
|
}
|
||||||
|
|
||||||
function Crash({ crashingError }: { crashingError: unknown }): null {
|
function Crash({ crashingError }: { crashingError: unknown }): null {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Matrix2d, toDomPrecision } from '@tldraw/primitives'
|
|
||||||
import { react, track, useQuickReactor, useValue } from '@tldraw/state'
|
import { react, track, useQuickReactor, useValue } from '@tldraw/state'
|
||||||
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||||
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
|
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
|
||||||
|
@ -12,10 +11,10 @@ import { useFixSafariDoubleTapZoomPencilEvents } from '../hooks/useFixSafariDoub
|
||||||
import { useGestureEvents } from '../hooks/useGestureEvents'
|
import { useGestureEvents } from '../hooks/useGestureEvents'
|
||||||
import { useHandleEvents } from '../hooks/useHandleEvents'
|
import { useHandleEvents } from '../hooks/useHandleEvents'
|
||||||
import { useScreenBounds } from '../hooks/useScreenBounds'
|
import { useScreenBounds } from '../hooks/useScreenBounds'
|
||||||
|
import { Matrix2d } from '../primitives/Matrix2d'
|
||||||
|
import { toDomPrecision } from '../primitives/utils'
|
||||||
import { debugFlags } from '../utils/debug-flags'
|
import { debugFlags } from '../utils/debug-flags'
|
||||||
import { LiveCollaborators } from './LiveCollaborators'
|
import { LiveCollaborators } from './LiveCollaborators'
|
||||||
import { SelectionBg } from './SelectionBg'
|
|
||||||
import { SelectionFg } from './SelectionFg'
|
|
||||||
import { Shape } from './Shape'
|
import { Shape } from './Shape'
|
||||||
import { ShapeIndicator } from './ShapeIndicator'
|
import { ShapeIndicator } from './ShapeIndicator'
|
||||||
|
|
||||||
|
@ -97,7 +96,7 @@ export const Canvas = track(function Canvas() {
|
||||||
{SvgDefs && <SvgDefs />}
|
{SvgDefs && <SvgDefs />}
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<SelectionBg />
|
<SelectionBackgroundWrapper />
|
||||||
<div className="tl-shapes">
|
<div className="tl-shapes">
|
||||||
<ShapesToDisplay />
|
<ShapesToDisplay />
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,7 +109,7 @@ export const Canvas = track(function Canvas() {
|
||||||
<HoveredShapeIndicator />
|
<HoveredShapeIndicator />
|
||||||
<HintedShapeIndicator />
|
<HintedShapeIndicator />
|
||||||
<SnapLinesWrapper />
|
<SnapLinesWrapper />
|
||||||
<SelectionFg />
|
<SelectionForegroundWrapper />
|
||||||
<LiveCollaborators />
|
<LiveCollaborators />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -124,12 +123,13 @@ const GridWrapper = track(function GridWrapper() {
|
||||||
|
|
||||||
// get grid from context
|
// get grid from context
|
||||||
|
|
||||||
|
const { gridSize } = editor.documentSettings
|
||||||
const { x, y, z } = editor.camera
|
const { x, y, z } = editor.camera
|
||||||
const isGridMode = editor.isGridMode
|
const isGridMode = editor.isGridMode
|
||||||
|
|
||||||
if (!(Grid && isGridMode)) return null
|
if (!(Grid && isGridMode)) return null
|
||||||
|
|
||||||
return <Grid x={x} y={y} z={z} size={editor.gridSize} />
|
return <Grid x={x} y={y} z={z} size={gridSize} />
|
||||||
})
|
})
|
||||||
|
|
||||||
const ScribbleWrapper = track(function ScribbleWrapper() {
|
const ScribbleWrapper = track(function ScribbleWrapper() {
|
||||||
|
@ -432,3 +432,15 @@ const UiLogger = track(() => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function SelectionForegroundWrapper() {
|
||||||
|
const { SelectionForeground } = useEditorComponents()
|
||||||
|
if (!SelectionForeground) return null
|
||||||
|
return <SelectionForeground />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectionBackgroundWrapper() {
|
||||||
|
const { SelectionBackground } = useEditorComponents()
|
||||||
|
if (!SelectionBackground) return null
|
||||||
|
return <SelectionBackground />
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
/** @public */
|
|
||||||
export type TLBackgroundComponent = () => JSX.Element | null
|
|
||||||
|
|
||||||
export function DefaultBackground() {
|
|
||||||
return <div className="tl-background" />
|
|
||||||
}
|
|
|
@ -1,11 +1,11 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TLErrorFallbackComponent } from './DefaultErrorFallback'
|
import { TLErrorFallbackComponent } from './default-components/DefaultErrorFallback'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export interface TLErrorBoundaryProps {
|
export interface TLErrorBoundaryProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
onError?: ((error: unknown) => void) | null
|
onError?: ((error: unknown) => void) | null
|
||||||
fallback: (props: { error: unknown }) => any
|
fallback: TLErrorFallbackComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
type TLErrorBoundaryState = { error: Error | null }
|
type TLErrorBoundaryState = { error: Error | null }
|
||||||
|
@ -21,13 +21,13 @@ export class ErrorBoundary extends React.Component<
|
||||||
return { error }
|
return { error }
|
||||||
}
|
}
|
||||||
|
|
||||||
state = initialState
|
override state = initialState
|
||||||
|
|
||||||
componentDidCatch(error: unknown) {
|
override componentDidCatch(error: unknown) {
|
||||||
this.props.onError?.(error)
|
this.props.onError?.(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
override render() {
|
||||||
const { error } = this.state
|
const { error } = this.state
|
||||||
|
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
|
@ -52,7 +52,7 @@ export function OptionalErrorBoundary({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary fallback={fallback} {...props}>
|
<ErrorBoundary fallback={fallback as any} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
|
|
30
packages/editor/src/lib/components/PositionedOnCanvas.tsx
Normal file
30
packages/editor/src/lib/components/PositionedOnCanvas.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { track } from '@tldraw/state'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { HTMLProps, useLayoutEffect, useRef } from 'react'
|
||||||
|
import { useEditor } from '../hooks/useEditor'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export const PositionedOnCanvas = track(function PositionedOnCanvas({
|
||||||
|
x: offsetX = 0,
|
||||||
|
y: offsetY = 0,
|
||||||
|
rotation = 0,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
x?: number
|
||||||
|
y?: number
|
||||||
|
rotation?: number
|
||||||
|
} & HTMLProps<HTMLDivElement>) {
|
||||||
|
const editor = useEditor()
|
||||||
|
const rContainer = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const { x, y, z } = editor.camera
|
||||||
|
const elm = rContainer.current
|
||||||
|
if (!elm) return
|
||||||
|
if (x === undefined) return
|
||||||
|
|
||||||
|
elm.style.transform = `translate(${x}px, ${y}px) scale(${z}) rotate(${rotation}rad) translate(${offsetX}px, ${offsetY}px)`
|
||||||
|
}, [editor.camera, offsetX, offsetY, rotation])
|
||||||
|
|
||||||
|
return <div ref={rContainer} {...rest} className={classNames('tl-positioned', rest.className)} />
|
||||||
|
})
|
|
@ -1,135 +0,0 @@
|
||||||
import { Matrix2d, toDomPrecision } from '@tldraw/primitives'
|
|
||||||
import { track } from '@tldraw/state'
|
|
||||||
import * as React from 'react'
|
|
||||||
import { TLPointerEventInfo } from '../editor/types/event-types'
|
|
||||||
import { useEditor } from '../hooks/useEditor'
|
|
||||||
import { releasePointerCapture, setPointerCapture } from '../utils/dom'
|
|
||||||
import { getPointerInfo } from '../utils/svg'
|
|
||||||
|
|
||||||
export const SelectionBg = track(function SelectionBg() {
|
|
||||||
const editor = useEditor()
|
|
||||||
|
|
||||||
const events = React.useMemo(() => {
|
|
||||||
const onPointerDown = (e: React.PointerEvent) => {
|
|
||||||
if ((e as any).isKilled) return
|
|
||||||
|
|
||||||
setPointerCapture(e.currentTarget, e)
|
|
||||||
|
|
||||||
const info: TLPointerEventInfo = {
|
|
||||||
type: 'pointer',
|
|
||||||
target: 'selection',
|
|
||||||
name: 'pointer_down',
|
|
||||||
...getPointerInfo(e, editor.getContainer()),
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.dispatch(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPointerMove = (e: React.PointerEvent) => {
|
|
||||||
if ((e as any).isKilled) return
|
|
||||||
|
|
||||||
const info: TLPointerEventInfo = {
|
|
||||||
type: 'pointer',
|
|
||||||
target: 'selection',
|
|
||||||
name: 'pointer_move',
|
|
||||||
...getPointerInfo(e, editor.getContainer()),
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.dispatch(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPointerUp = (e: React.PointerEvent) => {
|
|
||||||
if ((e as any).isKilled) return
|
|
||||||
|
|
||||||
releasePointerCapture(e.currentTarget, e)
|
|
||||||
|
|
||||||
const info: TLPointerEventInfo = {
|
|
||||||
type: 'pointer',
|
|
||||||
target: 'selection',
|
|
||||||
name: 'pointer_up',
|
|
||||||
...getPointerInfo(e, editor.getContainer()),
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.dispatch(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPointerEnter = (e: React.PointerEvent) => {
|
|
||||||
if ((e as any).isKilled) return
|
|
||||||
|
|
||||||
const info: TLPointerEventInfo = {
|
|
||||||
type: 'pointer',
|
|
||||||
target: 'selection',
|
|
||||||
name: 'pointer_enter',
|
|
||||||
...getPointerInfo(e, editor.getContainer()),
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.dispatch(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPointerLeave = (e: React.PointerEvent) => {
|
|
||||||
if ((e as any).isKilled) return
|
|
||||||
|
|
||||||
const info: TLPointerEventInfo = {
|
|
||||||
type: 'pointer',
|
|
||||||
target: 'selection',
|
|
||||||
name: 'pointer_leave',
|
|
||||||
...getPointerInfo(e, editor.getContainer()),
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.dispatch(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
onPointerDown,
|
|
||||||
onPointerMove,
|
|
||||||
onPointerUp,
|
|
||||||
onPointerEnter,
|
|
||||||
onPointerLeave,
|
|
||||||
}
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
const { selectionBounds: bounds, selectedIds } = editor
|
|
||||||
if (!bounds) return null
|
|
||||||
|
|
||||||
const shouldDisplay = editor.isInAny(
|
|
||||||
'select.idle',
|
|
||||||
'select.brushing',
|
|
||||||
'select.scribble_brushing',
|
|
||||||
'select.pointing_shape',
|
|
||||||
'select.pointing_selection',
|
|
||||||
'text.resizing'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (selectedIds.length === 1) {
|
|
||||||
const shape = editor.getShapeById(selectedIds[0])
|
|
||||||
if (!shape) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const util = editor.getShapeUtil(shape)
|
|
||||||
if (util.hideSelectionBoundsBg(shape)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const transform = Matrix2d.toCssString(
|
|
||||||
Matrix2d.Compose(
|
|
||||||
Matrix2d.Translate(bounds.minX, bounds.minY),
|
|
||||||
Matrix2d.Rotate(editor.selectionRotation)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="tl-selection__bg"
|
|
||||||
draggable={false}
|
|
||||||
style={{
|
|
||||||
transform,
|
|
||||||
width: toDomPrecision(Math.max(1, bounds.width)),
|
|
||||||
height: toDomPrecision(Math.max(1, bounds.height)),
|
|
||||||
pointerEvents: shouldDisplay ? 'all' : 'none',
|
|
||||||
opacity: shouldDisplay ? 1 : 0,
|
|
||||||
}}
|
|
||||||
{...events}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Matrix2d } from '@tldraw/primitives'
|
|
||||||
import { track, useQuickReactor, useStateTracking } from '@tldraw/state'
|
import { track, useQuickReactor, useStateTracking } from '@tldraw/state'
|
||||||
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEditor } from '../..'
|
|
||||||
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
|
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
|
||||||
|
import { useEditor } from '../hooks/useEditor'
|
||||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||||
import { useShapeEvents } from '../hooks/useShapeEvents'
|
import { useShapeEvents } from '../hooks/useShapeEvents'
|
||||||
|
import { Matrix2d } from '../primitives/Matrix2d'
|
||||||
import { OptionalErrorBoundary } from './ErrorBoundary'
|
import { OptionalErrorBoundary } from './ErrorBoundary'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -135,7 +135,7 @@ export const Shape = track(function Shape({
|
||||||
{isCulled && util.canUnmount(shape) ? (
|
{isCulled && util.canUnmount(shape) ? (
|
||||||
<CulledShape shape={shape} />
|
<CulledShape shape={shape} />
|
||||||
) : (
|
) : (
|
||||||
<OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
|
<OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
|
||||||
<InnerShape shape={shape} util={util} />
|
<InnerShape shape={shape} util={util} />
|
||||||
</OptionalErrorBoundary>
|
</OptionalErrorBoundary>
|
||||||
)}
|
)}
|
||||||
|
@ -146,7 +146,7 @@ export const Shape = track(function Shape({
|
||||||
|
|
||||||
const InnerShape = React.memo(
|
const InnerShape = React.memo(
|
||||||
function InnerShape<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) {
|
function InnerShape<T extends TLShape>({ shape, util }: { shape: T; util: ShapeUtil<T> }) {
|
||||||
return useStateTracking('InnerShape:' + util.type, () => util.component(shape))
|
return useStateTracking('InnerShape:' + shape.type, () => util.component(shape))
|
||||||
},
|
},
|
||||||
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
|
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
|
||||||
)
|
)
|
||||||
|
@ -159,7 +159,7 @@ const InnerShapeBackground = React.memo(
|
||||||
shape: T
|
shape: T
|
||||||
util: ShapeUtil<T>
|
util: ShapeUtil<T>
|
||||||
}) {
|
}) {
|
||||||
return useStateTracking('InnerShape:' + util.type, () => util.backgroundComponent?.(shape))
|
return useStateTracking('InnerShape:' + shape.type, () => util.backgroundComponent?.(shape))
|
||||||
},
|
},
|
||||||
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
|
(prev, next) => prev.shape.props === next.shape.props && prev.shape.meta === next.shape.meta
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { ComponentType } from 'react'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLBackgroundComponent = ComponentType<object> | null
|
||||||
|
|
||||||
|
export function DefaultBackground() {
|
||||||
|
return <div className="tl-background" />
|
||||||
|
}
|
|
@ -1,15 +1,15 @@
|
||||||
import { toDomPrecision } from '@tldraw/primitives'
|
|
||||||
import { Box2dModel } from '@tldraw/tlschema'
|
import { Box2dModel } from '@tldraw/tlschema'
|
||||||
import { useRef } from 'react'
|
import { ComponentType, useRef } from 'react'
|
||||||
import { useTransform } from '../hooks/useTransform'
|
import { useTransform } from '../../hooks/useTransform'
|
||||||
|
import { toDomPrecision } from '../../primitives/utils'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLBrushComponent = (props: {
|
export type TLBrushComponent = ComponentType<{
|
||||||
brush: Box2dModel
|
brush: Box2dModel
|
||||||
color?: string
|
color?: string
|
||||||
opacity?: number
|
opacity?: number
|
||||||
className?: string
|
className?: string
|
||||||
}) => any | null
|
}>
|
||||||
|
|
||||||
export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
|
export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
|
||||||
const rSvg = useRef<SVGSVGElement>(null)
|
const rSvg = useRef<SVGSVGElement>(null)
|
|
@ -1,17 +1,19 @@
|
||||||
import { Box2d, clamp, Vec2d } from '@tldraw/primitives'
|
|
||||||
import { Vec2dModel } from '@tldraw/tlschema'
|
import { Vec2dModel } from '@tldraw/tlschema'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useRef } from 'react'
|
import { ComponentType, useRef } from 'react'
|
||||||
import { useTransform } from '../hooks/useTransform'
|
import { useTransform } from '../../hooks/useTransform'
|
||||||
|
import { Box2d } from '../../primitives/Box2d'
|
||||||
|
import { Vec2d } from '../../primitives/Vec2d'
|
||||||
|
import { clamp } from '../../primitives/utils'
|
||||||
|
|
||||||
export type TLCollaboratorHintComponent = (props: {
|
export type TLCollaboratorHintComponent = ComponentType<{
|
||||||
className?: string
|
className?: string
|
||||||
point: Vec2dModel
|
point: Vec2dModel
|
||||||
viewport: Box2d
|
viewport: Box2d
|
||||||
zoom: number
|
zoom: number
|
||||||
opacity?: number
|
opacity?: number
|
||||||
color: string
|
color: string
|
||||||
}) => JSX.Element | null
|
}>
|
||||||
|
|
||||||
export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({
|
export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({
|
||||||
className,
|
className,
|
|
@ -1,17 +1,17 @@
|
||||||
import { Vec2dModel } from '@tldraw/tlschema'
|
import { Vec2dModel } from '@tldraw/tlschema'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { memo, useRef } from 'react'
|
import { ComponentType, memo, useRef } from 'react'
|
||||||
import { useTransform } from '../hooks/useTransform'
|
import { useTransform } from '../../hooks/useTransform'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLCursorComponent = (props: {
|
export type TLCursorComponent = ComponentType<{
|
||||||
className?: string
|
className?: string
|
||||||
point: Vec2dModel | null
|
point: Vec2dModel | null
|
||||||
zoom: number
|
zoom: number
|
||||||
color?: string
|
color?: string
|
||||||
name: string | null
|
name: string | null
|
||||||
chatMessage: string
|
chatMessage: string
|
||||||
}) => any | null
|
}>
|
||||||
|
|
||||||
const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name, chatMessage }) => {
|
const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name, chatMessage }) => {
|
||||||
const rCursor = useRef<HTMLDivElement>(null)
|
const rCursor = useRef<HTMLDivElement>(null)
|
|
@ -1,12 +1,12 @@
|
||||||
import { useValue } from '@tldraw/state'
|
import { useValue } from '@tldraw/state'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
import { ComponentType, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import { Editor } from '../editor/Editor'
|
import { Editor } from '../../editor/Editor'
|
||||||
import { EditorContext } from '../hooks/useEditor'
|
import { EditorContext } from '../../hooks/useEditor'
|
||||||
import { hardResetEditor } from '../utils/hard-reset'
|
import { hardResetEditor } from '../../utils/hardResetEditor'
|
||||||
import { refreshPage } from '../utils/refresh-page'
|
import { refreshPage } from '../../utils/refreshPage'
|
||||||
import { Canvas } from './Canvas'
|
import { Canvas } from '../Canvas'
|
||||||
import { ErrorBoundary } from './ErrorBoundary'
|
import { ErrorBoundary } from '../ErrorBoundary'
|
||||||
|
|
||||||
const BASE_ERROR_URL = 'https://github.com/tldraw/tldraw/issues/new'
|
const BASE_ERROR_URL = 'https://github.com/tldraw/tldraw/issues/new'
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ const BASE_ERROR_URL = 'https://github.com/tldraw/tldraw/issues/new'
|
||||||
function noop() {}
|
function noop() {}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLErrorFallbackComponent = (props: { error: unknown; editor?: Editor }) => any | null
|
export type TLErrorFallbackComponent = ComponentType<{ error: unknown; editor?: Editor }>
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }) => {
|
export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }) => {
|
|
@ -1,13 +1,14 @@
|
||||||
import { modulate } from '@tldraw/utils'
|
import { modulate } from '@tldraw/utils'
|
||||||
import { GRID_STEPS } from '../constants'
|
import { ComponentType } from 'react'
|
||||||
|
import { GRID_STEPS } from '../../constants'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLGridComponent = (props: {
|
export type TLGridComponent = ComponentType<{
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
z: number
|
z: number
|
||||||
size: number
|
size: number
|
||||||
}) => JSX.Element | null
|
}>
|
||||||
|
|
||||||
export const DefaultGrid: TLGridComponent = ({ x, y, z, size }) => {
|
export const DefaultGrid: TLGridComponent = ({ x, y, z, size }) => {
|
||||||
return (
|
return (
|
|
@ -1,11 +1,12 @@
|
||||||
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import { ComponentType } from 'react'
|
||||||
|
|
||||||
export type TLHandleComponent = (props: {
|
export type TLHandleComponent = ComponentType<{
|
||||||
shapeId: TLShapeId
|
shapeId: TLShapeId
|
||||||
handle: TLHandle
|
handle: TLHandle
|
||||||
className?: string
|
className?: string
|
||||||
}) => any | null
|
}>
|
||||||
|
|
||||||
export const DefaultHandle: TLHandleComponent = ({ handle, className }) => {
|
export const DefaultHandle: TLHandleComponent = ({ handle, className }) => {
|
||||||
return (
|
return (
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { TLScribble } from '@tldraw/tlschema'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { ComponentType } from 'react'
|
||||||
|
import { getSvgPathFromPoints } from '../../utils/getSvgPathFromPoints'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLScribbleComponent = ComponentType<{
|
||||||
|
scribble: TLScribble
|
||||||
|
zoom: number
|
||||||
|
color?: string
|
||||||
|
opacity?: number
|
||||||
|
className?: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const DefaultScribble: TLScribbleComponent = ({
|
||||||
|
scribble,
|
||||||
|
zoom,
|
||||||
|
color,
|
||||||
|
opacity,
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
if (!scribble.points.length) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg className={className ? classNames('tl-overlays__item', className) : className}>
|
||||||
|
<path
|
||||||
|
className="tl-scribble"
|
||||||
|
d={getSvgPathFromPoints(scribble.points, false)}
|
||||||
|
stroke={color ?? `var(--color-${scribble.color})`}
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={8 / zoom}
|
||||||
|
opacity={opacity ?? scribble.opacity}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { track } from '@tldraw/state'
|
||||||
|
import * as React from 'react'
|
||||||
|
import { TLPointerEventInfo } from '../../editor/types/event-types'
|
||||||
|
import { useEditor } from '../../hooks/useEditor'
|
||||||
|
import { Matrix2d } from '../../primitives/Matrix2d'
|
||||||
|
import { toDomPrecision } from '../../primitives/utils'
|
||||||
|
import { releasePointerCapture, setPointerCapture } from '../../utils/dom'
|
||||||
|
import { getPointerInfo } from '../../utils/getPointerInfo'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLSelectionBackgroundComponent = React.ComponentType<object>
|
||||||
|
|
||||||
|
export const DefaultSelectionBackground: TLSelectionBackgroundComponent = track(
|
||||||
|
function SelectionBg() {
|
||||||
|
const editor = useEditor()
|
||||||
|
|
||||||
|
const events = React.useMemo(() => {
|
||||||
|
const onPointerDown = (e: React.PointerEvent) => {
|
||||||
|
if ((e as any).isKilled) return
|
||||||
|
|
||||||
|
setPointerCapture(e.currentTarget, e)
|
||||||
|
|
||||||
|
const info: TLPointerEventInfo = {
|
||||||
|
type: 'pointer',
|
||||||
|
target: 'selection',
|
||||||
|
name: 'pointer_down',
|
||||||
|
...getPointerInfo(e, editor.getContainer()),
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.dispatch(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerMove = (e: React.PointerEvent) => {
|
||||||
|
if ((e as any).isKilled) return
|
||||||
|
|
||||||
|
const info: TLPointerEventInfo = {
|
||||||
|
type: 'pointer',
|
||||||
|
target: 'selection',
|
||||||
|
name: 'pointer_move',
|
||||||
|
...getPointerInfo(e, editor.getContainer()),
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.dispatch(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerUp = (e: React.PointerEvent) => {
|
||||||
|
if ((e as any).isKilled) return
|
||||||
|
|
||||||
|
releasePointerCapture(e.currentTarget, e)
|
||||||
|
|
||||||
|
const info: TLPointerEventInfo = {
|
||||||
|
type: 'pointer',
|
||||||
|
target: 'selection',
|
||||||
|
name: 'pointer_up',
|
||||||
|
...getPointerInfo(e, editor.getContainer()),
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.dispatch(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerEnter = (e: React.PointerEvent) => {
|
||||||
|
if ((e as any).isKilled) return
|
||||||
|
|
||||||
|
const info: TLPointerEventInfo = {
|
||||||
|
type: 'pointer',
|
||||||
|
target: 'selection',
|
||||||
|
name: 'pointer_enter',
|
||||||
|
...getPointerInfo(e, editor.getContainer()),
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.dispatch(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerLeave = (e: React.PointerEvent) => {
|
||||||
|
if ((e as any).isKilled) return
|
||||||
|
|
||||||
|
const info: TLPointerEventInfo = {
|
||||||
|
type: 'pointer',
|
||||||
|
target: 'selection',
|
||||||
|
name: 'pointer_leave',
|
||||||
|
...getPointerInfo(e, editor.getContainer()),
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.dispatch(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
onPointerDown,
|
||||||
|
onPointerMove,
|
||||||
|
onPointerUp,
|
||||||
|
onPointerEnter,
|
||||||
|
onPointerLeave,
|
||||||
|
}
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
const { selectionBounds: bounds, selectedIds } = editor
|
||||||
|
if (!bounds) return null
|
||||||
|
|
||||||
|
const shouldDisplay = editor.isInAny(
|
||||||
|
'select.idle',
|
||||||
|
'select.brushing',
|
||||||
|
'select.scribble_brushing',
|
||||||
|
'select.pointing_shape',
|
||||||
|
'select.pointing_selection',
|
||||||
|
'text.resizing'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedIds.length === 1) {
|
||||||
|
const shape = editor.getShapeById(selectedIds[0])
|
||||||
|
if (!shape) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const util = editor.getShapeUtil(shape)
|
||||||
|
if (util.hideSelectionBoundsBg(shape)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transform = Matrix2d.toCssString(
|
||||||
|
Matrix2d.Compose(
|
||||||
|
Matrix2d.Translate(bounds.minX, bounds.minY),
|
||||||
|
Matrix2d.Rotate(editor.selectionRotation)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="tl-selection__bg"
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
transform,
|
||||||
|
width: toDomPrecision(Math.max(1, bounds.width)),
|
||||||
|
height: toDomPrecision(Math.max(1, bounds.height)),
|
||||||
|
pointerEvents: shouldDisplay ? 'all' : 'none',
|
||||||
|
opacity: shouldDisplay ? 1 : 0,
|
||||||
|
}}
|
||||||
|
{...events}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { track } from '@tldraw/state'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { ComponentType, useRef } from 'react'
|
||||||
|
import { useEditor } from '../../hooks/useEditor'
|
||||||
|
import { useTransform } from '../../hooks/useTransform'
|
||||||
|
import { toDomPrecision } from '../../primitives/utils'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export type TLSelectionForegroundComponent = ComponentType<object>
|
||||||
|
|
||||||
|
export const DefaultSelectionForeground: TLSelectionForegroundComponent = track(() => {
|
||||||
|
const editor = useEditor()
|
||||||
|
const rSvg = useRef<SVGSVGElement>(null)
|
||||||
|
|
||||||
|
let bounds = editor.selectionBounds
|
||||||
|
|
||||||
|
const onlyShape = editor.onlySelectedShape
|
||||||
|
|
||||||
|
// if all shapes have an expandBy for the selection outline, we can expand by the l
|
||||||
|
const expandOutlineBy = onlyShape
|
||||||
|
? editor.getShapeUtil(onlyShape).expandSelectionOutlinePx(onlyShape)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
useTransform(rSvg, bounds?.x, bounds?.y, 1, editor.selectionRotation, {
|
||||||
|
x: -expandOutlineBy,
|
||||||
|
y: -expandOutlineBy,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!bounds) return null
|
||||||
|
|
||||||
|
bounds = bounds.clone().expandBy(expandOutlineBy)
|
||||||
|
|
||||||
|
const width = Math.max(1, bounds.width)
|
||||||
|
const height = Math.max(1, bounds.height)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={rSvg}
|
||||||
|
className="tl-overlays__item tl-selection__fg"
|
||||||
|
data-testid="selection-foreground"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
className={classNames('tl-selection__fg__outline')}
|
||||||
|
width={toDomPrecision(width)}
|
||||||
|
height={toDomPrecision(height)}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
})
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { ComponentType } from 'react'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLShapeErrorFallbackComponent = (props: { error: any }) => any | null
|
export type TLShapeErrorFallbackComponent = ComponentType<{ error: any }>
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const DefaultShapeErrorFallback: TLShapeErrorFallbackComponent = ({
|
export const DefaultShapeErrorFallback: TLShapeErrorFallbackComponent = ({
|
|
@ -1,7 +1,9 @@
|
||||||
|
import { ComponentType } from 'react'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLShapeIndicatorErrorFallback = (props: { error: unknown }) => any | null
|
export type TLShapeIndicatorErrorFallbackComponent = ComponentType<{ error: unknown }>
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const DefaultShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallback = () => {
|
export const DefaultShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent = () => {
|
||||||
return <circle cx={4} cy={4} r={8} strokeWidth="1" stroke="red" />
|
return <circle cx={4} cy={4} r={8} strokeWidth="1" stroke="red" />
|
||||||
}
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import { rangeIntersection } from '@tldraw/primitives'
|
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {
|
import {
|
||||||
type GapsSnapLine,
|
type GapsSnapLine,
|
||||||
type PointsSnapLine,
|
type PointsSnapLine,
|
||||||
type SnapLine,
|
type SnapLine,
|
||||||
} from '../editor/managers/SnapManager'
|
} from '../../editor/managers/SnapManager'
|
||||||
|
import { rangeIntersection } from '../../primitives/utils'
|
||||||
|
|
||||||
function PointsSnapLine({ points, zoom }: { zoom: number } & PointsSnapLine) {
|
function PointsSnapLine({ points, zoom }: { zoom: number } & PointsSnapLine) {
|
||||||
const l = 2.5 / zoom
|
const l = 2.5 / zoom
|
||||||
|
@ -153,11 +153,11 @@ function GapsSnapLine({ gaps, direction, zoom }: { zoom: number } & GapsSnapLine
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TLSnapLineComponent = (props: {
|
export type TLSnapLineComponent = React.ComponentType<{
|
||||||
className?: string
|
className?: string
|
||||||
line: SnapLine
|
line: SnapLine
|
||||||
zoom: number
|
zoom: number
|
||||||
}) => any
|
}>
|
||||||
|
|
||||||
export const DefaultSnapLine: TLSnapLineComponent = ({ className, line, zoom }) => {
|
export const DefaultSnapLine: TLSnapLineComponent = ({ className, line, zoom }) => {
|
||||||
return (
|
return (
|
|
@ -1,4 +1,6 @@
|
||||||
export type TLSpinnerComponent = () => any | null
|
import { ComponentType } from 'react'
|
||||||
|
|
||||||
|
export type TLSpinnerComponent = ComponentType<object>
|
||||||
|
|
||||||
export const DefaultSpinner: TLSpinnerComponent = () => {
|
export const DefaultSpinner: TLSpinnerComponent = () => {
|
||||||
return (
|
return (
|
|
@ -1,120 +0,0 @@
|
||||||
import { StrokePoint, toDomPrecision, Vec2d, VecLike } from '@tldraw/primitives'
|
|
||||||
|
|
||||||
export function getPointerInfo(e: React.PointerEvent | PointerEvent) {
|
|
||||||
;(e as any).isKilled = true
|
|
||||||
|
|
||||||
return {
|
|
||||||
point: {
|
|
||||||
x: e.clientX,
|
|
||||||
y: e.clientY,
|
|
||||||
z: e.pressure,
|
|
||||||
},
|
|
||||||
shiftKey: e.shiftKey,
|
|
||||||
altKey: e.altKey,
|
|
||||||
ctrlKey: e.metaKey || e.ctrlKey,
|
|
||||||
pointerId: e.pointerId,
|
|
||||||
button: e.button,
|
|
||||||
isPen: e.pointerType === 'pen',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function precise(A: VecLike) {
|
|
||||||
return `${toDomPrecision(A.x)},${toDomPrecision(A.y)} `
|
|
||||||
}
|
|
||||||
|
|
||||||
function average(A: VecLike, B: VecLike) {
|
|
||||||
return `${toDomPrecision((A.x + B.x) / 2)},${toDomPrecision((A.y + B.y) / 2)} `
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turn an array of points into a path of quadradic curves.
|
|
||||||
*
|
|
||||||
* @param points - The points returned from perfect-freehand
|
|
||||||
* @param closed - Whether the stroke is closed
|
|
||||||
*/
|
|
||||||
export function getSvgPathFromStroke(points: Vec2d[], closed = true): string {
|
|
||||||
const len = points.length
|
|
||||||
|
|
||||||
if (len < 2) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
let a = points[0]
|
|
||||||
let b = points[1]
|
|
||||||
|
|
||||||
if (len === 2) {
|
|
||||||
// If only two points, just draw a line
|
|
||||||
return `M${precise(a)}L${precise(b)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = ''
|
|
||||||
|
|
||||||
for (let i = 2, max = len - 1; i < max; i++) {
|
|
||||||
a = points[i]
|
|
||||||
b = points[i + 1]
|
|
||||||
result += average(a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closed) {
|
|
||||||
// If closed, draw a curve from the last point to the first
|
|
||||||
return `M${average(points[0], points[1])}Q${precise(points[1])}${average(
|
|
||||||
points[1],
|
|
||||||
points[2]
|
|
||||||
)}T${result}${average(points[len - 1], points[0])}${average(points[0], points[1])}Z`
|
|
||||||
} else {
|
|
||||||
// If not closed, draw a curve starting at the first point and
|
|
||||||
// ending at the midpoint of the last and second-last point, then
|
|
||||||
// complete the curve with a line segment to the last point.
|
|
||||||
return `M${precise(points[0])}Q${precise(points[1])}${average(points[1], points[2])}${
|
|
||||||
points.length > 3 ? 'T' : ''
|
|
||||||
}${result}L${precise(points[len - 1])}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turn an array of stroke points into a path of quadradic curves.
|
|
||||||
*
|
|
||||||
* @param points - The stroke points returned from perfect-freehand
|
|
||||||
* @param closed - Whether the shape is closed
|
|
||||||
*/
|
|
||||||
export function getSvgPathFromStrokePoints(points: StrokePoint[], closed = false): string {
|
|
||||||
const len = points.length
|
|
||||||
|
|
||||||
if (len < 2) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
let a = points[0].point
|
|
||||||
let b = points[1].point
|
|
||||||
|
|
||||||
if (len === 2) {
|
|
||||||
return `M${precise(a)}L${precise(b)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = ''
|
|
||||||
|
|
||||||
for (let i = 2, max = len - 1; i < max; i++) {
|
|
||||||
a = points[i].point
|
|
||||||
b = points[i + 1].point
|
|
||||||
result += average(a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closed) {
|
|
||||||
// If closed, draw a curve from the last point to the first
|
|
||||||
return `M${average(points[0].point, points[1].point)}Q${precise(points[1].point)}${average(
|
|
||||||
points[1].point,
|
|
||||||
points[2].point
|
|
||||||
)}T${result}${average(points[len - 1].point, points[0].point)}${average(
|
|
||||||
points[0].point,
|
|
||||||
points[1].point
|
|
||||||
)}Z`
|
|
||||||
} else {
|
|
||||||
// If not closed, draw a curve starting at the first point and
|
|
||||||
// ending at the midpoint of the last and second-last point, then
|
|
||||||
// complete the curve with a line segment to the last point.
|
|
||||||
return `M${precise(points[0].point)}Q${precise(points[1].point)}${average(
|
|
||||||
points[1].point,
|
|
||||||
points[2].point
|
|
||||||
)}${points.length > 3 ? 'T' : ''}${result}L${precise(points[len - 1].point)}`
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
} from '@tldraw/tlschema'
|
} from '@tldraw/tlschema'
|
||||||
import { objectMapFromEntries } from '@tldraw/utils'
|
import { objectMapFromEntries } from '@tldraw/utils'
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { uniqueId } from '../utils/data'
|
import { uniqueId } from '../utils/uniqueId'
|
||||||
|
|
||||||
const tabIdKey = 'TLDRAW_TAB_ID_v2' as const
|
const tabIdKey = 'TLDRAW_TAB_ID_v2' as const
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { atom } from '@tldraw/state'
|
||||||
import { defineMigrations, migrate } from '@tldraw/store'
|
import { defineMigrations, migrate } from '@tldraw/store'
|
||||||
import { getDefaultTranslationLocale } from '@tldraw/tlschema'
|
import { getDefaultTranslationLocale } from '@tldraw/tlschema'
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { uniqueId } from '../utils/data'
|
import { uniqueId } from '../utils/uniqueId'
|
||||||
|
|
||||||
const USER_DATA_KEY = 'TLDRAW_USER_DATA_v3'
|
const USER_DATA_KEY = 'TLDRAW_USER_DATA_v3'
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
import { HistoryEntry, SerializedStore, Store, StoreSchema } from '@tldraw/store'
|
import { HistoryEntry, SerializedStore, Store, StoreSchema } from '@tldraw/store'
|
||||||
import { TLRecord, TLStore, TLStoreProps, createTLSchema } from '@tldraw/tlschema'
|
import {
|
||||||
import { checkShapesAndAddCore } from './defaultShapes'
|
SchemaShapeInfo,
|
||||||
import { AnyTLShapeInfo, TLShapeInfo } from './defineShape'
|
TLRecord,
|
||||||
|
TLStore,
|
||||||
|
TLStoreProps,
|
||||||
|
TLUnknownShape,
|
||||||
|
createTLSchema,
|
||||||
|
} from '@tldraw/tlschema'
|
||||||
|
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
|
||||||
|
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLStoreOptions = {
|
export type TLStoreOptions = {
|
||||||
initialData?: SerializedStore<TLRecord>
|
initialData?: SerializedStore<TLRecord>
|
||||||
defaultName?: string
|
defaultName?: string
|
||||||
} & ({ shapes: readonly AnyTLShapeInfo[] } | { schema: StoreSchema<TLRecord, TLStoreProps> })
|
} & (
|
||||||
|
| { shapeUtils: readonly TLAnyShapeUtilConstructor[] }
|
||||||
|
| { schema: StoreSchema<TLRecord, TLStoreProps> }
|
||||||
|
)
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLStoreEventInfo = HistoryEntry<TLRecord>
|
export type TLStoreEventInfo = HistoryEntry<TLRecord>
|
||||||
|
@ -22,7 +32,7 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
|
||||||
const schema =
|
const schema =
|
||||||
'schema' in rest
|
'schema' in rest
|
||||||
? rest.schema
|
? rest.schema
|
||||||
: createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapes)) })
|
: createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapeUtils)) })
|
||||||
return new Store({
|
return new Store({
|
||||||
schema,
|
schema,
|
||||||
initialData,
|
initialData,
|
||||||
|
@ -32,6 +42,14 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function shapesArrayToShapeMap(shapes: TLShapeInfo[]) {
|
function shapesArrayToShapeMap(shapeUtils: TLShapeUtilConstructor<TLUnknownShape>[]) {
|
||||||
return Object.fromEntries(shapes.map((s) => [s.type, s]))
|
return Object.fromEntries(
|
||||||
|
shapeUtils.map((s): [string, SchemaShapeInfo] => [
|
||||||
|
s.type,
|
||||||
|
{
|
||||||
|
props: s.props,
|
||||||
|
migrations: s.migrations,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,19 @@
|
||||||
import { ArrowShape } from '../editor/shapes/arrow/ArrowShape'
|
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
|
||||||
import { BookmarkShape } from '../editor/shapes/bookmark/BookmarkShape'
|
import { GroupShapeUtil } from '../editor/shapes/group/GroupShapeUtil'
|
||||||
import { DrawShape } from '../editor/shapes/draw/DrawShape'
|
|
||||||
import { EmbedShape } from '../editor/shapes/embed/EmbedShape'
|
/** @public */
|
||||||
import { FrameShape } from '../editor/shapes/frame/FrameShape'
|
export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor<any>
|
||||||
import { GeoShape } from '../editor/shapes/geo/GeoShape'
|
|
||||||
import { GroupShape } from '../editor/shapes/group/GroupShape'
|
|
||||||
import { HighlightShape } from '../editor/shapes/highlight/HighlightShape'
|
|
||||||
import { ImageShape } from '../editor/shapes/image/ImageShape'
|
|
||||||
import { LineShape } from '../editor/shapes/line/LineShape'
|
|
||||||
import { NoteShape } from '../editor/shapes/note/NoteShape'
|
|
||||||
import { TextShape } from '../editor/shapes/text/TextShape'
|
|
||||||
import { VideoShape } from '../editor/shapes/video/VideoShape'
|
|
||||||
import { AnyTLShapeInfo, TLShapeInfo } from './defineShape'
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const coreShapes = [
|
export const coreShapes = [
|
||||||
// created by grouping interactions, probably the corest core shape that we have
|
// created by grouping interactions, probably the corest core shape that we have
|
||||||
GroupShape,
|
GroupShapeUtil,
|
||||||
// created by embed menu / url drop
|
|
||||||
EmbedShape,
|
|
||||||
// created by copy and paste / url drop
|
|
||||||
BookmarkShape,
|
|
||||||
// created by copy and paste / file drop
|
|
||||||
ImageShape,
|
|
||||||
// created by copy and paste
|
|
||||||
TextShape,
|
|
||||||
] as const
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const defaultShapes = [
|
|
||||||
DrawShape,
|
|
||||||
GeoShape,
|
|
||||||
LineShape,
|
|
||||||
NoteShape,
|
|
||||||
FrameShape,
|
|
||||||
ArrowShape,
|
|
||||||
HighlightShape,
|
|
||||||
VideoShape,
|
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
const coreShapeTypes = new Set<string>(coreShapes.map((s) => s.type))
|
const coreShapeTypes = new Set<string>(coreShapes.map((s) => s.type))
|
||||||
export function checkShapesAndAddCore(customShapes: readonly TLShapeInfo[]) {
|
|
||||||
const shapes: AnyTLShapeInfo[] = [...coreShapes]
|
export function checkShapesAndAddCore(customShapes: readonly TLAnyShapeUtilConstructor[]) {
|
||||||
|
const shapes = [...coreShapes] as TLAnyShapeUtilConstructor[]
|
||||||
|
|
||||||
const addedCustomShapeTypes = new Set<string>()
|
const addedCustomShapeTypes = new Set<string>()
|
||||||
for (const customShape of customShapes) {
|
for (const customShape of customShapes) {
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { ArrowShapeTool } from '../editor/shapes/arrow/ArrowShapeTool'
|
|
||||||
import { DrawShapeTool } from '../editor/shapes/draw/DrawShapeTool'
|
|
||||||
import { FrameShapeTool } from '../editor/shapes/frame/FrameShapeTool'
|
|
||||||
import { GeoShapeTool } from '../editor/shapes/geo/GeoShapeTool'
|
|
||||||
import { HighlightShapeTool } from '../editor/shapes/highlight/HighlightShapeTool'
|
|
||||||
import { LineShapeTool } from '../editor/shapes/line/LineShapeTool'
|
|
||||||
import { NoteShapeTool } from '../editor/shapes/note/NoteShapeTool'
|
|
||||||
import { TextShapeTool } from '../editor/shapes/text/TextShapeTool'
|
|
||||||
import { EraserTool } from '../editor/tools/EraserTool/EraserTool'
|
|
||||||
import { HandTool } from '../editor/tools/HandTool/HandTool'
|
|
||||||
import { LaserTool } from '../editor/tools/LaserTool/LaserTool'
|
|
||||||
import { TLStateNodeConstructor } from '../editor/tools/StateNode'
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const coreTools = [
|
|
||||||
// created by copy and paste
|
|
||||||
TextShapeTool,
|
|
||||||
]
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const defaultTools: TLStateNodeConstructor[] = [
|
|
||||||
HandTool,
|
|
||||||
EraserTool,
|
|
||||||
LaserTool,
|
|
||||||
DrawShapeTool,
|
|
||||||
GeoShapeTool,
|
|
||||||
LineShapeTool,
|
|
||||||
NoteShapeTool,
|
|
||||||
FrameShapeTool,
|
|
||||||
ArrowShapeTool,
|
|
||||||
HighlightShapeTool,
|
|
||||||
]
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { Migrations } from '@tldraw/store'
|
|
||||||
import { ShapeProps, TLBaseShape, TLUnknownShape } from '@tldraw/tlschema'
|
|
||||||
import { assert } from '@tldraw/utils'
|
|
||||||
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type TLShapeInfo<T extends TLUnknownShape = TLUnknownShape> = {
|
|
||||||
type: T['type']
|
|
||||||
util: TLShapeUtilConstructor<T>
|
|
||||||
props?: ShapeProps<T>
|
|
||||||
migrations?: Migrations
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AnyTLShapeInfo = TLShapeInfo<TLBaseShape<any, any>>
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export function defineShape<T extends TLUnknownShape>(
|
|
||||||
type: T['type'],
|
|
||||||
opts: Omit<TLShapeInfo<T>, 'type'>
|
|
||||||
): TLShapeInfo<T> {
|
|
||||||
assert(
|
|
||||||
type === opts.util.type,
|
|
||||||
`Shape type "${type}" does not match util type "${opts.util.type}"`
|
|
||||||
)
|
|
||||||
return { type, ...opts }
|
|
||||||
}
|
|
|
@ -1,21 +1,10 @@
|
||||||
import { EASINGS } from '@tldraw/primitives'
|
import { EASINGS } from './primitives/easings'
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const MAX_SHAPES_PER_PAGE = 2000
|
export const MAX_SHAPES_PER_PAGE = 2000
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const MAX_PAGES = 40
|
export const MAX_PAGES = 40
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export const REMOVE_SYMBOL = Symbol('remove')
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export const RICH_TYPES: Record<string, boolean> = {
|
|
||||||
Date: true,
|
|
||||||
RegExp: true,
|
|
||||||
String: true,
|
|
||||||
Number: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const ANIMATION_SHORT_MS = 80
|
export const ANIMATION_SHORT_MS = 80
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -44,17 +33,9 @@ export const MAJOR_NUDGE_FACTOR = 10
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const MINOR_NUDGE_FACTOR = 1
|
export const MINOR_NUDGE_FACTOR = 1
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export const MAX_ASSET_WIDTH = 1000
|
|
||||||
/** @internal */
|
|
||||||
export const MAX_ASSET_HEIGHT = 1000
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const GRID_INCREMENT = 5
|
export const GRID_INCREMENT = 5
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export const MIN_CROP_SIZE = 8
|
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const DOUBLE_CLICK_DURATION = 450
|
export const DOUBLE_CLICK_DURATION = 450
|
||||||
/** @internal */
|
/** @internal */
|
||||||
|
@ -84,7 +65,7 @@ export const DEFAULT_ANIMATION_OPTIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const HAND_TOOL_FRICTION = 0.09
|
export const CAMERA_SLIDE_FRICTION = 0.09
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const GRID_STEPS = [
|
export const GRID_STEPS = [
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,11 @@
|
||||||
import { Vec2d } from '@tldraw/primitives'
|
|
||||||
import {
|
import {
|
||||||
COARSE_DRAG_DISTANCE,
|
COARSE_DRAG_DISTANCE,
|
||||||
DOUBLE_CLICK_DURATION,
|
DOUBLE_CLICK_DURATION,
|
||||||
DRAG_DISTANCE,
|
DRAG_DISTANCE,
|
||||||
MULTI_CLICK_DURATION,
|
MULTI_CLICK_DURATION,
|
||||||
} from '../../constants'
|
} from '../../constants'
|
||||||
import { uniqueId } from '../../utils/data'
|
import { Vec2d } from '../../primitives/Vec2d'
|
||||||
|
import { uniqueId } from '../../utils/uniqueId'
|
||||||
import type { Editor } from '../Editor'
|
import type { Editor } from '../Editor'
|
||||||
import { TLClickEventInfo, TLPointerEventInfo } from '../types/event-types'
|
import { TLClickEventInfo, TLPointerEventInfo } from '../types/event-types'
|
||||||
|
|
||||||
|
|
|
@ -1,599 +0,0 @@
|
||||||
import { Vec2d, VecLike } from '@tldraw/primitives'
|
|
||||||
import {
|
|
||||||
AssetRecordType,
|
|
||||||
EmbedDefinition,
|
|
||||||
TLAsset,
|
|
||||||
TLAssetId,
|
|
||||||
TLEmbedShape,
|
|
||||||
TLShapePartial,
|
|
||||||
TLTextShape,
|
|
||||||
TLTextShapeProps,
|
|
||||||
createShapeId,
|
|
||||||
} from '@tldraw/tlschema'
|
|
||||||
import { compact, getHashForString } from '@tldraw/utils'
|
|
||||||
import { MAX_ASSET_HEIGHT, MAX_ASSET_WIDTH } from '../../constants'
|
|
||||||
import {
|
|
||||||
ACCEPTED_IMG_TYPE,
|
|
||||||
ACCEPTED_VID_TYPE,
|
|
||||||
containBoxSize,
|
|
||||||
getFileMetaData,
|
|
||||||
getImageSizeFromSrc,
|
|
||||||
getResizedImageDataUrl,
|
|
||||||
getVideoSizeFromSrc,
|
|
||||||
isImage,
|
|
||||||
} from '../../utils/assets'
|
|
||||||
import { truncateStringWithEllipsis } from '../../utils/dom'
|
|
||||||
import { getEmbedInfo } from '../../utils/embeds'
|
|
||||||
import { Editor } from '../Editor'
|
|
||||||
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shapes/shared/default-shape-constants'
|
|
||||||
import { INDENT } from '../shapes/text/TextHelpers'
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export type TLExternalContent =
|
|
||||||
| {
|
|
||||||
type: 'text'
|
|
||||||
point?: VecLike
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'files'
|
|
||||||
files: File[]
|
|
||||||
point?: VecLike
|
|
||||||
ignoreParent: boolean
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'url'
|
|
||||||
url: string
|
|
||||||
point?: VecLike
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'svg-text'
|
|
||||||
text: string
|
|
||||||
point?: VecLike
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'embed'
|
|
||||||
url: string
|
|
||||||
point?: VecLike
|
|
||||||
embed: EmbedDefinition
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export class ExternalContentManager {
|
|
||||||
constructor(public editor: Editor) {}
|
|
||||||
|
|
||||||
handleContent = async (info: TLExternalContent) => {
|
|
||||||
switch (info.type) {
|
|
||||||
case 'text': {
|
|
||||||
return await this.handleText(this.editor, info)
|
|
||||||
}
|
|
||||||
case 'files': {
|
|
||||||
return await this.handleFiles(this.editor, info)
|
|
||||||
}
|
|
||||||
case 'embed': {
|
|
||||||
return await this.handleEmbed(this.editor, info)
|
|
||||||
}
|
|
||||||
case 'svg-text': {
|
|
||||||
return await this.handleSvgText(this.editor, info)
|
|
||||||
}
|
|
||||||
case 'url': {
|
|
||||||
return await this.handleUrl(this.editor, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle svg text from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* editor.this.handleSvgText = myCustomMethod
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param editor - The editor instance.
|
|
||||||
* @param info - The info object describing the external content.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
async handleSvgText(
|
|
||||||
editor: Editor,
|
|
||||||
{ point, text }: Extract<TLExternalContent, { type: 'svg-text' }>
|
|
||||||
) {
|
|
||||||
const position =
|
|
||||||
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
|
|
||||||
|
|
||||||
const svg = new DOMParser().parseFromString(text, 'image/svg+xml').querySelector('svg')
|
|
||||||
if (!svg) {
|
|
||||||
throw new Error('No <svg/> element present')
|
|
||||||
}
|
|
||||||
|
|
||||||
let width = parseFloat(svg.getAttribute('width') || '0')
|
|
||||||
let height = parseFloat(svg.getAttribute('height') || '0')
|
|
||||||
|
|
||||||
if (!(width && height)) {
|
|
||||||
document.body.appendChild(svg)
|
|
||||||
const box = svg.getBoundingClientRect()
|
|
||||||
document.body.removeChild(svg)
|
|
||||||
|
|
||||||
width = box.width
|
|
||||||
height = box.height
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset = await this.createAssetFromFile(
|
|
||||||
editor,
|
|
||||||
new File([text], 'asset.svg', { type: 'image/svg+xml' })
|
|
||||||
)
|
|
||||||
|
|
||||||
this.createShapesForAssets(editor, [asset], position)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle embed info from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* editor.this.handleEmbed = myCustomMethod
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param editor - The editor instance
|
|
||||||
* @param info - The info object describing the external content.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
async handleEmbed(
|
|
||||||
editor: Editor,
|
|
||||||
{ point, url, embed }: Extract<TLExternalContent, { type: 'embed' }>
|
|
||||||
) {
|
|
||||||
const position =
|
|
||||||
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
|
|
||||||
|
|
||||||
const { width, height } = embed
|
|
||||||
|
|
||||||
const shapePartial: TLShapePartial<TLEmbedShape> = {
|
|
||||||
id: createShapeId(),
|
|
||||||
type: 'embed',
|
|
||||||
x: position.x - (width || 450) / 2,
|
|
||||||
y: position.y - (height || 450) / 2,
|
|
||||||
props: {
|
|
||||||
w: width,
|
|
||||||
h: height,
|
|
||||||
url,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.createShapes([shapePartial], true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle files from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* editor.this.handleFiles = myCustomMethod
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param editor - The editor instance
|
|
||||||
* @param info - The info object describing the external content.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
async handleFiles(
|
|
||||||
editor: Editor,
|
|
||||||
{ point, files }: Extract<TLExternalContent, { type: 'files' }>
|
|
||||||
) {
|
|
||||||
const position =
|
|
||||||
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
|
|
||||||
|
|
||||||
const pagePoint = new Vec2d(position.x, position.y)
|
|
||||||
|
|
||||||
const assets: TLAsset[] = []
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
files.map(async (file, i) => {
|
|
||||||
// Use mime type instead of file ext, this is because
|
|
||||||
// window.navigator.clipboard does not preserve file names
|
|
||||||
// of copied files.
|
|
||||||
if (!file.type) throw new Error('No mime type')
|
|
||||||
|
|
||||||
// We can only accept certain extensions (either images or a videos)
|
|
||||||
if (!ACCEPTED_IMG_TYPE.concat(ACCEPTED_VID_TYPE).includes(file.type)) {
|
|
||||||
console.warn(`${file.name} not loaded - Extension not allowed.`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const asset = await this.createAssetFromFile(editor, file)
|
|
||||||
|
|
||||||
if (!asset) throw Error('Could not create an asset')
|
|
||||||
|
|
||||||
assets[i] = asset
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
this.createShapesForAssets(editor, compact(assets), pagePoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle plain text from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* editor.this.handleText = myCustomMethod
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param editor - The editor instance
|
|
||||||
* @param info - The info object describing the external content.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
async handleText(editor: Editor, { point, text }: Extract<TLExternalContent, { type: 'text' }>) {
|
|
||||||
const p =
|
|
||||||
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
|
|
||||||
|
|
||||||
const defaultProps = editor.getShapeUtil<TLTextShape>('text').getDefaultProps()
|
|
||||||
|
|
||||||
const textToPaste = stripTrailingWhitespace(
|
|
||||||
stripCommonMinimumIndentation(replaceTabsWithSpaces(text))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Measure the text with default values
|
|
||||||
let w: number
|
|
||||||
let h: number
|
|
||||||
let autoSize: boolean
|
|
||||||
let align = 'middle' as TLTextShapeProps['align']
|
|
||||||
|
|
||||||
const isMultiLine = textToPaste.split('\n').length > 1
|
|
||||||
|
|
||||||
// check whether the text contains the most common characters in RTL languages
|
|
||||||
const isRtl = rtlRegex.test(textToPaste)
|
|
||||||
|
|
||||||
if (isMultiLine) {
|
|
||||||
align = isMultiLine ? (isRtl ? 'end' : 'start') : 'middle'
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawSize = editor.textMeasure.measureText(textToPaste, {
|
|
||||||
...TEXT_PROPS,
|
|
||||||
fontFamily: FONT_FAMILIES[defaultProps.font],
|
|
||||||
fontSize: FONT_SIZES[defaultProps.size],
|
|
||||||
width: 'fit-content',
|
|
||||||
})
|
|
||||||
|
|
||||||
const minWidth = Math.min(
|
|
||||||
isMultiLine ? editor.viewportPageBounds.width * 0.9 : 920,
|
|
||||||
Math.max(200, editor.viewportPageBounds.width * 0.9)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (rawSize.w > minWidth) {
|
|
||||||
const shrunkSize = editor.textMeasure.measureText(textToPaste, {
|
|
||||||
...TEXT_PROPS,
|
|
||||||
fontFamily: FONT_FAMILIES[defaultProps.font],
|
|
||||||
fontSize: FONT_SIZES[defaultProps.size],
|
|
||||||
width: minWidth + 'px',
|
|
||||||
})
|
|
||||||
w = shrunkSize.w
|
|
||||||
h = shrunkSize.h
|
|
||||||
autoSize = false
|
|
||||||
align = isRtl ? 'end' : 'start'
|
|
||||||
} else {
|
|
||||||
// autosize is fine
|
|
||||||
w = rawSize.w
|
|
||||||
h = rawSize.h
|
|
||||||
autoSize = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p.y - h / 2 < editor.viewportPageBounds.minY + 40) {
|
|
||||||
p.y = editor.viewportPageBounds.minY + 40 + h / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.createShapes<TLTextShape>([
|
|
||||||
{
|
|
||||||
id: createShapeId(),
|
|
||||||
type: 'text',
|
|
||||||
x: p.x - w / 2,
|
|
||||||
y: p.y - h / 2,
|
|
||||||
props: {
|
|
||||||
text: textToPaste,
|
|
||||||
// if the text has more than one line, align it to the left
|
|
||||||
align,
|
|
||||||
autoSize,
|
|
||||||
w,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle urls from an external source. Feeling lucky? Overwrite this at runtime to change the way this type of external content is handled.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* editor.this.handleUrl = myCustomMethod
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param editor - The editor instance
|
|
||||||
* @param info - The info object describing the external content.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
handleUrl = async (
|
|
||||||
editor: Editor,
|
|
||||||
{ point, url }: Extract<TLExternalContent, { type: 'url' }>
|
|
||||||
) => {
|
|
||||||
// try to paste as an embed first
|
|
||||||
const embedInfo = getEmbedInfo(url)
|
|
||||||
|
|
||||||
if (embedInfo) {
|
|
||||||
return this.handleEmbed(editor, {
|
|
||||||
type: 'embed',
|
|
||||||
url: embedInfo.url,
|
|
||||||
point,
|
|
||||||
embed: embedInfo.definition,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const position =
|
|
||||||
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
|
|
||||||
|
|
||||||
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
|
|
||||||
|
|
||||||
// Use an existing asset if we have one, or else else create a new one
|
|
||||||
let asset = editor.getAssetById(assetId) as TLAsset
|
|
||||||
let shouldAlsoCreateAsset = false
|
|
||||||
if (!asset) {
|
|
||||||
shouldAlsoCreateAsset = true
|
|
||||||
asset = await this.createAssetFromUrl(editor, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.batch(() => {
|
|
||||||
if (shouldAlsoCreateAsset) {
|
|
||||||
editor.createAssets([asset])
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createShapesForAssets(editor, [asset], position)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async createShapesForAssets(editor: Editor, assets: TLAsset[], position: VecLike) {
|
|
||||||
if (!assets.length) return
|
|
||||||
|
|
||||||
const currentPoint = Vec2d.From(position)
|
|
||||||
const paritals: TLShapePartial[] = []
|
|
||||||
|
|
||||||
for (const asset of assets) {
|
|
||||||
switch (asset.type) {
|
|
||||||
case 'bookmark': {
|
|
||||||
paritals.push({
|
|
||||||
id: createShapeId(),
|
|
||||||
type: 'bookmark',
|
|
||||||
x: currentPoint.x - 150,
|
|
||||||
y: currentPoint.y - 160,
|
|
||||||
opacity: 1,
|
|
||||||
props: {
|
|
||||||
assetId: asset.id,
|
|
||||||
url: asset.props.src,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
currentPoint.x += 300
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'image': {
|
|
||||||
paritals.push({
|
|
||||||
id: createShapeId(),
|
|
||||||
type: 'image',
|
|
||||||
x: currentPoint.x - asset.props.w / 2,
|
|
||||||
y: currentPoint.y - asset.props.h / 2,
|
|
||||||
opacity: 1,
|
|
||||||
props: {
|
|
||||||
assetId: asset.id,
|
|
||||||
w: asset.props.w,
|
|
||||||
h: asset.props.h,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
currentPoint.x += asset.props.w
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'video': {
|
|
||||||
paritals.push({
|
|
||||||
id: createShapeId(),
|
|
||||||
type: 'video',
|
|
||||||
x: currentPoint.x - asset.props.w / 2,
|
|
||||||
y: currentPoint.y - asset.props.h / 2,
|
|
||||||
opacity: 1,
|
|
||||||
props: {
|
|
||||||
assetId: asset.id,
|
|
||||||
w: asset.props.w,
|
|
||||||
h: asset.props.h,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
currentPoint.x += asset.props.w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.batch(() => {
|
|
||||||
// Create any assets
|
|
||||||
const assetsToCreate = assets.filter((asset) => !editor.getAssetById(asset.id))
|
|
||||||
if (assetsToCreate.length) {
|
|
||||||
editor.createAssets(assetsToCreate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the shapes
|
|
||||||
editor.createShapes(paritals, true)
|
|
||||||
|
|
||||||
// Re-position shapes so that the center of the group is at the provided point
|
|
||||||
const { viewportPageBounds } = editor
|
|
||||||
let { selectedPageBounds } = editor
|
|
||||||
|
|
||||||
if (selectedPageBounds) {
|
|
||||||
const offset = selectedPageBounds!.center.sub(position)
|
|
||||||
|
|
||||||
editor.updateShapes(
|
|
||||||
paritals.map((partial) => {
|
|
||||||
return {
|
|
||||||
id: partial.id,
|
|
||||||
type: partial.type,
|
|
||||||
x: partial.x! - offset.x,
|
|
||||||
y: partial.y! - offset.y,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zoom out to fit the shapes, if necessary
|
|
||||||
selectedPageBounds = editor.selectedPageBounds
|
|
||||||
if (selectedPageBounds && !viewportPageBounds.contains(selectedPageBounds)) {
|
|
||||||
editor.zoomToSelection()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override this method to change how assets are created from files.
|
|
||||||
*
|
|
||||||
* @param editor - The editor instance
|
|
||||||
* @param file - The file to create the asset from.
|
|
||||||
*/
|
|
||||||
async createAssetFromFile(_editor: Editor, file: File): Promise<TLAsset> {
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onerror = () => reject(reader.error)
|
|
||||||
reader.onload = async () => {
|
|
||||||
let dataUrl = reader.result as string
|
|
||||||
|
|
||||||
const isImageType = isImage(file.type)
|
|
||||||
const sizeFn = isImageType ? getImageSizeFromSrc : getVideoSizeFromSrc
|
|
||||||
|
|
||||||
// Hack to make .mov videos work via dataURL.
|
|
||||||
if (file.type === 'video/quicktime' && dataUrl.includes('video/quicktime')) {
|
|
||||||
dataUrl = dataUrl.replace('video/quicktime', 'video/mp4')
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalSize = await sizeFn(dataUrl)
|
|
||||||
const size = containBoxSize(originalSize, { w: MAX_ASSET_WIDTH, h: MAX_ASSET_HEIGHT })
|
|
||||||
|
|
||||||
if (size !== originalSize && (file.type === 'image/jpeg' || file.type === 'image/png')) {
|
|
||||||
// If we created a new size and the type is an image, rescale the image
|
|
||||||
dataUrl = await getResizedImageDataUrl(dataUrl, size.w, size.h)
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(dataUrl))
|
|
||||||
|
|
||||||
const metadata = await getFileMetaData(file)
|
|
||||||
|
|
||||||
const asset: Extract<TLAsset, { type: 'image' | 'video' }> = {
|
|
||||||
id: assetId,
|
|
||||||
type: isImageType ? 'image' : 'video',
|
|
||||||
typeName: 'asset',
|
|
||||||
props: {
|
|
||||||
name: file.name,
|
|
||||||
src: dataUrl,
|
|
||||||
w: size.w,
|
|
||||||
h: size.h,
|
|
||||||
mimeType: file.type,
|
|
||||||
isAnimated: metadata.isAnimated,
|
|
||||||
},
|
|
||||||
meta: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(asset)
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override me to change the way assets are created from urls.
|
|
||||||
*
|
|
||||||
* @param editor - The editor instance
|
|
||||||
* @param url - The url to create the asset from
|
|
||||||
*/
|
|
||||||
async createAssetFromUrl(_editor: Editor, url: string): Promise<TLAsset> {
|
|
||||||
let meta: { image: string; title: string; description: string }
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch(url, { method: 'GET', mode: 'no-cors' })
|
|
||||||
const html = await resp.text()
|
|
||||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
|
||||||
meta = {
|
|
||||||
image: doc.head.querySelector('meta[property="og:image"]')?.getAttribute('content') ?? '',
|
|
||||||
title:
|
|
||||||
doc.head.querySelector('meta[property="og:title"]')?.getAttribute('content') ??
|
|
||||||
truncateStringWithEllipsis(url, 32),
|
|
||||||
description:
|
|
||||||
doc.head.querySelector('meta[property="og:description"]')?.getAttribute('content') ?? '',
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
meta = { image: '', title: truncateStringWithEllipsis(url, 32), description: '' }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the bookmark asset from the meta
|
|
||||||
return {
|
|
||||||
id: AssetRecordType.createId(getHashForString(url)),
|
|
||||||
typeName: 'asset',
|
|
||||||
type: 'bookmark',
|
|
||||||
props: {
|
|
||||||
src: url,
|
|
||||||
description: meta.description,
|
|
||||||
image: meta.image,
|
|
||||||
title: meta.title,
|
|
||||||
},
|
|
||||||
meta: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------------------- Helpers -------------------- */
|
|
||||||
|
|
||||||
const rtlRegex = /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace any tabs with double spaces.
|
|
||||||
* @param text - The text to replace tabs in.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
function replaceTabsWithSpaces(text: string) {
|
|
||||||
return text.replace(/\t/g, INDENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip common minimum indentation from each line.
|
|
||||||
* @param text - The text to strip.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
function stripCommonMinimumIndentation(text: string): string {
|
|
||||||
// Split the text into individual lines
|
|
||||||
const lines = text.split('\n')
|
|
||||||
|
|
||||||
// remove any leading lines that are only whitespace or newlines
|
|
||||||
while (lines[0].trim().length === 0) {
|
|
||||||
lines.shift()
|
|
||||||
}
|
|
||||||
|
|
||||||
let minIndentation = Infinity
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim().length > 0) {
|
|
||||||
const indentation = line.length - line.trimStart().length
|
|
||||||
minIndentation = Math.min(minIndentation, indentation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.map((line) => line.slice(minIndentation)).join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip trailing whitespace from each line and remove any trailing newlines.
|
|
||||||
* @param text - The text to strip.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
function stripTrailingWhitespace(text: string): string {
|
|
||||||
return text.replace(/[ \t]+$/gm, '').replace(/\n+$/, '')
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { atom, transact } from '@tldraw/state'
|
import { atom, transact } from '@tldraw/state'
|
||||||
import { devFreeze } from '@tldraw/store'
|
import { devFreeze } from '@tldraw/store'
|
||||||
import { uniqueId } from '../../utils/data'
|
import { uniqueId } from '../../utils/uniqueId'
|
||||||
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types'
|
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types'
|
||||||
import { Stack, stack } from './Stack'
|
import { Stack, stack } from './Stack'
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
|
import { atom, computed, EMPTY_ARRAY } from '@tldraw/state'
|
||||||
|
import { TLGroupShape, TLParentId, TLShape, TLShapeId, Vec2dModel } from '@tldraw/tlschema'
|
||||||
|
import { dedupe, deepCopy } from '@tldraw/utils'
|
||||||
import {
|
import {
|
||||||
Box2d,
|
Box2d,
|
||||||
flipSelectionHandleX,
|
flipSelectionHandleX,
|
||||||
flipSelectionHandleY,
|
flipSelectionHandleY,
|
||||||
isSelectionCorner,
|
isSelectionCorner,
|
||||||
Matrix2d,
|
|
||||||
rangeIntersection,
|
|
||||||
rangesOverlap,
|
|
||||||
SelectionCorner,
|
SelectionCorner,
|
||||||
SelectionEdge,
|
SelectionEdge,
|
||||||
Vec2d,
|
} from '../../primitives/Box2d'
|
||||||
VecLike,
|
import { Matrix2d } from '../../primitives/Matrix2d'
|
||||||
} from '@tldraw/primitives'
|
import { rangeIntersection, rangesOverlap } from '../../primitives/utils'
|
||||||
import { atom, computed, EMPTY_ARRAY } from '@tldraw/state'
|
import { Vec2d, VecLike } from '../../primitives/Vec2d'
|
||||||
import { TLGroupShape, TLParentId, TLShape, TLShapeId, Vec2dModel } from '@tldraw/tlschema'
|
import { uniqueId } from '../../utils/uniqueId'
|
||||||
import { dedupe, deepCopy } from '@tldraw/utils'
|
|
||||||
import { uniqueId } from '../../utils/data'
|
|
||||||
import type { Editor } from '../Editor'
|
import type { Editor } from '../Editor'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export type PointsSnapLine = {
|
export type PointsSnapLine = {
|
||||||
id: string
|
id: string
|
||||||
type: 'points'
|
type: 'points'
|
||||||
points: VecLike[]
|
points: VecLike[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export type GapsSnapLine = {
|
export type GapsSnapLine = {
|
||||||
id: string
|
id: string
|
||||||
type: 'gaps'
|
type: 'gaps'
|
||||||
|
@ -31,6 +32,8 @@ export type GapsSnapLine = {
|
||||||
endEdge: [VecLike, VecLike]
|
endEdge: [VecLike, VecLike]
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export type SnapLine = PointsSnapLine | GapsSnapLine
|
export type SnapLine = PointsSnapLine | GapsSnapLine
|
||||||
|
|
||||||
export type SnapInteractionType =
|
export type SnapInteractionType =
|
||||||
|
@ -43,6 +46,7 @@ export type SnapInteractionType =
|
||||||
type: 'resize'
|
type: 'resize'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export interface SnapPoint {
|
export interface SnapPoint {
|
||||||
id: string
|
id: string
|
||||||
x: number
|
x: number
|
||||||
|
@ -208,6 +212,7 @@ function dedupeGapSnaps(snaps: Array<Extract<SnapLine, { type: 'gaps' }>>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public */
|
||||||
export class SnapManager {
|
export class SnapManager {
|
||||||
private _snapLines = atom<SnapLine[] | undefined>('snapLines', undefined)
|
private _snapLines = atom<SnapLine[] | undefined>('snapLines', undefined)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
import { Box2dModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
import { Box2dModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
||||||
import { uniqueId } from '../../utils/data'
|
import { uniqueId } from '../../utils/uniqueId'
|
||||||
import { Editor } from '../Editor'
|
import { Editor } from '../Editor'
|
||||||
import { TextHelpers } from '../shapes/text/TextHelpers'
|
|
||||||
|
const fixNewLines = /\r?\n|\r/g
|
||||||
|
|
||||||
|
function normalizeTextForDom(text: string) {
|
||||||
|
return text
|
||||||
|
.replace(fixNewLines, '\n')
|
||||||
|
.split('\n')
|
||||||
|
.map((x) => x || ' ')
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
const textAlignmentsForLtr = {
|
const textAlignmentsForLtr = {
|
||||||
start: 'left',
|
start: 'left',
|
||||||
|
@ -73,7 +82,7 @@ export class TextManager {
|
||||||
elm.style.setProperty('max-width', opts.maxWidth)
|
elm.style.setProperty('max-width', opts.maxWidth)
|
||||||
elm.style.setProperty('padding', opts.padding)
|
elm.style.setProperty('padding', opts.padding)
|
||||||
|
|
||||||
elm.textContent = TextHelpers.normalizeTextForDom(textToMeasure)
|
elm.textContent = normalizeTextForDom(textToMeasure)
|
||||||
|
|
||||||
const rect = elm.getBoundingClientRect()
|
const rect = elm.getBoundingClientRect()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Vec2d } from '@tldraw/primitives'
|
import { Vec2d } from '../../primitives/Vec2d'
|
||||||
import { Editor } from '../Editor'
|
import { Editor } from '../Editor'
|
||||||
|
|
||||||
export class TickManager {
|
export class TickManager {
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { Box2d, linesIntersect, pointInPolygon, Vec2d, VecLike } from '@tldraw/primitives'
|
|
||||||
import { TLBaseShape } from '@tldraw/tlschema'
|
import { TLBaseShape } from '@tldraw/tlschema'
|
||||||
|
import { Box2d } from '../../primitives/Box2d'
|
||||||
|
import { Vec2d, VecLike } from '../../primitives/Vec2d'
|
||||||
|
import { linesIntersect } from '../../primitives/intersect'
|
||||||
|
import { pointInPolygon } from '../../primitives/utils'
|
||||||
import { ShapeUtil, TLOnResizeHandler } from './ShapeUtil'
|
import { ShapeUtil, TLOnResizeHandler } from './ShapeUtil'
|
||||||
import { resizeBox } from './shared/resizeBox'
|
import { resizeBox } from './shared/resizeBox'
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
|
import { Migrations } from '@tldraw/store'
|
||||||
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
|
import { ShapeProps, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
|
||||||
|
import { Box2d } from '../../primitives/Box2d'
|
||||||
|
import { Vec2d, VecLike } from '../../primitives/Vec2d'
|
||||||
|
import { linesIntersect } from '../../primitives/intersect'
|
||||||
import type { Editor } from '../Editor'
|
import type { Editor } from '../Editor'
|
||||||
|
import { SvgExportContext } from '../types/SvgExportContext'
|
||||||
import { TLResizeHandle } from '../types/selection-types'
|
import { TLResizeHandle } from '../types/selection-types'
|
||||||
import { SvgExportContext } from './shared/SvgExportContext'
|
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export interface TLShapeUtilConstructor<
|
export interface TLShapeUtilConstructor<
|
||||||
T extends TLUnknownShape,
|
T extends TLUnknownShape,
|
||||||
U extends ShapeUtil<T> = ShapeUtil<T>
|
U extends ShapeUtil<T> = ShapeUtil<T>
|
||||||
> {
|
> {
|
||||||
new (editor: Editor, type: T['type'], styleProps: ReadonlyMap<StyleProp<unknown>, string>): U
|
new (editor: Editor): U
|
||||||
type: T['type']
|
type: T['type']
|
||||||
|
props?: ShapeProps<T>
|
||||||
|
migrations?: Migrations
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -25,27 +30,9 @@ export interface TLShapeUtilCanvasSvgDef {
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||||
constructor(
|
constructor(public editor: Editor) {}
|
||||||
public editor: Editor,
|
static props?: ShapeProps<TLUnknownShape>
|
||||||
public readonly type: Shape['type'],
|
static migrations?: Migrations
|
||||||
public readonly styleProps: ReadonlyMap<StyleProp<unknown>, string>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
setStyleInPartial<T>(
|
|
||||||
style: StyleProp<T>,
|
|
||||||
shape: TLShapePartial<Shape>,
|
|
||||||
value: T
|
|
||||||
): TLShapePartial<Shape> {
|
|
||||||
const styleKey = this.styleProps.get(style)
|
|
||||||
if (!styleKey) return shape
|
|
||||||
return {
|
|
||||||
...shape,
|
|
||||||
props: {
|
|
||||||
...shape.props,
|
|
||||||
[styleKey]: value,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the shape util, which should match the shape's type.
|
* The type of the shape util, which should match the shape's type.
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { arrowShapeMigrations, arrowShapeProps } from '@tldraw/tlschema'
|
|
||||||
import { defineShape } from '../../../config/defineShape'
|
|
||||||
import { ArrowShapeUtil } from './ArrowShapeUtil'
|
|
||||||
|
|
||||||
/** @public */
|
|
||||||
export const ArrowShape = defineShape('arrow', {
|
|
||||||
util: ArrowShapeUtil,
|
|
||||||
props: arrowShapeProps,
|
|
||||||
migrations: arrowShapeMigrations,
|
|
||||||
})
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue