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',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// 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
|
||||
- `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
|
||||
- `store`: an in-memory reactive database
|
||||
- `tldraw`: the main tldraw package containing both the editor and the UI
|
||||
- `tlschema`: shape definitions and migrations
|
||||
- `ui`: the editor's user interface
|
||||
- `utils`: low-level data utilities shared by other libraries
|
||||
- `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.waitForSelector('.tl-canvas')
|
||||
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 { assert } from '@tldraw/utils'
|
||||
import { rename, writeFile } from 'fs/promises'
|
||||
import { setupPage } from '../shared-e2e'
|
||||
|
||||
let page: Page
|
||||
declare const editor: Editor
|
||||
|
||||
test.describe('Export snapshots', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
})
|
||||
test.beforeEach(async () => {
|
||||
await setupPage(page)
|
||||
})
|
||||
|
||||
const snapshots = {} as Record<string, TLShapePartial[]>
|
||||
|
||||
for (const fill of ['none', 'semi', 'solid', 'pattern']) {
|
||||
|
@ -172,7 +163,9 @@ test.describe('Export 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) => {
|
||||
editor
|
||||
.updateInstanceState({ exportBackground: false })
|
||||
|
@ -188,17 +181,17 @@ test.describe('Export snapshots', () => {
|
|||
await page.click('[data-testid="menu-item.export-as-svg"]')
|
||||
|
||||
const download = await downloadEvent
|
||||
const path = await download.path()
|
||||
assert(path)
|
||||
const path = (await download.path()) as string
|
||||
// assert(path)
|
||||
await rename(path, path + '.svg')
|
||||
await writeFile(
|
||||
path + '.html',
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<img src="${path}.svg" />
|
||||
`,
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<img src="${path}.svg" />
|
||||
`,
|
||||
'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",
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@tldraw/assets": "workspace:*",
|
||||
"@tldraw/state": "workspace:*",
|
||||
"@tldraw/tldraw": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"@tldraw/validate": "workspace:*",
|
||||
"@vercel/analytics": "^1.0.1",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"react": "^18.2.0",
|
||||
|
|
|
@ -2,24 +2,30 @@ import { Tldraw, TLEditorComponents } from '@tldraw/tldraw'
|
|||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
const components: Partial<TLEditorComponents> = {
|
||||
Brush: ({ brush }) => (
|
||||
<rect
|
||||
className="tl-brush"
|
||||
stroke="red"
|
||||
fill="none"
|
||||
width={Math.max(1, brush.w)}
|
||||
height={Math.max(1, brush.h)}
|
||||
transform={`translate(${brush.x},${brush.y})`}
|
||||
/>
|
||||
),
|
||||
Brush: function MyBrush({ brush }) {
|
||||
return (
|
||||
<svg className="tl-overlays__item">
|
||||
<rect
|
||||
className="tl-brush"
|
||||
stroke="red"
|
||||
fill="none"
|
||||
width={Math.max(1, brush.w)}
|
||||
height={Math.max(1, brush.h)}
|
||||
transform={`translate(${brush.x},${brush.y})`}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
Scribble: ({ scribble, opacity, color }) => {
|
||||
return (
|
||||
<polyline
|
||||
points={scribble.points.map((p) => `${p.x},${p.y}`).join(' ')}
|
||||
stroke={color ?? 'black'}
|
||||
opacity={opacity ?? '1'}
|
||||
fill="none"
|
||||
/>
|
||||
<svg className="tl-overlays__item">
|
||||
<polyline
|
||||
points={scribble.points.map((p) => `${p.x},${p.y}`).join(' ')}
|
||||
stroke={color ?? 'black'}
|
||||
opacity={opacity ?? '1'}
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
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 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() {
|
||||
const rRaf = useRef<any>(-1)
|
||||
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 { throttle } from '@tldraw/utils'
|
||||
import { useLayoutEffect, useState } from 'react'
|
||||
|
||||
const PERSISTENCE_KEY = 'example-3'
|
||||
|
||||
export default function PersistenceExample() {
|
||||
const [store] = useState(() => createTLStore({ shapes: defaultShapes }))
|
||||
const [store] = useState(() => createTLStore({ shapeUtils: defaultShapeUtils }))
|
||||
const [loadingState, setLoadingState] = useState<
|
||||
{ status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string }
|
||||
>({
|
||||
|
|
|
@ -4,12 +4,11 @@ import {
|
|||
DefaultColorStyle,
|
||||
HTMLContainer,
|
||||
StyleProp,
|
||||
T,
|
||||
TLBaseShape,
|
||||
TLDefaultColorStyle,
|
||||
defineShape,
|
||||
getDefaultColorTheme,
|
||||
} from '@tldraw/tldraw'
|
||||
import { T } from '@tldraw/validate'
|
||||
|
||||
// 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.
|
||||
|
@ -33,6 +32,15 @@ export type CardShape = TLBaseShape<
|
|||
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
|
||||
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 canResize = (_shape: CardShape) => true
|
||||
override canBind = (_shape: CardShape) => true
|
||||
|
@ -87,17 +95,12 @@ export class CardShapeTool extends BaseBoxShapeTool {
|
|||
static override id = 'card'
|
||||
static override initial = 'idle'
|
||||
override shapeType = 'card'
|
||||
}
|
||||
|
||||
export const CardShape = defineShape('card', {
|
||||
util: CardShapeUtil,
|
||||
// to use a style prop, you need to describe all the props in your shape.
|
||||
props: {
|
||||
props = {
|
||||
w: T.number,
|
||||
h: T.number,
|
||||
// You can re-use tldraw built-in styles...
|
||||
color: DefaultColorStyle,
|
||||
// ...or your own custom styles.
|
||||
filter: MyFilterStyle,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { CardShape, CardShapeTool } from './CardShape'
|
||||
import { CardShapeTool, CardShapeUtil } from './CardShape'
|
||||
import { FilterStyleUi } from './FilterStyleUi'
|
||||
import { uiOverrides } from './ui-overrides'
|
||||
|
||||
const shapes = [CardShape]
|
||||
const customShapeUtils = [CardShapeUtil]
|
||||
const customTools = [CardShapeTool]
|
||||
|
||||
export default function CustomStylesExample() {
|
||||
return (
|
||||
|
@ -12,8 +13,8 @@ export default function CustomStylesExample() {
|
|||
<Tldraw
|
||||
autoFocus
|
||||
persistenceKey="custom-styles-example"
|
||||
shapes={shapes}
|
||||
tools={[CardShapeTool]}
|
||||
shapeUtils={customShapeUtils}
|
||||
tools={customTools}
|
||||
overrides={uiOverrides}
|
||||
>
|
||||
<FilterStyleUi />
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { track } from '@tldraw/state'
|
||||
import { useEditor } from '@tldraw/tldraw'
|
||||
import { track, useEditor } from '@tldraw/tldraw'
|
||||
import { MyFilterStyle } from './CardShape'
|
||||
|
||||
export const FilterStyleUi = track(function FilterStyleUi() {
|
||||
|
|
|
@ -9,7 +9,7 @@ export const uiOverrides: TLUiOverrides = {
|
|||
kbd: 'c',
|
||||
readonlyOk: false,
|
||||
onSelect: () => {
|
||||
editor.setSelectedTool('card')
|
||||
editor.setCurrentTool('card')
|
||||
},
|
||||
}
|
||||
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,
|
||||
} from '@tldraw/tldraw'
|
||||
import { useState } from 'react'
|
||||
import { cardShapeMigrations } from './card-shape-migrations'
|
||||
import { cardShapeProps } from './card-shape-props'
|
||||
import { ICardShape } from './card-shape-types'
|
||||
|
||||
// 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> {
|
||||
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
|
||||
override isAspectRatioLocked = (_shape: ICardShape) => false
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { DefaultColorStyle, ShapeProps, StyleProp } from '@tldraw/tldraw'
|
||||
import { T } from '@tldraw/validate'
|
||||
import { DefaultColorStyle, ShapeProps, StyleProp, T } from '@tldraw/tldraw'
|
||||
import { ICardShape } from './card-shape-types'
|
||||
|
||||
export const WeightStyle = StyleProp.defineEnum('myApp:weight', {
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { CardShapeTool } from './CardShape/CardShapeTool'
|
||||
import { customShapes } from './custom-shapes'
|
||||
import { CardShapeUtil } from './CardShape/CardShapeUtil'
|
||||
import { uiOverrides } from './ui-overrides'
|
||||
|
||||
const customShapeUtils = [CardShapeUtil]
|
||||
const customTools = [CardShapeTool]
|
||||
|
||||
export default function CustomConfigExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
autoFocus
|
||||
// Pass in the array of custom shape definitions
|
||||
shapes={customShapes}
|
||||
// Pass in the array of custom tools
|
||||
tools={[CardShapeTool]}
|
||||
// Pass in the array of custom shape classes
|
||||
shapeUtils={customShapeUtils}
|
||||
// Pass in the array of custom tool classes
|
||||
tools={customTools}
|
||||
// Pass in any overrides to the user interface
|
||||
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',
|
||||
readonlyOk: false,
|
||||
onSelect: () => {
|
||||
editor.setSelectedTool('card')
|
||||
editor.setCurrentTool('card')
|
||||
},
|
||||
}
|
||||
return tools
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { track } from '@tldraw/state'
|
||||
import { Canvas, TldrawEditor, defaultShapes, defaultTools, useEditor } from '@tldraw/tldraw'
|
||||
import { Canvas, Tldraw, track, useEditor } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { useEffect } from 'react'
|
||||
import './custom-ui.css'
|
||||
|
@ -7,10 +6,10 @@ import './custom-ui.css'
|
|||
export default function CustomUiExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<TldrawEditor shapes={defaultShapes} tools={defaultTools} autoFocus>
|
||||
<Tldraw hideUi autoFocus>
|
||||
<Canvas />
|
||||
<CustomUi />
|
||||
</TldrawEditor>
|
||||
</Tldraw>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -24,6 +23,22 @@ const CustomUi = track(() => {
|
|||
case 'Delete':
|
||||
case 'Backspace': {
|
||||
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
|
||||
className="custom-button"
|
||||
data-isactive={editor.currentToolId === 'select'}
|
||||
onClick={() => editor.setSelectedTool('select')}
|
||||
onClick={() => editor.setCurrentTool('select')}
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
<button
|
||||
className="custom-button"
|
||||
data-isactive={editor.currentToolId === 'draw'}
|
||||
onClick={() => editor.setSelectedTool('draw')}
|
||||
onClick={() => editor.setCurrentTool('draw')}
|
||||
>
|
||||
Pencil
|
||||
</button>
|
||||
<button
|
||||
className="custom-button"
|
||||
data-isactive={editor.currentToolId === 'eraser'}
|
||||
onClick={() => editor.setSelectedTool('eraser')}
|
||||
onClick={() => editor.setCurrentTool('eraser')}
|
||||
>
|
||||
Eraser
|
||||
</button>
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
ContextMenu,
|
||||
TldrawEditor,
|
||||
TldrawUi,
|
||||
defaultShapes,
|
||||
defaultShapeUtils,
|
||||
defaultTools,
|
||||
} from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
@ -12,7 +12,8 @@ export default function ExplodedExample() {
|
|||
return (
|
||||
<div className="tldraw__editor">
|
||||
<TldrawEditor
|
||||
shapes={defaultShapes}
|
||||
initialState="select"
|
||||
shapeUtils={defaultShapeUtils}
|
||||
tools={defaultTools}
|
||||
autoFocus
|
||||
persistenceKey="exploded-example"
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { createShapeId, Tldraw, TLShapePartial } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { ErrorShape } from './ErrorShape'
|
||||
import { ErrorShape, ErrorShapeUtil } from './ErrorShape'
|
||||
|
||||
const shapes = [ErrorShape]
|
||||
const shapes = [ErrorShapeUtil]
|
||||
|
||||
export default function ErrorBoundaryExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
shapes={shapes}
|
||||
shapeUtils={shapes}
|
||||
tools={[]}
|
||||
components={{
|
||||
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 class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
|
||||
static override type = 'error' as const
|
||||
override type = 'error' as const
|
||||
|
||||
getDefaultProps() {
|
||||
return { message: 'Error!', w: 100, h: 100 }
|
||||
|
@ -16,5 +15,3 @@ export class ErrorShapeUtil extends BaseBoxShapeUtil<ErrorShape> {
|
|||
throw new Error(`Error shape indicator!`)
|
||||
}
|
||||
}
|
||||
|
||||
export const ErrorShape = defineShape('error', { util: ErrorShapeUtil })
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { getAssetUrlsByMetaUrl } from '@tldraw/assets/urls'
|
||||
import {
|
||||
DefaultErrorFallback,
|
||||
ErrorBoundary,
|
||||
setDefaultEditorAssetUrls,
|
||||
setDefaultUiAssetUrls,
|
||||
} from '@tldraw/tldraw'
|
||||
import { DefaultErrorFallback, ErrorBoundary, setDefaultUiAssetUrls } from '@tldraw/tldraw'
|
||||
import { setDefaultEditorAssetUrls } from '@tldraw/tldraw/src/lib/utils/assetUrls'
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
||||
|
@ -28,6 +24,7 @@ import HideUiExample from './9-hide-ui/HideUiExample'
|
|||
import ExamplesTldrawLogo from './ExamplesTldrawLogo'
|
||||
import { ListLink } from './components/ListLink'
|
||||
import EndToEnd from './end-to-end/end-to-end'
|
||||
import OnlyEditorExample from './only-editor/OnlyEditor'
|
||||
import YjsExample from './yjs/YjsExample'
|
||||
|
||||
// This example is only used for end to end tests
|
||||
|
@ -50,6 +47,11 @@ export const allExamples: Example[] = [
|
|||
path: '/develop',
|
||||
element: <ExampleBasic />,
|
||||
},
|
||||
{
|
||||
title: 'Collaboration (with Yjs)',
|
||||
path: '/yjs',
|
||||
element: <YjsExample />,
|
||||
},
|
||||
{
|
||||
title: 'Editor API',
|
||||
path: '/api',
|
||||
|
@ -120,11 +122,6 @@ export const allExamples: Example[] = [
|
|||
path: '/persistence',
|
||||
element: <PersistenceExample />,
|
||||
},
|
||||
{
|
||||
title: 'Custom styles',
|
||||
path: '/yjs',
|
||||
element: <YjsExample />,
|
||||
},
|
||||
{
|
||||
title: 'Custom styles',
|
||||
path: '/custom-styles',
|
||||
|
@ -135,6 +132,11 @@ export const allExamples: Example[] = [
|
|||
path: '/shape-meta',
|
||||
element: <ShapeMetaExample />,
|
||||
},
|
||||
{
|
||||
title: 'Only editor',
|
||||
path: '/only-editor',
|
||||
element: <OnlyEditorExample />,
|
||||
},
|
||||
// not listed
|
||||
{
|
||||
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, useEditor } from '@tldraw/tldraw'
|
||||
import { Tldraw, track, useEditor } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { useYjsStore } from './useYjsStore'
|
||||
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { computed, react, transact } from '@tldraw/state'
|
||||
import {
|
||||
DocumentRecordType,
|
||||
InstancePresenceRecordType,
|
||||
PageRecordType,
|
||||
TLAnyShapeUtilConstructor,
|
||||
TLDocument,
|
||||
TLInstancePresence,
|
||||
TLPageId,
|
||||
TLRecord,
|
||||
TLShapeInfo,
|
||||
TLStoreWithStatus,
|
||||
computed,
|
||||
createPresenceStateDerivation,
|
||||
createTLStore,
|
||||
defaultShapes,
|
||||
defaultShapeUtils,
|
||||
getUserPreferences,
|
||||
react,
|
||||
} from '@tldraw/tldraw'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
|
@ -21,14 +22,16 @@ import * as Y from 'yjs'
|
|||
export function useYjsStore({
|
||||
roomId = 'example',
|
||||
hostUrl = process.env.NODE_ENV === 'development' ? 'ws://localhost:1234' : 'wss://demos.yjs.dev',
|
||||
shapes = [],
|
||||
shapeUtils = [],
|
||||
}: Partial<{
|
||||
hostUrl: string
|
||||
roomId: string
|
||||
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 { doc, room, yRecords } = useMemo(() => {
|
||||
|
@ -75,7 +78,7 @@ export function useYjsStore({
|
|||
// is empty, initialize the yjs doc with the default store records.
|
||||
if (yRecords.size === 0) {
|
||||
// Create the initial store records
|
||||
transact(() => {
|
||||
Y.transact(doc, () => {
|
||||
store.clear()
|
||||
store.put([
|
||||
DocumentRecordType.create({
|
||||
|
@ -97,7 +100,7 @@ export function useYjsStore({
|
|||
})
|
||||
} else {
|
||||
// Replace the store records with the yjs doc records
|
||||
transact(() => {
|
||||
Y.transact(doc, () => {
|
||||
store.clear()
|
||||
store.put([...yRecords.values()])
|
||||
})
|
||||
|
|
|
@ -34,11 +34,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tldraw/assets": "workspace:*",
|
||||
"@tldraw/editor": "workspace:*",
|
||||
"@tldraw/file-format": "workspace:*",
|
||||
"@tldraw/tldraw": "workspace:*",
|
||||
"@tldraw/ui": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^17.0.14",
|
||||
"@types/react": "^18.0.24",
|
||||
|
@ -50,7 +46,6 @@
|
|||
"esbuild": "^0.18.3",
|
||||
"fs-extra": "^11.1.0",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"nanoid": "4.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tslib": "^2.4.0"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import esbuild from 'esbuild'
|
||||
import fs from 'fs'
|
||||
import fse from 'fs-extra'
|
||||
import fse, { exists } from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { logEnv } from '../../vscode-script-utils/cli'
|
||||
import { exists, getDirname } from '../../vscode-script-utils/path'
|
||||
import { logEnv } from './cli'
|
||||
import { getDirname } from './path'
|
||||
|
||||
const rootDir = getDirname(import.meta.url, '../')
|
||||
const log = logEnv('editor')
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import dotenv from 'dotenv'
|
||||
import esbuild from 'esbuild'
|
||||
import fs from 'fs'
|
||||
import fse from 'fs-extra'
|
||||
import fse, { exists } from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { logEnv } from '../../vscode-script-utils/cli'
|
||||
import { copyEditor } from '../../vscode-script-utils/helpers'
|
||||
import { exists, getDirname } from '../../vscode-script-utils/path'
|
||||
import { logEnv } from './cli'
|
||||
import { copyEditor } from './helpers'
|
||||
import { getDirname } from './path'
|
||||
|
||||
dotenv.config()
|
||||
const rootDir = getDirname(import.meta.url, '../')
|
||||
|
|
|
@ -3,7 +3,7 @@ import fse from 'fs-extra'
|
|||
import { join } 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 }) {
|
||||
const editorRoot = join(vscodeDir, 'editor')
|
|
@ -1,13 +1,15 @@
|
|||
import { useEditor } from '@tldraw/editor'
|
||||
import { parseAndLoadDocument, serializeTldrawJson } from '@tldraw/file-format'
|
||||
import { useDefaultHelpers } from '@tldraw/ui'
|
||||
import { debounce } from '@tldraw/utils'
|
||||
import {
|
||||
debounce,
|
||||
parseAndLoadDocument,
|
||||
serializeTldrawJson,
|
||||
useDefaultHelpers,
|
||||
useEditor,
|
||||
} from '@tldraw/tldraw'
|
||||
import React from 'react'
|
||||
import '../public/index.css'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
// @ts-ignore
|
||||
import type { VscodeMessage } from '../../messages'
|
||||
import '../public/index.css'
|
||||
import { vscode } from './utils/vscode'
|
||||
|
||||
export const ChangeResponder = () => {
|
||||
const editor = useEditor()
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { useEditor } from '@tldraw/editor'
|
||||
import { parseAndLoadDocument } from '@tldraw/file-format'
|
||||
import { useDefaultHelpers } from '@tldraw/ui'
|
||||
import { parseAndLoadDocument, useDefaultHelpers, useEditor } from '@tldraw/tldraw'
|
||||
import React from 'react'
|
||||
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'
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
import '@tldraw/editor/editor.css'
|
||||
import { ContextMenu, TLUiMenuSchema, TldrawUi } from '@tldraw/ui'
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
import '@tldraw/ui/ui.css'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
// eslint-disable-next-line import/no-internal-modules
|
||||
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 { VscodeMessage } from '../../messages'
|
||||
import '../public/index.css'
|
||||
|
@ -128,26 +125,22 @@ function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerPro
|
|||
const assetUrls = useMemo(() => getAssetUrlsByImport({ baseUrl: assetSrc }), [assetSrc])
|
||||
|
||||
const handleMount = useCallback((editor: Editor) => {
|
||||
editor.externalContentManager.createAssetFromUrl = onCreateAssetFromUrl
|
||||
editor.registerExternalAssetHandler('url', onCreateAssetFromUrl)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<TldrawEditor
|
||||
shapes={defaultShapes}
|
||||
tools={defaultTools}
|
||||
<Tldraw
|
||||
shapeUtils={defaultShapeUtils}
|
||||
tools={defaultShapeTools}
|
||||
assetUrls={assetUrls}
|
||||
persistenceKey={uri}
|
||||
onMount={handleMount}
|
||||
overrides={[menuOverrides, linksUiOverrides]}
|
||||
autoFocus
|
||||
>
|
||||
{/* <DarkModeHandler themeKind={themeKind} /> */}
|
||||
<TldrawUi assetUrls={assetUrls} overrides={[menuOverrides, linksUiOverrides]}>
|
||||
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
|
||||
<ChangeResponder />
|
||||
<ContextMenu>
|
||||
<Canvas />
|
||||
</ContextMenu>
|
||||
</TldrawUi>
|
||||
</TldrawEditor>
|
||||
<FileOpen fileContents={fileContents} forceDarkMode={isDarkMode} />
|
||||
<ChangeResponder />
|
||||
</Tldraw>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { AssetRecordType, Editor, TLAsset, truncateStringWithEllipsis } from '@tldraw/editor'
|
||||
import { getHashForString } from '@tldraw/utils'
|
||||
import { AssetRecordType, TLAsset, TLExternalAssetContent, getHashForString } from '@tldraw/tldraw'
|
||||
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 {
|
||||
// First, try to get the data from vscode
|
||||
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'
|
||||
|
||||
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 { vscode } from './vscode'
|
||||
|
||||
|
@ -26,7 +26,7 @@ export function rpc(
|
|||
type ErrorType = VscodeMessagePairs[typeof id]['error']
|
||||
|
||||
const type = (id + '/request') as RequestType['type']
|
||||
const uuid = nanoid()
|
||||
const uuid = uniqueId()
|
||||
return new Promise<ResponseType['data']>((resolve, reject) => {
|
||||
const inMessage = {
|
||||
uuid,
|
||||
|
|
|
@ -22,11 +22,6 @@
|
|||
"skipDefaultLibCheck": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src", "../messages", "scripts", "../vscode-script-utils"],
|
||||
"references": [
|
||||
{ "path": "../../../packages/file-format" },
|
||||
{ "path": "../../../packages/ui" },
|
||||
{ "path": "../../../packages/editor" },
|
||||
{ "path": "../../../packages/utils" }
|
||||
]
|
||||
"include": ["src", "../messages", "scripts"],
|
||||
"references": [{ "path": "../../../packages/tldraw" }]
|
||||
}
|
||||
|
|
|
@ -131,8 +131,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tldraw/editor": "workspace:*",
|
||||
"@tldraw/file-format": "workspace:*",
|
||||
"@tldraw/store": "workspace:*",
|
||||
"@tldraw/tldraw": "workspace:*",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/vscode": "^1.75.1",
|
||||
|
@ -152,7 +151,6 @@
|
|||
},
|
||||
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296",
|
||||
"dependencies": {
|
||||
"nanoid": "4.0.2",
|
||||
"node-fetch": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import esbuild from 'esbuild'
|
||||
import { logEnv } from '../../vscode-script-utils/cli'
|
||||
import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers'
|
||||
import { logEnv } from './cli'
|
||||
import { copyEditor, removeDistDirectory } from './helpers'
|
||||
|
||||
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 { join } from 'path'
|
||||
import { logEnv } from '../../vscode-script-utils/cli'
|
||||
import { copyEditor, removeDistDirectory } from '../../vscode-script-utils/helpers'
|
||||
import { getDirname } from '../../vscode-script-utils/path'
|
||||
import { logEnv } from './cli'
|
||||
import { copyEditor, removeDistDirectory } from './helpers'
|
||||
import { getDirname } from './path'
|
||||
|
||||
const rootDir = getDirname(import.meta.url, '../')
|
||||
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 { defaultFileContents, fileExists, loadFile } from './file'
|
||||
import { nicelog } from './utils'
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { nanoid } from 'nanoid'
|
||||
import { uniqueId } from '@tldraw/tldraw'
|
||||
import * as vscode from 'vscode'
|
||||
import { TLDrawDocument } from './TldrawDocument'
|
||||
import { GlobalStateKeys, WebViewMessageHandler } from './WebViewMessageHandler'
|
||||
// @ts-ignore
|
||||
|
||||
export class TldrawWebviewManager {
|
||||
private disposables: vscode.Disposable[] = []
|
||||
|
@ -15,7 +14,7 @@ export class TldrawWebviewManager {
|
|||
) {
|
||||
let userId = context.globalState.get(GlobalStateKeys.UserId)
|
||||
if (!userId) {
|
||||
userId = 'user:' + nanoid()
|
||||
userId = 'user:' + uniqueId()
|
||||
context.globalState.update(GlobalStateKeys.UserId, userId)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { UnknownRecord } from '@tldraw/store'
|
||||
import { isEqual } from 'lodash'
|
||||
import fetch from 'node-fetch'
|
||||
import * as vscode from 'vscode'
|
||||
import { TLDrawDocument } from './TldrawDocument'
|
||||
import { loadFile } from './file'
|
||||
|
||||
import { UnknownRecord } from '@tldraw/tldraw'
|
||||
// @ts-ignore
|
||||
import type { VscodeMessage } from '../../messages'
|
||||
import { nicelog } from './utils'
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { createTLStore, defaultShapes } from '@tldraw/editor'
|
||||
import { TldrawFile } from '@tldraw/file-format'
|
||||
import { TldrawFile, createTLStore, defaultShapeUtils } from '@tldraw/tldraw'
|
||||
import * as vscode from 'vscode'
|
||||
import { nicelog } from './utils'
|
||||
|
||||
export const defaultFileContents: TldrawFile = {
|
||||
tldrawFileFormatVersion: 1,
|
||||
schema: createTLStore({ shapes: defaultShapes }).schema.serialize(),
|
||||
schema: createTLStore({ shapeUtils: defaultShapeUtils }).schema.serialize(),
|
||||
records: [],
|
||||
}
|
||||
|
||||
export const fileContentWithErrors: TldrawFile = {
|
||||
tldrawFileFormatVersion: 1,
|
||||
schema: createTLStore({ shapes: defaultShapes }).schema.serialize(),
|
||||
schema: createTLStore({ shapeUtils: defaultShapeUtils }).schema.serialize(),
|
||||
records: [{ typeName: 'shape', id: null } as any],
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,6 @@
|
|||
"skipDefaultLibCheck": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src", "../messages", "scripts", "../vscode-script-utils"],
|
||||
"references": [{ "path": "../../../packages/file-format" }]
|
||||
"include": ["src", "../messages", "scripts"],
|
||||
"references": [{ "path": "../../../packages/tldraw" }]
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ const MyCustomShapes = [MyCardShape]
|
|||
export default function () {
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<Tldraw shapes={MyCustomShapes}/>
|
||||
<Tldraw shapeUtils={MyCustomShapes}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ The [`defineShape`](/gen/editor/defineShape) function can also be used to includ
|
|||
export default function () {
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<Tldraw shapes={MyCustomShapes} onMount={editor => {
|
||||
<Tldraw shapeUtils={MyCustomShapes} onMount={editor => {
|
||||
editor.createShapes([{ type: "card" }])
|
||||
}}/>
|
||||
</div>
|
||||
|
|
|
@ -94,5 +94,8 @@
|
|||
},
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"svgo": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -245,14 +245,11 @@ input,
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tlui-following {
|
||||
display: block;
|
||||
.tl-positioned {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
z-index: 9999999;
|
||||
pointer-events: none;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
/* ------------------- Background ------------------- */
|
||||
|
|
|
@ -45,24 +45,21 @@
|
|||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tldraw/indices": "workspace:*",
|
||||
"@tldraw/primitives": "workspace:*",
|
||||
"@tldraw/state": "workspace:*",
|
||||
"@tldraw/store": "workspace:*",
|
||||
"@tldraw/tlschema": "workspace:*",
|
||||
"@tldraw/utils": "workspace:*",
|
||||
"@tldraw/validate": "workspace:*",
|
||||
"@types/canvas-size": "^1.2.0",
|
||||
"@types/core-js": "^2.5.5",
|
||||
"@use-gesture/react": "^10.2.27",
|
||||
"canvas-size": "^1.2.6",
|
||||
"classnames": "^2.3.2",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"core-js": "^3.31.1",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"idb": "^7.1.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash.uniq": "^4.5.0",
|
||||
"nanoid": "4.0.2"
|
||||
"nanoid": "^3.3.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18",
|
||||
|
@ -95,7 +92,7 @@
|
|||
"^.+\\.*.css$"
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(nanoid|escape-string-regexp)/)"
|
||||
"node_modules/(?!(nanoid)/)"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^~(.*)": "<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
|
||||
// incorrect output. https://github.com/evanw/esbuild/issues/1737
|
||||
|
||||
// eslint-disable-next-line local/no-export-star
|
||||
export * from '@tldraw/indices'
|
||||
export {
|
||||
EMPTY_ARRAY,
|
||||
atom,
|
||||
computed,
|
||||
react,
|
||||
track,
|
||||
transact,
|
||||
transaction,
|
||||
useComputed,
|
||||
useQuickReactor,
|
||||
useReactor,
|
||||
useValue,
|
||||
whyAmIRunning,
|
||||
type Atom,
|
||||
type Signal,
|
||||
} 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
|
||||
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 {
|
||||
ErrorScreen,
|
||||
LoadingScreen,
|
||||
|
@ -26,20 +33,16 @@ export {
|
|||
type TldrawEditorBaseProps,
|
||||
type TldrawEditorProps,
|
||||
} from './lib/TldrawEditor'
|
||||
export {
|
||||
defaultEditorAssetUrls,
|
||||
setDefaultEditorAssetUrls,
|
||||
type TLEditorAssetUrls,
|
||||
} from './lib/assetUrls'
|
||||
export { Canvas } from './lib/components/Canvas'
|
||||
export { DefaultErrorFallback } from './lib/components/DefaultErrorFallback'
|
||||
export {
|
||||
ErrorBoundary,
|
||||
OptionalErrorBoundary,
|
||||
type TLErrorBoundaryProps,
|
||||
} from './lib/components/ErrorBoundary'
|
||||
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
|
||||
export { PositionedOnCanvas } from './lib/components/PositionedOnCanvas'
|
||||
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
|
||||
export { DefaultErrorFallback } from './lib/components/default-components/DefaultErrorFallback'
|
||||
export {
|
||||
TAB_ID,
|
||||
createSessionStateSnapshotSignal,
|
||||
|
@ -60,39 +63,35 @@ export {
|
|||
type TLStoreOptions,
|
||||
} from './lib/config/createTLStore'
|
||||
export { createTLUser } from './lib/config/createTLUser'
|
||||
export { coreShapes, defaultShapes } from './lib/config/defaultShapes'
|
||||
export { defaultTools } from './lib/config/defaultTools'
|
||||
export { defineShape, type TLShapeInfo } from './lib/config/defineShape'
|
||||
export { coreShapes, type TLAnyShapeUtilConstructor } from './lib/config/defaultShapes'
|
||||
export {
|
||||
ANIMATION_MEDIUM_MS,
|
||||
ANIMATION_SHORT_MS,
|
||||
CAMERA_SLIDE_FRICTION,
|
||||
DEFAULT_ANIMATION_OPTIONS,
|
||||
DOUBLE_CLICK_DURATION,
|
||||
DRAG_DISTANCE,
|
||||
GRID_INCREMENT,
|
||||
GRID_STEPS,
|
||||
HAND_TOOL_FRICTION,
|
||||
HASH_PATTERN_ZOOM_NAMES,
|
||||
MAJOR_NUDGE_FACTOR,
|
||||
MAX_ASSET_HEIGHT,
|
||||
MAX_ASSET_WIDTH,
|
||||
MAX_PAGES,
|
||||
MAX_SHAPES_PER_PAGE,
|
||||
MAX_ZOOM,
|
||||
MINOR_NUDGE_FACTOR,
|
||||
MIN_ZOOM,
|
||||
MULTI_CLICK_DURATION,
|
||||
REMOVE_SYMBOL,
|
||||
RICH_TYPES,
|
||||
SVG_PADDING,
|
||||
ZOOMS,
|
||||
} from './lib/constants'
|
||||
export { Editor, type TLAnimationOptions, type TLEditorOptions } from './lib/editor/Editor'
|
||||
export {
|
||||
ExternalContentManager as PlopManager,
|
||||
type TLExternalContent,
|
||||
} from './lib/editor/managers/ExternalContentManager'
|
||||
export { ScribbleManager } from './lib/editor/managers/ScribbleManager'
|
||||
SnapManager,
|
||||
type GapsSnapLine,
|
||||
type PointsSnapLine,
|
||||
type SnapLine,
|
||||
type SnapPoint,
|
||||
} from './lib/editor/managers/SnapManager'
|
||||
export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseBoxShapeUtil'
|
||||
export {
|
||||
ShapeUtil,
|
||||
|
@ -117,38 +116,25 @@ export {
|
|||
type TLOnTranslateStartHandler,
|
||||
type TLResizeInfo,
|
||||
type TLResizeMode,
|
||||
type TLShapeUtilCanvasSvgDef,
|
||||
type TLShapeUtilConstructor,
|
||||
type TLShapeUtilFlag,
|
||||
} 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 { HighlightShape } from './lib/editor/shapes/highlight/HighlightShape'
|
||||
export { HighlightShapeUtil } from './lib/editor/shapes/highlight/HighlightShapeUtil'
|
||||
export { ImageShape } from './lib/editor/shapes/image/ImageShape'
|
||||
export { ImageShapeUtil } from './lib/editor/shapes/image/ImageShapeUtil'
|
||||
export { LineShape } from './lib/editor/shapes/line/LineShape'
|
||||
export { LineShapeUtil, getSplineForLineShape } from './lib/editor/shapes/line/LineShapeUtil'
|
||||
export { NoteShape } from './lib/editor/shapes/note/NoteShape'
|
||||
export { NoteShapeUtil } from './lib/editor/shapes/note/NoteShapeUtil'
|
||||
export { getArrowheadPathForType } from './lib/editor/shapes/shared/arrow/arrowheads'
|
||||
export {
|
||||
getCurvedArrowHandlePath,
|
||||
getSolidCurvedArrowPath,
|
||||
} from './lib/editor/shapes/shared/arrow/curved-arrow'
|
||||
export { getArrowTerminalsInArrowSpace } from './lib/editor/shapes/shared/arrow/shared'
|
||||
export {
|
||||
getSolidStraightArrowPath,
|
||||
getStraightArrowHandlePath,
|
||||
} from './lib/editor/shapes/shared/arrow/straight-arrow'
|
||||
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 { 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 TLEventMap, type TLEventMapHandler } from './lib/editor/types/emit-types'
|
||||
export {
|
||||
|
@ -184,6 +170,10 @@ export {
|
|||
type UiEvent,
|
||||
type UiEventType,
|
||||
} from './lib/editor/types/event-types'
|
||||
export {
|
||||
type TLExternalAssetContent,
|
||||
type TLExternalContent,
|
||||
} from './lib/editor/types/external-content'
|
||||
export {
|
||||
type TLCommand,
|
||||
type TLCommandHandler,
|
||||
|
@ -192,83 +182,126 @@ export {
|
|||
} from './lib/editor/types/history-types'
|
||||
export { type RequiredKeys } from './lib/editor/types/misc-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 { getCursor } from './lib/hooks/useCursor'
|
||||
export { useEditor } from './lib/hooks/useEditor'
|
||||
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 { usePeerIds } from './lib/hooks/usePeerIds'
|
||||
export { usePresence } from './lib/hooks/usePresence'
|
||||
export { useSelectionEvents } from './lib/hooks/useSelectionEvents'
|
||||
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 {
|
||||
ReadonlySharedStyleMap,
|
||||
SharedStyleMap,
|
||||
type SharedStyle,
|
||||
} from './lib/utils/SharedStylesMap'
|
||||
export { WeakMapCache } from './lib/utils/WeakMapCache'
|
||||
export {
|
||||
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 { dataUrlToFile } from './lib/utils/assets'
|
||||
export { debugFlags, featureFlags, type DebugFlag } from './lib/utils/debug-flags'
|
||||
export {
|
||||
getRotatedBoxShadow,
|
||||
loopToHtmlElement,
|
||||
preventDefault,
|
||||
releasePointerCapture,
|
||||
setPointerCapture,
|
||||
truncateStringWithEllipsis,
|
||||
usePrefersReducedMotion,
|
||||
stopEventPropagation,
|
||||
} 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 {
|
||||
getEmbedInfo,
|
||||
getEmbedInfoUnsafely,
|
||||
matchEmbedUrl,
|
||||
matchUrl,
|
||||
type TLEmbedResult,
|
||||
} from './lib/utils/embeds'
|
||||
getIndexAbove,
|
||||
getIndexBelow,
|
||||
getIndexBetween,
|
||||
getIndices,
|
||||
getIndicesAbove,
|
||||
getIndicesBelow,
|
||||
getIndicesBetween,
|
||||
sortByIndex,
|
||||
} from './lib/utils/reordering/reordering'
|
||||
export {
|
||||
downloadDataURLAsFile,
|
||||
getSvgAsDataUrl,
|
||||
getSvgAsDataUrlSync,
|
||||
getSvgAsImage,
|
||||
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'
|
||||
applyRotationToSnapshotShapes,
|
||||
getRotationSnapshot,
|
||||
type TLRotationSnapshot,
|
||||
} from './lib/utils/rotation'
|
||||
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 { 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'
|
||||
|
||||
/** @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 { TLRecord, TLStore } from '@tldraw/tlschema'
|
||||
import { RecursivePartial, Required, annotateError } from '@tldraw/utils'
|
||||
import { Required, annotateError } from '@tldraw/utils'
|
||||
import React, {
|
||||
memo,
|
||||
useCallback,
|
||||
|
@ -10,11 +10,11 @@ import React, {
|
|||
useSyncExternalStore,
|
||||
} from 'react'
|
||||
|
||||
import { TLEditorAssetUrls, useDefaultEditorAssetsWithOverrides } from './assetUrls'
|
||||
import { DefaultErrorFallback } from './components/DefaultErrorFallback'
|
||||
import { Canvas } from './components/Canvas'
|
||||
import { OptionalErrorBoundary } from './components/ErrorBoundary'
|
||||
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
|
||||
import { TLUser, createTLUser } from './config/createTLUser'
|
||||
import { AnyTLShapeInfo } from './config/defineShape'
|
||||
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
|
||||
import { Editor } from './editor/Editor'
|
||||
import { TLStateNodeConstructor } from './editor/tools/StateNode'
|
||||
import { ContainerProvider, useContainer } from './hooks/useContainer'
|
||||
|
@ -29,7 +29,6 @@ import {
|
|||
import { useEvent } from './hooks/useEvent'
|
||||
import { useForceUpdate } from './hooks/useForceUpdate'
|
||||
import { useLocalStore } from './hooks/useLocalStore'
|
||||
import { usePreloadAssets } from './hooks/usePreloadAssets'
|
||||
import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix'
|
||||
import { useZoomCss } from './hooks/useZoomCss'
|
||||
import { TLStoreWithStatus } from './utils/sync/StoreWithStatus'
|
||||
|
@ -65,20 +64,15 @@ export interface TldrawEditorBaseProps {
|
|||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -93,6 +87,11 @@ export interface TldrawEditorBaseProps {
|
|||
* Called when the editor has mounted.
|
||||
*/
|
||||
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
|
||||
|
||||
/** @public */
|
||||
|
@ -133,7 +132,7 @@ export const TldrawEditor = memo(function TldrawEditor({
|
|||
// defaults applied in @tldraw/tldraw.
|
||||
const withDefaults = {
|
||||
...rest,
|
||||
shapes: rest.shapes ?? EMPTY_SHAPES_ARRAY,
|
||||
shapeUtils: rest.shapeUtils ?? EMPTY_SHAPE_UTILS_ARRAY,
|
||||
tools: rest.tools ?? EMPTY_TOOLS_ARRAY,
|
||||
}
|
||||
|
||||
|
@ -167,12 +166,12 @@ export const TldrawEditor = memo(function TldrawEditor({
|
|||
})
|
||||
|
||||
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({
|
||||
shapes,
|
||||
shapeUtils,
|
||||
initialData,
|
||||
persistenceKey,
|
||||
sessionId,
|
||||
|
@ -186,7 +185,10 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({
|
|||
store,
|
||||
user,
|
||||
...rest
|
||||
}: Required<TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser }, 'shapes' | 'tools'>) {
|
||||
}: Required<
|
||||
TldrawEditorProps & { store: TLStoreWithStatus; user: TLUser },
|
||||
'shapeUtils' | 'tools'
|
||||
>) {
|
||||
const container = useContainer()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -225,16 +227,16 @@ function TldrawEditorWithReadyStore({
|
|||
children,
|
||||
store,
|
||||
tools,
|
||||
shapes,
|
||||
shapeUtils,
|
||||
autoFocus,
|
||||
user,
|
||||
assetUrls,
|
||||
initialState,
|
||||
}: Required<
|
||||
TldrawEditorProps & {
|
||||
store: TLStore
|
||||
user: TLUser
|
||||
},
|
||||
'shapes' | 'tools'
|
||||
'shapeUtils' | 'tools'
|
||||
>) {
|
||||
const { ErrorFallback } = useEditorComponents()
|
||||
const container = useContainer()
|
||||
|
@ -243,10 +245,11 @@ function TldrawEditorWithReadyStore({
|
|||
useLayoutEffect(() => {
|
||||
const editor = new Editor({
|
||||
store,
|
||||
shapes,
|
||||
shapeUtils,
|
||||
tools,
|
||||
getContainer: () => container,
|
||||
user,
|
||||
initialState,
|
||||
})
|
||||
;(window as any).app = editor
|
||||
;(window as any).editor = editor
|
||||
|
@ -255,10 +258,10 @@ function TldrawEditorWithReadyStore({
|
|||
return () => {
|
||||
editor.dispose()
|
||||
}
|
||||
}, [container, shapes, tools, store, user])
|
||||
}, [container, shapeUtils, tools, store, user, initialState])
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (editor && autoFocus) editor.focus()
|
||||
if (editor && autoFocus) editor.isFocused = true
|
||||
}, [editor, autoFocus])
|
||||
|
||||
const onMountEvent = useEvent((editor: Editor) => {
|
||||
|
@ -288,17 +291,6 @@ function TldrawEditorWithReadyStore({
|
|||
() => 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) {
|
||||
return null
|
||||
}
|
||||
|
@ -311,7 +303,7 @@ function TldrawEditorWithReadyStore({
|
|||
// document in the event of an error to reassure them that their work is
|
||||
// not lost.
|
||||
<OptionalErrorBoundary
|
||||
fallback={ErrorFallback}
|
||||
fallback={ErrorFallback as any}
|
||||
onError={(error) =>
|
||||
editor.annotateError(error, { origin: 'react.tldraw', willCrashApp: true })
|
||||
}
|
||||
|
@ -334,7 +326,7 @@ function Layout({ children }: { children: any }) {
|
|||
useSafariFocusOutFix()
|
||||
useForceUpdate()
|
||||
|
||||
return children
|
||||
return children ?? <Canvas />
|
||||
}
|
||||
|
||||
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 { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
|
||||
|
@ -12,10 +11,10 @@ import { useFixSafariDoubleTapZoomPencilEvents } from '../hooks/useFixSafariDoub
|
|||
import { useGestureEvents } from '../hooks/useGestureEvents'
|
||||
import { useHandleEvents } from '../hooks/useHandleEvents'
|
||||
import { useScreenBounds } from '../hooks/useScreenBounds'
|
||||
import { Matrix2d } from '../primitives/Matrix2d'
|
||||
import { toDomPrecision } from '../primitives/utils'
|
||||
import { debugFlags } from '../utils/debug-flags'
|
||||
import { LiveCollaborators } from './LiveCollaborators'
|
||||
import { SelectionBg } from './SelectionBg'
|
||||
import { SelectionFg } from './SelectionFg'
|
||||
import { Shape } from './Shape'
|
||||
import { ShapeIndicator } from './ShapeIndicator'
|
||||
|
||||
|
@ -97,7 +96,7 @@ export const Canvas = track(function Canvas() {
|
|||
{SvgDefs && <SvgDefs />}
|
||||
</defs>
|
||||
</svg>
|
||||
<SelectionBg />
|
||||
<SelectionBackgroundWrapper />
|
||||
<div className="tl-shapes">
|
||||
<ShapesToDisplay />
|
||||
</div>
|
||||
|
@ -110,7 +109,7 @@ export const Canvas = track(function Canvas() {
|
|||
<HoveredShapeIndicator />
|
||||
<HintedShapeIndicator />
|
||||
<SnapLinesWrapper />
|
||||
<SelectionFg />
|
||||
<SelectionForegroundWrapper />
|
||||
<LiveCollaborators />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -124,12 +123,13 @@ const GridWrapper = track(function GridWrapper() {
|
|||
|
||||
// get grid from context
|
||||
|
||||
const { gridSize } = editor.documentSettings
|
||||
const { x, y, z } = editor.camera
|
||||
const isGridMode = editor.isGridMode
|
||||
|
||||
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() {
|
||||
|
@ -432,3 +432,15 @@ const UiLogger = track(() => {
|
|||
</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 { TLErrorFallbackComponent } from './DefaultErrorFallback'
|
||||
import { TLErrorFallbackComponent } from './default-components/DefaultErrorFallback'
|
||||
|
||||
/** @public */
|
||||
export interface TLErrorBoundaryProps {
|
||||
children: React.ReactNode
|
||||
onError?: ((error: unknown) => void) | null
|
||||
fallback: (props: { error: unknown }) => any
|
||||
fallback: TLErrorFallbackComponent
|
||||
}
|
||||
|
||||
type TLErrorBoundaryState = { error: Error | null }
|
||||
|
@ -21,13 +21,13 @@ export class ErrorBoundary extends React.Component<
|
|||
return { error }
|
||||
}
|
||||
|
||||
state = initialState
|
||||
override state = initialState
|
||||
|
||||
componentDidCatch(error: unknown) {
|
||||
override componentDidCatch(error: unknown) {
|
||||
this.props.onError?.(error)
|
||||
}
|
||||
|
||||
render() {
|
||||
override render() {
|
||||
const { error } = this.state
|
||||
|
||||
if (error !== null) {
|
||||
|
@ -52,7 +52,7 @@ export function OptionalErrorBoundary({
|
|||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={fallback} {...props}>
|
||||
<ErrorBoundary fallback={fallback as any} {...props}>
|
||||
{children}
|
||||
</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 { TLShape, TLShapeId } from '@tldraw/tlschema'
|
||||
import * as React from 'react'
|
||||
import { useEditor } from '../..'
|
||||
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
|
||||
import { useEditor } from '../hooks/useEditor'
|
||||
import { useEditorComponents } from '../hooks/useEditorComponents'
|
||||
import { useShapeEvents } from '../hooks/useShapeEvents'
|
||||
import { Matrix2d } from '../primitives/Matrix2d'
|
||||
import { OptionalErrorBoundary } from './ErrorBoundary'
|
||||
|
||||
/*
|
||||
|
@ -135,7 +135,7 @@ export const Shape = track(function Shape({
|
|||
{isCulled && util.canUnmount(shape) ? (
|
||||
<CulledShape shape={shape} />
|
||||
) : (
|
||||
<OptionalErrorBoundary fallback={ShapeErrorFallback} onError={annotateError}>
|
||||
<OptionalErrorBoundary fallback={ShapeErrorFallback as any} onError={annotateError}>
|
||||
<InnerShape shape={shape} util={util} />
|
||||
</OptionalErrorBoundary>
|
||||
)}
|
||||
|
@ -146,7 +146,7 @@ export const Shape = track(function Shape({
|
|||
|
||||
const InnerShape = React.memo(
|
||||
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
|
||||
)
|
||||
|
@ -159,7 +159,7 @@ const InnerShapeBackground = React.memo(
|
|||
shape: 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
|
||||
)
|
||||
|
|
|
@ -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 { useRef } from 'react'
|
||||
import { useTransform } from '../hooks/useTransform'
|
||||
import { ComponentType, useRef } from 'react'
|
||||
import { useTransform } from '../../hooks/useTransform'
|
||||
import { toDomPrecision } from '../../primitives/utils'
|
||||
|
||||
/** @public */
|
||||
export type TLBrushComponent = (props: {
|
||||
export type TLBrushComponent = ComponentType<{
|
||||
brush: Box2dModel
|
||||
color?: string
|
||||
opacity?: number
|
||||
className?: string
|
||||
}) => any | null
|
||||
}>
|
||||
|
||||
export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
|
||||
const rSvg = useRef<SVGSVGElement>(null)
|
|
@ -1,17 +1,19 @@
|
|||
import { Box2d, clamp, Vec2d } from '@tldraw/primitives'
|
||||
import { Vec2dModel } from '@tldraw/tlschema'
|
||||
import classNames from 'classnames'
|
||||
import { useRef } from 'react'
|
||||
import { useTransform } from '../hooks/useTransform'
|
||||
import { ComponentType, useRef } from 'react'
|
||||
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
|
||||
point: Vec2dModel
|
||||
viewport: Box2d
|
||||
zoom: number
|
||||
opacity?: number
|
||||
color: string
|
||||
}) => JSX.Element | null
|
||||
}>
|
||||
|
||||
export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({
|
||||
className,
|
|
@ -1,17 +1,17 @@
|
|||
import { Vec2dModel } from '@tldraw/tlschema'
|
||||
import classNames from 'classnames'
|
||||
import { memo, useRef } from 'react'
|
||||
import { useTransform } from '../hooks/useTransform'
|
||||
import { ComponentType, memo, useRef } from 'react'
|
||||
import { useTransform } from '../../hooks/useTransform'
|
||||
|
||||
/** @public */
|
||||
export type TLCursorComponent = (props: {
|
||||
export type TLCursorComponent = ComponentType<{
|
||||
className?: string
|
||||
point: Vec2dModel | null
|
||||
zoom: number
|
||||
color?: string
|
||||
name: string | null
|
||||
chatMessage: string
|
||||
}) => any | null
|
||||
}>
|
||||
|
||||
const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name, chatMessage }) => {
|
||||
const rCursor = useRef<HTMLDivElement>(null)
|
|
@ -1,12 +1,12 @@
|
|||
import { useValue } from '@tldraw/state'
|
||||
import classNames from 'classnames'
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { Editor } from '../editor/Editor'
|
||||
import { EditorContext } from '../hooks/useEditor'
|
||||
import { hardResetEditor } from '../utils/hard-reset'
|
||||
import { refreshPage } from '../utils/refresh-page'
|
||||
import { Canvas } from './Canvas'
|
||||
import { ErrorBoundary } from './ErrorBoundary'
|
||||
import { ComponentType, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { Editor } from '../../editor/Editor'
|
||||
import { EditorContext } from '../../hooks/useEditor'
|
||||
import { hardResetEditor } from '../../utils/hardResetEditor'
|
||||
import { refreshPage } from '../../utils/refreshPage'
|
||||
import { Canvas } from '../Canvas'
|
||||
import { ErrorBoundary } from '../ErrorBoundary'
|
||||
|
||||
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() {}
|
||||
|
||||
/** @public */
|
||||
export type TLErrorFallbackComponent = (props: { error: unknown; editor?: Editor }) => any | null
|
||||
export type TLErrorFallbackComponent = ComponentType<{ error: unknown; editor?: Editor }>
|
||||
|
||||
/** @internal */
|
||||
export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }) => {
|
|
@ -1,13 +1,14 @@
|
|||
import { modulate } from '@tldraw/utils'
|
||||
import { GRID_STEPS } from '../constants'
|
||||
import { ComponentType } from 'react'
|
||||
import { GRID_STEPS } from '../../constants'
|
||||
|
||||
/** @public */
|
||||
export type TLGridComponent = (props: {
|
||||
export type TLGridComponent = ComponentType<{
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
size: number
|
||||
}) => JSX.Element | null
|
||||
}>
|
||||
|
||||
export const DefaultGrid: TLGridComponent = ({ x, y, z, size }) => {
|
||||
return (
|
|
@ -1,11 +1,12 @@
|
|||
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
|
||||
import classNames from 'classnames'
|
||||
import { ComponentType } from 'react'
|
||||
|
||||
export type TLHandleComponent = (props: {
|
||||
export type TLHandleComponent = ComponentType<{
|
||||
shapeId: TLShapeId
|
||||
handle: TLHandle
|
||||
className?: string
|
||||
}) => any | null
|
||||
}>
|
||||
|
||||
export const DefaultHandle: TLHandleComponent = ({ handle, className }) => {
|
||||
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 */
|
||||
export type TLShapeErrorFallbackComponent = (props: { error: any }) => any | null
|
||||
export type TLShapeErrorFallbackComponent = ComponentType<{ error: any }>
|
||||
|
||||
/** @internal */
|
||||
export const DefaultShapeErrorFallback: TLShapeErrorFallbackComponent = ({
|
|
@ -1,7 +1,9 @@
|
|||
import { ComponentType } from 'react'
|
||||
|
||||
/** @public */
|
||||
export type TLShapeIndicatorErrorFallback = (props: { error: unknown }) => any | null
|
||||
export type TLShapeIndicatorErrorFallbackComponent = ComponentType<{ error: unknown }>
|
||||
|
||||
/** @internal */
|
||||
export const DefaultShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallback = () => {
|
||||
export const DefaultShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent = () => {
|
||||
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 * as React from 'react'
|
||||
import {
|
||||
type GapsSnapLine,
|
||||
type PointsSnapLine,
|
||||
type SnapLine,
|
||||
} from '../editor/managers/SnapManager'
|
||||
} from '../../editor/managers/SnapManager'
|
||||
import { rangeIntersection } from '../../primitives/utils'
|
||||
|
||||
function PointsSnapLine({ points, zoom }: { zoom: number } & PointsSnapLine) {
|
||||
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
|
||||
line: SnapLine
|
||||
zoom: number
|
||||
}) => any
|
||||
}>
|
||||
|
||||
export const DefaultSnapLine: TLSnapLineComponent = ({ className, line, zoom }) => {
|
||||
return (
|
|
@ -1,4 +1,6 @@
|
|||
export type TLSpinnerComponent = () => any | null
|
||||
import { ComponentType } from 'react'
|
||||
|
||||
export type TLSpinnerComponent = ComponentType<object>
|
||||
|
||||
export const DefaultSpinner: TLSpinnerComponent = () => {
|
||||
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'
|
||||
import { objectMapFromEntries } from '@tldraw/utils'
|
||||
import { T } from '@tldraw/validate'
|
||||
import { uniqueId } from '../utils/data'
|
||||
import { uniqueId } from '../utils/uniqueId'
|
||||
|
||||
const tabIdKey = 'TLDRAW_TAB_ID_v2' as const
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { atom } from '@tldraw/state'
|
|||
import { defineMigrations, migrate } from '@tldraw/store'
|
||||
import { getDefaultTranslationLocale } from '@tldraw/tlschema'
|
||||
import { T } from '@tldraw/validate'
|
||||
import { uniqueId } from '../utils/data'
|
||||
import { uniqueId } from '../utils/uniqueId'
|
||||
|
||||
const USER_DATA_KEY = 'TLDRAW_USER_DATA_v3'
|
||||
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
import { HistoryEntry, SerializedStore, Store, StoreSchema } from '@tldraw/store'
|
||||
import { TLRecord, TLStore, TLStoreProps, createTLSchema } from '@tldraw/tlschema'
|
||||
import { checkShapesAndAddCore } from './defaultShapes'
|
||||
import { AnyTLShapeInfo, TLShapeInfo } from './defineShape'
|
||||
import {
|
||||
SchemaShapeInfo,
|
||||
TLRecord,
|
||||
TLStore,
|
||||
TLStoreProps,
|
||||
TLUnknownShape,
|
||||
createTLSchema,
|
||||
} from '@tldraw/tlschema'
|
||||
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
|
||||
import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from './defaultShapes'
|
||||
|
||||
/** @public */
|
||||
export type TLStoreOptions = {
|
||||
initialData?: SerializedStore<TLRecord>
|
||||
defaultName?: string
|
||||
} & ({ shapes: readonly AnyTLShapeInfo[] } | { schema: StoreSchema<TLRecord, TLStoreProps> })
|
||||
} & (
|
||||
| { shapeUtils: readonly TLAnyShapeUtilConstructor[] }
|
||||
| { schema: StoreSchema<TLRecord, TLStoreProps> }
|
||||
)
|
||||
|
||||
/** @public */
|
||||
export type TLStoreEventInfo = HistoryEntry<TLRecord>
|
||||
|
@ -22,7 +32,7 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
|
|||
const schema =
|
||||
'schema' in rest
|
||||
? rest.schema
|
||||
: createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapes)) })
|
||||
: createTLSchema({ shapes: shapesArrayToShapeMap(checkShapesAndAddCore(rest.shapeUtils)) })
|
||||
return new Store({
|
||||
schema,
|
||||
initialData,
|
||||
|
@ -32,6 +42,14 @@ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStor
|
|||
})
|
||||
}
|
||||
|
||||
function shapesArrayToShapeMap(shapes: TLShapeInfo[]) {
|
||||
return Object.fromEntries(shapes.map((s) => [s.type, s]))
|
||||
function shapesArrayToShapeMap(shapeUtils: TLShapeUtilConstructor<TLUnknownShape>[]) {
|
||||
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 { BookmarkShape } from '../editor/shapes/bookmark/BookmarkShape'
|
||||
import { DrawShape } from '../editor/shapes/draw/DrawShape'
|
||||
import { EmbedShape } from '../editor/shapes/embed/EmbedShape'
|
||||
import { FrameShape } from '../editor/shapes/frame/FrameShape'
|
||||
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'
|
||||
import { TLShapeUtilConstructor } from '../editor/shapes/ShapeUtil'
|
||||
import { GroupShapeUtil } from '../editor/shapes/group/GroupShapeUtil'
|
||||
|
||||
/** @public */
|
||||
export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor<any>
|
||||
|
||||
/** @public */
|
||||
export const coreShapes = [
|
||||
// created by grouping interactions, probably the corest core shape that we have
|
||||
GroupShape,
|
||||
// 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,
|
||||
GroupShapeUtil,
|
||||
] as const
|
||||
|
||||
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>()
|
||||
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 */
|
||||
export const MAX_SHAPES_PER_PAGE = 2000
|
||||
/** @internal */
|
||||
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 */
|
||||
export const ANIMATION_SHORT_MS = 80
|
||||
/** @internal */
|
||||
|
@ -44,17 +33,9 @@ export const MAJOR_NUDGE_FACTOR = 10
|
|||
/** @internal */
|
||||
export const MINOR_NUDGE_FACTOR = 1
|
||||
|
||||
/** @internal */
|
||||
export const MAX_ASSET_WIDTH = 1000
|
||||
/** @internal */
|
||||
export const MAX_ASSET_HEIGHT = 1000
|
||||
|
||||
/** @internal */
|
||||
export const GRID_INCREMENT = 5
|
||||
|
||||
/** @internal */
|
||||
export const MIN_CROP_SIZE = 8
|
||||
|
||||
/** @internal */
|
||||
export const DOUBLE_CLICK_DURATION = 450
|
||||
/** @internal */
|
||||
|
@ -84,7 +65,7 @@ export const DEFAULT_ANIMATION_OPTIONS = {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
export const HAND_TOOL_FRICTION = 0.09
|
||||
export const CAMERA_SLIDE_FRICTION = 0.09
|
||||
|
||||
/** @public */
|
||||
export const GRID_STEPS = [
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,11 @@
|
|||
import { Vec2d } from '@tldraw/primitives'
|
||||
import {
|
||||
COARSE_DRAG_DISTANCE,
|
||||
DOUBLE_CLICK_DURATION,
|
||||
DRAG_DISTANCE,
|
||||
MULTI_CLICK_DURATION,
|
||||
} from '../../constants'
|
||||
import { uniqueId } from '../../utils/data'
|
||||
import { Vec2d } from '../../primitives/Vec2d'
|
||||
import { uniqueId } from '../../utils/uniqueId'
|
||||
import type { Editor } from '../Editor'
|
||||
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 { devFreeze } from '@tldraw/store'
|
||||
import { uniqueId } from '../../utils/data'
|
||||
import { uniqueId } from '../../utils/uniqueId'
|
||||
import { TLCommandHandler, TLHistoryEntry } from '../types/history-types'
|
||||
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 {
|
||||
Box2d,
|
||||
flipSelectionHandleX,
|
||||
flipSelectionHandleY,
|
||||
isSelectionCorner,
|
||||
Matrix2d,
|
||||
rangeIntersection,
|
||||
rangesOverlap,
|
||||
SelectionCorner,
|
||||
SelectionEdge,
|
||||
Vec2d,
|
||||
VecLike,
|
||||
} from '@tldraw/primitives'
|
||||
import { atom, computed, EMPTY_ARRAY } from '@tldraw/state'
|
||||
import { TLGroupShape, TLParentId, TLShape, TLShapeId, Vec2dModel } from '@tldraw/tlschema'
|
||||
import { dedupe, deepCopy } from '@tldraw/utils'
|
||||
import { uniqueId } from '../../utils/data'
|
||||
} from '../../primitives/Box2d'
|
||||
import { Matrix2d } from '../../primitives/Matrix2d'
|
||||
import { rangeIntersection, rangesOverlap } from '../../primitives/utils'
|
||||
import { Vec2d, VecLike } from '../../primitives/Vec2d'
|
||||
import { uniqueId } from '../../utils/uniqueId'
|
||||
import type { Editor } from '../Editor'
|
||||
|
||||
/** @public */
|
||||
export type PointsSnapLine = {
|
||||
id: string
|
||||
type: 'points'
|
||||
points: VecLike[]
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type GapsSnapLine = {
|
||||
id: string
|
||||
type: 'gaps'
|
||||
|
@ -31,6 +32,8 @@ export type GapsSnapLine = {
|
|||
endEdge: [VecLike, VecLike]
|
||||
}>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type SnapLine = PointsSnapLine | GapsSnapLine
|
||||
|
||||
export type SnapInteractionType =
|
||||
|
@ -43,6 +46,7 @@ export type SnapInteractionType =
|
|||
type: 'resize'
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface SnapPoint {
|
||||
id: string
|
||||
x: number
|
||||
|
@ -208,6 +212,7 @@ function dedupeGapSnaps(snaps: Array<Extract<SnapLine, { type: 'gaps' }>>) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export class SnapManager {
|
||||
private _snapLines = atom<SnapLine[] | undefined>('snapLines', undefined)
|
||||
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import { Box2dModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
||||
import { uniqueId } from '../../utils/data'
|
||||
import { uniqueId } from '../../utils/uniqueId'
|
||||
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 = {
|
||||
start: 'left',
|
||||
|
@ -73,7 +82,7 @@ export class TextManager {
|
|||
elm.style.setProperty('max-width', opts.maxWidth)
|
||||
elm.style.setProperty('padding', opts.padding)
|
||||
|
||||
elm.textContent = TextHelpers.normalizeTextForDom(textToMeasure)
|
||||
elm.textContent = normalizeTextForDom(textToMeasure)
|
||||
|
||||
const rect = elm.getBoundingClientRect()
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Vec2d } from '@tldraw/primitives'
|
||||
import { Vec2d } from '../../primitives/Vec2d'
|
||||
import { Editor } from '../Editor'
|
||||
|
||||
export class TickManager {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { Box2d, linesIntersect, pointInPolygon, Vec2d, VecLike } from '@tldraw/primitives'
|
||||
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 { resizeBox } from './shared/resizeBox'
|
||||
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
|
||||
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
|
||||
import { Migrations } from '@tldraw/store'
|
||||
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 { SvgExportContext } from '../types/SvgExportContext'
|
||||
import { TLResizeHandle } from '../types/selection-types'
|
||||
import { SvgExportContext } from './shared/SvgExportContext'
|
||||
|
||||
/** @public */
|
||||
export interface TLShapeUtilConstructor<
|
||||
T extends TLUnknownShape,
|
||||
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']
|
||||
props?: ShapeProps<T>
|
||||
migrations?: Migrations
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -25,27 +30,9 @@ export interface TLShapeUtilCanvasSvgDef {
|
|||
|
||||
/** @public */
|
||||
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
||||
constructor(
|
||||
public editor: Editor,
|
||||
public readonly type: Shape['type'],
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
constructor(public editor: Editor) {}
|
||||
static props?: ShapeProps<TLUnknownShape>
|
||||
static migrations?: Migrations
|
||||
|
||||
/**
|
||||
* 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