Custom Tools DX + screenshot example (#2198)
This PR adds a custom tool example, the `Screenshot Tool`. It demonstrates how a user can create a custom tool together with custom tool UI. ### Change Type - [x] `minor` — New feature ### Test Plan 1. Use the screenshot example ### Release Notes - adds ScreenshotTool custom tool example - improvements and new exports related to copying and exporting images / files - loosens up types around icons and translations - moving `StateNode.isActive` into an atom - adding `Editor.path`
This commit is contained in:
parent
d683cc0943
commit
14e8d19a71
116 changed files with 2559 additions and 1519 deletions
|
@ -1,249 +1,234 @@
|
|||
export {}
|
||||
import test, { Page, expect } from '@playwright/test'
|
||||
import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
|
||||
import assert from 'assert'
|
||||
import { rename, writeFile } from 'fs/promises'
|
||||
import { setupPage } from '../shared-e2e'
|
||||
|
||||
// import test, { Page, expect } from '@playwright/test'
|
||||
// import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
|
||||
// import assert from 'assert'
|
||||
// import { rename, writeFile } from 'fs/promises'
|
||||
// import { setupPage } from '../shared-e2e'
|
||||
declare const editor: Editor
|
||||
|
||||
// declare const editor: Editor
|
||||
test.describe('Export snapshots', () => {
|
||||
const snapshots = {
|
||||
'Exports geo text with leading line breaks': [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'geo',
|
||||
props: {
|
||||
w: 100,
|
||||
h: 30,
|
||||
text: '\n\n\n\n\n\ntext',
|
||||
},
|
||||
},
|
||||
],
|
||||
'Exports geo text with trailing line breaks': [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'geo',
|
||||
props: {
|
||||
w: 100,
|
||||
h: 30,
|
||||
text: 'text\n\n\n\n\n\n',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as Record<string, TLShapePartial[]>
|
||||
|
||||
// test.describe('Export snapshots', () => {
|
||||
// const snapshots = {
|
||||
// 'Exports geo text with leading line breaks': [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'geo',
|
||||
// props: {
|
||||
// w: 100,
|
||||
// h: 30,
|
||||
// text: '\n\n\n\n\n\ntext',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// 'Exports geo text with trailing line breaks': [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'geo',
|
||||
// props: {
|
||||
// w: 100,
|
||||
// h: 30,
|
||||
// text: 'text\n\n\n\n\n\n',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// } as Record<string, TLShapePartial[]>
|
||||
for (const fill of ['none', 'semi', 'solid', 'pattern']) {
|
||||
snapshots[`geo fill=${fill}`] = [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'geo',
|
||||
props: {
|
||||
fill,
|
||||
color: 'green',
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// for (const fill of ['none', 'semi', 'solid', 'pattern']) {
|
||||
// snapshots[`geo fill=${fill}`] = [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'geo',
|
||||
// props: {
|
||||
// fill,
|
||||
// color: 'green',
|
||||
// w: 100,
|
||||
// h: 100,
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
snapshots[`arrow fill=${fill}`] = [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'arrow',
|
||||
props: {
|
||||
color: 'light-green',
|
||||
fill: fill,
|
||||
arrowheadStart: 'square',
|
||||
arrowheadEnd: 'dot',
|
||||
start: { type: 'point', x: 0, y: 0 },
|
||||
end: { type: 'point', x: 100, y: 100 },
|
||||
bend: 20,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// snapshots[`arrow fill=${fill}`] = [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'arrow',
|
||||
// props: {
|
||||
// color: 'light-green',
|
||||
// fill: fill,
|
||||
// arrowheadStart: 'square',
|
||||
// arrowheadEnd: 'dot',
|
||||
// start: { type: 'point', x: 0, y: 0 },
|
||||
// end: { type: 'point', x: 100, y: 100 },
|
||||
// bend: 20,
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
snapshots[`draw fill=${fill}`] = [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'draw',
|
||||
props: {
|
||||
color: 'light-violet',
|
||||
fill: fill,
|
||||
segments: [
|
||||
{
|
||||
type: 'straight',
|
||||
points: [{ x: 0, y: 0 }],
|
||||
},
|
||||
{
|
||||
type: 'straight',
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 100, y: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'straight',
|
||||
points: [
|
||||
{ x: 100, y: 0 },
|
||||
{ x: 0, y: 100 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'straight',
|
||||
points: [
|
||||
{ x: 0, y: 100 },
|
||||
{ x: 100, y: 100 },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'straight',
|
||||
points: [
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 0, y: 0 },
|
||||
],
|
||||
},
|
||||
],
|
||||
isClosed: true,
|
||||
isComplete: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// snapshots[`draw fill=${fill}`] = [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'draw',
|
||||
// props: {
|
||||
// color: 'light-violet',
|
||||
// fill: fill,
|
||||
// segments: [
|
||||
// {
|
||||
// type: 'straight',
|
||||
// points: [{ x: 0, y: 0 }],
|
||||
// },
|
||||
// {
|
||||
// type: 'straight',
|
||||
// points: [
|
||||
// { x: 0, y: 0 },
|
||||
// { x: 100, y: 0 },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// type: 'straight',
|
||||
// points: [
|
||||
// { x: 100, y: 0 },
|
||||
// { x: 0, y: 100 },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// type: 'straight',
|
||||
// points: [
|
||||
// { x: 0, y: 100 },
|
||||
// { x: 100, y: 100 },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// type: 'straight',
|
||||
// points: [
|
||||
// { x: 100, y: 100 },
|
||||
// { x: 0, y: 0 },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// isClosed: true,
|
||||
// isComplete: true,
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
for (const font of ['draw', 'sans', 'serif', 'mono']) {
|
||||
snapshots[`geo font=${font}`] = [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'geo',
|
||||
props: {
|
||||
text: 'test',
|
||||
color: 'blue',
|
||||
font,
|
||||
w: 100,
|
||||
h: 100,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// for (const font of ['draw', 'sans', 'serif', 'mono']) {
|
||||
// snapshots[`geo font=${font}`] = [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'geo',
|
||||
// props: {
|
||||
// text: 'test',
|
||||
// color: 'blue',
|
||||
// font,
|
||||
// w: 100,
|
||||
// h: 100,
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
snapshots[`arrow font=${font}`] = [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'arrow',
|
||||
props: {
|
||||
color: 'blue',
|
||||
fill: 'solid',
|
||||
arrowheadStart: 'square',
|
||||
arrowheadEnd: 'arrow',
|
||||
font,
|
||||
start: { type: 'point', x: 0, y: 0 },
|
||||
end: { type: 'point', x: 100, y: 100 },
|
||||
bend: 20,
|
||||
text: 'test',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// snapshots[`arrow font=${font}`] = [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'arrow',
|
||||
// props: {
|
||||
// color: 'blue',
|
||||
// fill: 'solid',
|
||||
// arrowheadStart: 'square',
|
||||
// arrowheadEnd: 'arrow',
|
||||
// font,
|
||||
// start: { type: 'point', x: 0, y: 0 },
|
||||
// end: { type: 'point', x: 100, y: 100 },
|
||||
// bend: 20,
|
||||
// text: 'test',
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
snapshots[`arrow font=${font}`] = [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'arrow',
|
||||
props: {
|
||||
color: 'blue',
|
||||
fill: 'solid',
|
||||
arrowheadStart: 'square',
|
||||
arrowheadEnd: 'arrow',
|
||||
font,
|
||||
start: { type: 'point', x: 0, y: 0 },
|
||||
end: { type: 'point', x: 100, y: 100 },
|
||||
bend: 20,
|
||||
text: 'test',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// snapshots[`arrow font=${font}`] = [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'arrow',
|
||||
// props: {
|
||||
// color: 'blue',
|
||||
// fill: 'solid',
|
||||
// arrowheadStart: 'square',
|
||||
// arrowheadEnd: 'arrow',
|
||||
// font,
|
||||
// start: { type: 'point', x: 0, y: 0 },
|
||||
// end: { type: 'point', x: 100, y: 100 },
|
||||
// bend: 20,
|
||||
// text: 'test',
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
snapshots[`note font=${font}`] = [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'note',
|
||||
props: {
|
||||
color: 'violet',
|
||||
font,
|
||||
text: 'test',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// snapshots[`note font=${font}`] = [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'note',
|
||||
// props: {
|
||||
// color: 'violet',
|
||||
// font,
|
||||
// text: 'test',
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
snapshots[`text font=${font}`] = [
|
||||
{
|
||||
id: 'shape:testShape' as TLShapeId,
|
||||
type: 'text',
|
||||
props: {
|
||||
color: 'red',
|
||||
font,
|
||||
text: 'test',
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// snapshots[`text font=${font}`] = [
|
||||
// {
|
||||
// id: 'shape:testShape' as TLShapeId,
|
||||
// type: 'text',
|
||||
// props: {
|
||||
// color: 'red',
|
||||
// font,
|
||||
// text: 'test',
|
||||
// },
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
const snapshotsToTest = Object.entries(snapshots)
|
||||
const filteredSnapshots = snapshotsToTest // maybe we filter these down, there are a lot of them
|
||||
|
||||
// for (const [name, shapes] of Object.entries(snapshots)) {
|
||||
// test(`Exports with ${name}`, async ({ browser }) => {
|
||||
// const page = await browser.newPage()
|
||||
// await setupPage(page)
|
||||
// await page.evaluate((shapes) => {
|
||||
// editor
|
||||
// .updateInstanceState({ exportBackground: false })
|
||||
// .selectAll()
|
||||
// .deleteShapes(editor.selectedShapeIds)
|
||||
// .createShapes(shapes)
|
||||
// }, shapes as any)
|
||||
for (const [name, shapes] of filteredSnapshots) {
|
||||
test(`Exports with ${name} in dark mode`, async ({ browser }) => {
|
||||
const page = await browser.newPage()
|
||||
await setupPage(page)
|
||||
await page.evaluate((shapes) => {
|
||||
editor.user.updateUserPreferences({ isDarkMode: true })
|
||||
editor
|
||||
.updateInstanceState({ exportBackground: false })
|
||||
.selectAll()
|
||||
.deleteShapes(editor.selectedShapeIds)
|
||||
.createShapes(shapes)
|
||||
}, shapes as any)
|
||||
|
||||
// await snapshotTest(page)
|
||||
// })
|
||||
// }
|
||||
await snapshotTest(page)
|
||||
})
|
||||
}
|
||||
|
||||
// for (const [name, shapes] of Object.entries(snapshots)) {
|
||||
// test(`Exports with ${name} in dark mode`, async ({ browser }) => {
|
||||
// const page = await browser.newPage()
|
||||
// await setupPage(page)
|
||||
// await page.evaluate((shapes) => {
|
||||
// editor.user.updateUserPreferences({ isDarkMode: true })
|
||||
// editor
|
||||
// .updateInstanceState({ exportBackground: false })
|
||||
// .selectAll()
|
||||
// .deleteShapes(editor.selectedShapeIds)
|
||||
// .createShapes(shapes)
|
||||
// }, shapes as any)
|
||||
async function snapshotTest(page: Page) {
|
||||
page.waitForEvent('download').then(async (download) => {
|
||||
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" />
|
||||
`,
|
||||
'utf-8'
|
||||
)
|
||||
|
||||
// await snapshotTest(page)
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
// async function snapshotTest(page: Page) {
|
||||
// page.waitForEvent('download').then(async (download) => {
|
||||
// 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" />
|
||||
// `,
|
||||
// 'utf-8'
|
||||
// )
|
||||
|
||||
// await page.goto(`file://${path}.html`)
|
||||
// const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
|
||||
// await expect(page).toHaveScreenshot({
|
||||
// omitBackground: true,
|
||||
// clip,
|
||||
// })
|
||||
// })
|
||||
// await page.evaluate(() => (window as any)['tldraw-export']())
|
||||
// }
|
||||
await page.goto(`file://${path}.html`)
|
||||
const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
|
||||
await expect(page).toHaveScreenshot({
|
||||
omitBackground: true,
|
||||
clip,
|
||||
})
|
||||
})
|
||||
await page.evaluate(() => (window as any)['tldraw-export']())
|
||||
}
|
||||
})
|
||||
|
|
5
apps/examples/public/tool-screenshot.svg
Normal file
5
apps/examples/public/tool-screenshot.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 15C20 17.7614 17.7614 20 15 20C12.2386 20 10 17.7614 10 15C10 12.2386 12.2386 10 15 10C17.7614 10 20 12.2386 20 15Z" stroke="black" stroke-width="2"/>
|
||||
<rect x="21" y="5" width="4" height="4" rx="2" fill="black"/>
|
||||
<path d="M5 3H25C26.1046 3 27 3.89543 27 5V25C27 26.1046 26.1046 27 25 27H5C3.89543 27 3 26.1046 3 25V5C3 3.89543 3.89543 3 5 3Z" stroke="black" stroke-width="2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 493 B |
|
@ -1,7 +1,7 @@
|
|||
import { Tldraw, TLEditorComponents } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
const components: Partial<TLEditorComponents> = {
|
||||
const components: TLEditorComponents = {
|
||||
Brush: function MyBrush({ brush }) {
|
||||
return (
|
||||
<svg className="tl-overlays__item">
|
||||
|
|
|
@ -8,7 +8,7 @@ export const uiOverrides: TLUiOverrides = {
|
|||
tools.card = {
|
||||
id: 'card',
|
||||
icon: 'color',
|
||||
label: 'Card' as any,
|
||||
label: 'Card',
|
||||
kbd: 'c',
|
||||
readonlyOk: false,
|
||||
onSelect: () => {
|
||||
|
|
|
@ -72,7 +72,7 @@ const MyComponentInFront = track(() => {
|
|||
)
|
||||
})
|
||||
|
||||
const components: Partial<TLEditorComponents> = {
|
||||
const components: TLEditorComponents = {
|
||||
OnTheCanvas: MyComponent,
|
||||
InFrontOfTheCanvas: MyComponentInFront,
|
||||
SnapLine: null,
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { StateNode, TLCancelEvent, TLInterruptEvent } from '@tldraw/tldraw'
|
||||
import { ScreenshotDragging } from './childStates/Dragging'
|
||||
import { ScreenshotIdle } from './childStates/Idle'
|
||||
import { ScreenshotPointing } from './childStates/Pointing'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
export class ScreenshotTool extends StateNode {
|
||||
// [1]
|
||||
static override id = 'screenshot'
|
||||
static override initial = 'idle'
|
||||
static override children = () => [ScreenshotIdle, ScreenshotPointing, ScreenshotDragging]
|
||||
|
||||
// [2]
|
||||
override onEnter = () => {
|
||||
this.editor.setCursor({ type: 'cross', rotation: 0 })
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
this.editor.setCursor({ type: 'default', rotation: 0 })
|
||||
}
|
||||
|
||||
// [3]
|
||||
override onInterrupt: TLInterruptEvent = () => {
|
||||
this.complete()
|
||||
}
|
||||
|
||||
override onCancel: TLCancelEvent = () => {
|
||||
this.complete()
|
||||
}
|
||||
|
||||
private complete() {
|
||||
this.parent.transition('select', {})
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This file contains our screenshot tool. The tool is a StateNode with the `id` "screenshot".
|
||||
|
||||
[1]
|
||||
It has three child state nodes, ScreenshotIdle, ScreenshotPointing, and ScreenshotDragging.
|
||||
Its initial state is `idle`.
|
||||
|
||||
[2]
|
||||
When the screenshot tool is entered, we set the cursor to a crosshair. When it is exited, we
|
||||
set the cursor back to the default cursor.
|
||||
|
||||
[3]
|
||||
When the screenshot tool is interrupted or cancelled, we transition back to the select tool.
|
||||
*/
|
|
@ -0,0 +1,123 @@
|
|||
import { Box2d, StateNode, atom, copyAs, exportAs } from '@tldraw/tldraw'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
export class ScreenshotDragging extends StateNode {
|
||||
static override id = 'dragging'
|
||||
|
||||
// [1]
|
||||
screenshotBox = atom('screenshot brush', new Box2d())
|
||||
|
||||
// [2]
|
||||
override onEnter = () => {
|
||||
this.update()
|
||||
}
|
||||
|
||||
override onPointerMove = () => {
|
||||
this.update()
|
||||
}
|
||||
|
||||
override onKeyDown = () => {
|
||||
this.update()
|
||||
}
|
||||
|
||||
override onKeyUp = () => {
|
||||
this.update()
|
||||
}
|
||||
|
||||
private update() {
|
||||
const {
|
||||
inputs: { shiftKey, altKey, originPagePoint, currentPagePoint },
|
||||
} = this.editor
|
||||
|
||||
const box = Box2d.FromPoints([originPagePoint, currentPagePoint])
|
||||
|
||||
if (shiftKey) {
|
||||
if (box.w > box.h * (16 / 9)) {
|
||||
box.h = box.w * (9 / 16)
|
||||
} else {
|
||||
box.w = box.h * (16 / 9)
|
||||
}
|
||||
|
||||
if (currentPagePoint.x < originPagePoint.x) {
|
||||
box.x = originPagePoint.x - box.w
|
||||
}
|
||||
|
||||
if (currentPagePoint.y < originPagePoint.y) {
|
||||
box.y = originPagePoint.y - box.h
|
||||
}
|
||||
}
|
||||
|
||||
if (altKey) {
|
||||
box.w *= 2
|
||||
box.h *= 2
|
||||
box.x = originPagePoint.x - box.w / 2
|
||||
box.y = originPagePoint.y - box.h / 2
|
||||
}
|
||||
|
||||
this.screenshotBox.set(box)
|
||||
}
|
||||
|
||||
// [3]
|
||||
override onPointerUp = () => {
|
||||
const { editor } = this
|
||||
const box = this.screenshotBox.value
|
||||
|
||||
// get all shapes contained by or intersecting the box
|
||||
const shapes = editor.currentPageShapes.filter((s) => {
|
||||
const pageBounds = editor.getShapeMaskedPageBounds(s)
|
||||
if (!pageBounds) return false
|
||||
return box.includes(pageBounds)
|
||||
})
|
||||
|
||||
if (shapes.length) {
|
||||
if (editor.inputs.ctrlKey) {
|
||||
// Copy the shapes to the clipboard
|
||||
copyAs(
|
||||
editor,
|
||||
shapes.map((s) => s.id),
|
||||
'png',
|
||||
{ bounds: box, background: editor.instanceState.exportBackground }
|
||||
)
|
||||
} else {
|
||||
// Export the shapes as a png
|
||||
exportAs(
|
||||
editor,
|
||||
shapes.map((s) => s.id),
|
||||
'png',
|
||||
{ bounds: box, background: editor.instanceState.exportBackground }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.setCurrentTool('select')
|
||||
}
|
||||
|
||||
// [4]
|
||||
override onCancel = () => {
|
||||
this.editor.setCurrentTool('select')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[1]
|
||||
This state has a reactive property (an Atom) called "screenshotBox". This is the box
|
||||
that the user is drawing on the screen as they drag their pointer. We use an Atom here
|
||||
so that our UI can subscribe to this property using `useValue` (see the ScreenshotBox
|
||||
component in ScreenshotToolExample).
|
||||
|
||||
[2]
|
||||
When the user enters this state, or when they move their pointer, we update the
|
||||
screenshotBox property to be drawn between the place where the user started pointing
|
||||
and the place where their pointer is now. If the user is holding Shift, then we modify
|
||||
the dimensions of this box so that it is in a 16:9 aspect ratio.
|
||||
|
||||
[3]
|
||||
When the user makes a pointer up and stops dragging, we export the shapes contained by
|
||||
the screenshot box as a png. If the user is holding the ctrl key, we copy the shapes
|
||||
to the clipboard instead.
|
||||
|
||||
[4]
|
||||
When the user cancels (esc key) or makes a pointer up event, we transition back to the
|
||||
select tool.
|
||||
*/
|
|
@ -0,0 +1,17 @@
|
|||
import { StateNode, TLEventHandlers } from '@tldraw/tldraw'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
export class ScreenshotIdle extends StateNode {
|
||||
static override id = 'idle'
|
||||
|
||||
// [1]
|
||||
override onPointerDown: TLEventHandlers['onPointerUp'] = () => {
|
||||
this.parent.transition('pointing')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[1]
|
||||
When we the user makes a pointer down event, we transition to the pointing state.
|
||||
*/
|
|
@ -0,0 +1,38 @@
|
|||
import { StateNode, TLEventHandlers } from '@tldraw/tldraw'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
export class ScreenshotPointing extends StateNode {
|
||||
static override id = 'pointing'
|
||||
|
||||
// [1]
|
||||
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.parent.transition('dragging')
|
||||
}
|
||||
}
|
||||
|
||||
// [2]
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||
this.complete()
|
||||
}
|
||||
|
||||
override onCancel: TLEventHandlers['onCancel'] = () => {
|
||||
this.complete()
|
||||
}
|
||||
|
||||
private complete() {
|
||||
this.parent.transition('idle')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[1]
|
||||
When the user makes a pointer move event, we check if they are dragging. If they are,
|
||||
we transition to the dragging state. If they are not yet dragging, we stay in this state.
|
||||
|
||||
[2]
|
||||
When the user cancelles or makes a pointer up event (while this state is still active,
|
||||
so after the user has started pointing but before they've moved their pointer far enough
|
||||
to start dragging), then we transition back to the idle state.
|
||||
*/
|
|
@ -0,0 +1,157 @@
|
|||
import {
|
||||
Box2d,
|
||||
TLEditorComponents,
|
||||
TLUiAssetUrlOverrides,
|
||||
TLUiOverrides,
|
||||
Tldraw,
|
||||
toolbarItem,
|
||||
useEditor,
|
||||
useValue,
|
||||
} from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { ScreenshotTool } from './ScreenshotTool/ScreenshotTool'
|
||||
import { ScreenshotDragging } from './ScreenshotTool/childStates/Dragging'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
// [1]
|
||||
const customTools = [ScreenshotTool]
|
||||
|
||||
// [2]
|
||||
const customUiOverrides: TLUiOverrides = {
|
||||
tools: (editor, tools) => {
|
||||
return {
|
||||
...tools,
|
||||
screenshot: {
|
||||
id: 'screenshot',
|
||||
label: 'Screenshot',
|
||||
readonlyOk: false,
|
||||
icon: 'tool-screenshot',
|
||||
kbd: 'j',
|
||||
onSelect() {
|
||||
editor.setCurrentTool('screenshot')
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
toolbar: (_editor, toolbarItems, { tools }) => {
|
||||
toolbarItems.splice(4, 0, toolbarItem(tools.screenshot))
|
||||
return toolbarItems
|
||||
},
|
||||
}
|
||||
|
||||
// [3]
|
||||
const customAssetUrls: TLUiAssetUrlOverrides = {
|
||||
icons: {
|
||||
'tool-screenshot': '/tool-screenshot.svg',
|
||||
},
|
||||
}
|
||||
|
||||
// [4]
|
||||
function ScreenshotBox() {
|
||||
const editor = useEditor()
|
||||
|
||||
const screenshotBrush = useValue(
|
||||
'screenshot brush',
|
||||
() => {
|
||||
// Check whether the screenshot tool (and its dragging state) is active
|
||||
if (editor.getPath() !== 'screenshot.dragging') return null
|
||||
|
||||
// Get screenshot.dragging state node
|
||||
const draggingState = editor.getStateDescendant<ScreenshotDragging>('screenshot.dragging')!
|
||||
|
||||
// Get the box from the screenshot.dragging state node
|
||||
const box = draggingState.screenshotBox.get()
|
||||
|
||||
// The box is in "page space", i.e. panned and zoomed with the canvas, but we
|
||||
// want to show it in front of the canvas, so we'll need to convert it to
|
||||
// "page space", i.e. uneffected by scale, and relative to the tldraw
|
||||
// page's top left corner.
|
||||
const { zoomLevel } = editor
|
||||
const { x, y } = editor.pageToScreen({ x: box.x, y: box.y })
|
||||
return new Box2d(x, y, box.w * zoomLevel, box.h * zoomLevel)
|
||||
},
|
||||
[editor]
|
||||
)
|
||||
|
||||
if (!screenshotBrush) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
transform: `translate(${screenshotBrush.x}px, ${screenshotBrush.y}px)`,
|
||||
width: screenshotBrush.w,
|
||||
height: screenshotBrush.h,
|
||||
border: '1px solid var(--color-text-0)',
|
||||
zIndex: 999,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const customComponents: TLEditorComponents = {
|
||||
InFrontOfTheCanvas: () => {
|
||||
return <ScreenshotBox />
|
||||
},
|
||||
}
|
||||
|
||||
// [5]
|
||||
export default function ScreenshotToolExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
persistenceKey="tldraw_screenshot_example"
|
||||
tools={customTools}
|
||||
overrides={customUiOverrides}
|
||||
assetUrls={customAssetUrls}
|
||||
components={customComponents}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
Introduction:
|
||||
|
||||
This example shows how to create a custom tool. In tldraw, tools are parts of the
|
||||
tldraw state chart. While the most common use for tools is creating shapes, you can
|
||||
use tools to create other types of interactions too! In this example, we create a
|
||||
"screenshot tool" that lets the user draw a box on the canvas. When the user finishes
|
||||
drawing their box, we'll export (or copy) a screenshot of that area.
|
||||
|
||||
[1]
|
||||
Our custom tool is a class that extends the StateNode class. See the ScreenshotTool
|
||||
files for more about the too. We define an array (outside of any React component)
|
||||
to hold the custom tools. We'll pass this into the Tldraw component's `tools` prop.
|
||||
|
||||
[2]
|
||||
Here we add our custom tool to the toolbar. We do this by providing a custom
|
||||
toolbar override to the Tldraw component. This override is a function that takes
|
||||
the current editor, the default toolbar items, and the default tools. It returns
|
||||
the new toolbar items. We use the toolbarItem helper to create a new toolbar item
|
||||
for our custom tool. We then splice it into the toolbar items array at the 4th index.
|
||||
This puts it after the eraser tool. We'll pass our overrides object into the
|
||||
Tldraw component's `overrides` prop.
|
||||
|
||||
[3]
|
||||
Our toolbar item is using a custom icon, so we need to provide the asset url for it.
|
||||
We do this by providing a custom assetUrls object to the Tldraw component.
|
||||
This object is a map of icon ids to their urls. The icon ids are the same as the
|
||||
icon prop on the toolbar item. We'll pass our assetUrls object into the Tldraw
|
||||
component's `assetUrls` prop.
|
||||
|
||||
[4]
|
||||
We want to show a box on the canvas when the screenshot tool is active. We do this
|
||||
by providing an override to the InFrontOfTheCanvas component. This component will be shown
|
||||
in front of the canvas but behind any other UI elements, such as menus and the toolbar.
|
||||
We'll pass our components object into the Tldraw component's `components` prop.
|
||||
|
||||
[5]
|
||||
Finally we pass all of our customizations into the Tldraw component. It's important
|
||||
that the customizations are defined outside of the React component, otherwise they
|
||||
will cause the Tldraw component to see them as new values on every render, which may
|
||||
produce unexpected results.
|
||||
*/
|
|
@ -30,6 +30,7 @@ import MultipleExample from './examples/MultipleExample'
|
|||
import OnTheCanvasExample from './examples/OnTheCanvas'
|
||||
import PersistenceExample from './examples/PersistenceExample'
|
||||
import ReadOnlyExample from './examples/ReadOnlyExample'
|
||||
import ScreenshotToolExample from './examples/ScreenshotToolExample/ScreenshotToolExample'
|
||||
import ScrollExample from './examples/ScrollExample'
|
||||
import ShapeMetaExample from './examples/ShapeMetaExample'
|
||||
import SnapshotExample from './examples/SnapshotExample/SnapshotExample'
|
||||
|
@ -116,6 +117,11 @@ export const allExamples: Example[] = [
|
|||
path: 'custom-ui',
|
||||
element: <CustomUiExample />,
|
||||
},
|
||||
{
|
||||
title: 'Custom Tool (Screenshot)',
|
||||
path: 'screenshot-tool',
|
||||
element: <ScreenshotToolExample />,
|
||||
},
|
||||
{
|
||||
title: 'Hide UI',
|
||||
path: 'hide-ui',
|
||||
|
|
|
@ -58,6 +58,7 @@ const menuOverrides = {
|
|||
schema.forEach((item) => {
|
||||
if (item.id === 'menu' && item.type === 'group') {
|
||||
item.children = item.children.filter((menuItem) => {
|
||||
if (!menuItem) return false
|
||||
if (menuItem.id === 'file' && menuItem.type === 'submenu') {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -275,6 +275,9 @@ export class Box2d {
|
|||
zeroFix(): this;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type BoxLike = Box2d | Box2dModel;
|
||||
|
||||
// @internal (undocumented)
|
||||
export const CAMERA_SLIDE_FRICTION = 0.09;
|
||||
|
||||
|
@ -625,7 +628,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// @deprecated (undocumented)
|
||||
get currentPageState(): TLInstancePageState;
|
||||
// @deprecated (undocumented)
|
||||
get currentTool(): StateNode | undefined;
|
||||
get currentTool(): StateNode;
|
||||
// @deprecated (undocumented)
|
||||
get currentToolId(): string;
|
||||
deleteAssets(assets: TLAsset[] | TLAssetId[]): this;
|
||||
|
@ -701,7 +704,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getCurrentPageShapes(): TLShape[];
|
||||
getCurrentPageShapesSorted(): TLShape[];
|
||||
getCurrentPageState(): TLInstancePageState;
|
||||
getCurrentTool(): StateNode | undefined;
|
||||
getCurrentTool(): StateNode;
|
||||
getCurrentToolId(): string;
|
||||
getDocumentSettings(): TLDocument;
|
||||
getDroppingOverShape(point: VecLike, droppingShapes?: TLShape[]): TLUnknownShape | undefined;
|
||||
|
@ -783,16 +786,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getSharedOpacity(): SharedStyle<number>;
|
||||
getSharedStyles(): ReadonlySharedStyleMap;
|
||||
getSortedChildIdsForParent(parent: TLPage | TLParentId | TLShape): TLShapeId[];
|
||||
getStateDescendant(path: string): StateNode | undefined;
|
||||
getStateDescendant<T extends StateNode>(path: string): T | undefined;
|
||||
// @internal (undocumented)
|
||||
getStyleForNextShape<T>(style: StyleProp<T>): T;
|
||||
getSvg(shapes: TLShape[] | TLShapeId[], opts?: Partial<{
|
||||
scale: number;
|
||||
background: boolean;
|
||||
padding: number;
|
||||
darkMode?: boolean | undefined;
|
||||
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio'];
|
||||
}>): Promise<SVGSVGElement | undefined>;
|
||||
getSvg(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<SVGSVGElement | undefined>;
|
||||
getViewportPageBounds(): Box2d;
|
||||
getViewportPageCenter(): Vec2d;
|
||||
getViewportScreenBounds(): Box2d;
|
||||
|
@ -2128,7 +2125,7 @@ export interface TldrawEditorBaseProps {
|
|||
autoFocus?: boolean;
|
||||
children?: any;
|
||||
className?: string;
|
||||
components?: Partial<TLEditorComponents>;
|
||||
components?: TLEditorComponents;
|
||||
inferDarkMode?: boolean;
|
||||
initialState?: string;
|
||||
onMount?: TLOnMountHandler;
|
||||
|
@ -2150,13 +2147,9 @@ export type TldrawEditorProps = TldrawEditorBaseProps & ({
|
|||
});
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLEditorComponents = {
|
||||
export type TLEditorComponents = Partial<{
|
||||
[K in keyof BaseEditorComponents]: BaseEditorComponents[K] | null;
|
||||
} & {
|
||||
ErrorFallback: TLErrorFallbackComponent;
|
||||
ShapeErrorFallback: TLShapeErrorFallbackComponent;
|
||||
ShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent;
|
||||
};
|
||||
} & ErrorComponents>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface TLEditorOptions {
|
||||
|
@ -2682,6 +2675,16 @@ export type TLStoreWithStatus = {
|
|||
// @public (undocumented)
|
||||
export type TLSvgDefsComponent = React.ComponentType;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLSvgOptions = {
|
||||
bounds: Box2d;
|
||||
scale: number;
|
||||
background: boolean;
|
||||
padding: number;
|
||||
darkMode?: boolean;
|
||||
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio'];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLTickEvent = (elapsed: number) => void;
|
||||
|
||||
|
|
|
@ -3770,6 +3770,42 @@
|
|||
],
|
||||
"implementsTokenRanges": []
|
||||
},
|
||||
{
|
||||
"kind": "TypeAlias",
|
||||
"canonicalReference": "@tldraw/editor!BoxLike:type",
|
||||
"docComment": "/**\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export type BoxLike = "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Box2d",
|
||||
"canonicalReference": "@tldraw/editor!Box2d:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": " | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Box2dModel",
|
||||
"canonicalReference": "@tldraw/tlschema!Box2dModel:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/primitives/Box2d.ts",
|
||||
"releaseTag": "Public",
|
||||
"name": "BoxLike",
|
||||
"typeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!canonicalizeRotation:function(1)",
|
||||
|
@ -8256,10 +8292,6 @@
|
|||
"text": "StateNode",
|
||||
"canonicalReference": "@tldraw/editor!StateNode:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": " | undefined"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
|
@ -8271,7 +8303,7 @@
|
|||
"name": "currentTool",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
"endIndex": 2
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
|
@ -10447,10 +10479,6 @@
|
|||
"text": "StateNode",
|
||||
"canonicalReference": "@tldraw/editor!StateNode:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": " | undefined"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
|
@ -10459,7 +10487,7 @@
|
|||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
"endIndex": 2
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
|
@ -13658,7 +13686,16 @@
|
|||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getStateDescendant(path: "
|
||||
"text": "getStateDescendant<T extends "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "StateNode",
|
||||
"canonicalReference": "@tldraw/editor!StateNode:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">(path: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -13668,24 +13705,32 @@
|
|||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "StateNode",
|
||||
"canonicalReference": "@tldraw/editor!StateNode:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": " | undefined"
|
||||
"text": "T | undefined"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"typeParameters": [
|
||||
{
|
||||
"typeParameterName": "T",
|
||||
"constraintTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"defaultTypeTokenRange": {
|
||||
"startIndex": 0,
|
||||
"endIndex": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 5
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
|
@ -13694,8 +13739,8 @@
|
|||
{
|
||||
"parameterName": "path",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
|
@ -13740,27 +13785,18 @@
|
|||
"text": "Partial",
|
||||
"canonicalReference": "!Partial:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n scale: number;\n background: boolean;\n padding: number;\n darkMode?: boolean | undefined;\n preserveAspectRatio: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "React.SVGAttributes",
|
||||
"canonicalReference": "@types/react!React.SVGAttributes:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<"
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "SVGSVGElement",
|
||||
"canonicalReference": "!SVGSVGElement:interface"
|
||||
"text": "TLSvgOptions",
|
||||
"canonicalReference": "@tldraw/editor!TLSvgOptions:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">['preserveAspectRatio'];\n }>"
|
||||
"text": ">"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -13791,8 +13827,8 @@
|
|||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 13,
|
||||
"endIndex": 17
|
||||
"startIndex": 11,
|
||||
"endIndex": 15
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
|
@ -13810,7 +13846,7 @@
|
|||
"parameterName": "opts",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 6,
|
||||
"endIndex": 12
|
||||
"endIndex": 10
|
||||
},
|
||||
"isOptional": true
|
||||
}
|
||||
|
@ -37739,24 +37775,11 @@
|
|||
"kind": "Content",
|
||||
"text": "components?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Partial",
|
||||
"canonicalReference": "!Partial:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<"
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLEditorComponents",
|
||||
"canonicalReference": "@tldraw/editor!TLEditorComponents:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
|
@ -37768,7 +37791,7 @@
|
|||
"name": "components",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 5
|
||||
"endIndex": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -38050,9 +38073,14 @@
|
|||
"kind": "Content",
|
||||
"text": "export type TLEditorComponents = "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Partial",
|
||||
"canonicalReference": "!Partial:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n [K in keyof "
|
||||
"text": "<{\n [K in keyof "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -38070,34 +38098,16 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[K] | null;\n} & {\n ErrorFallback: "
|
||||
"text": "[K] | null;\n} & "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLErrorFallbackComponent",
|
||||
"canonicalReference": "@tldraw/editor!~TLErrorFallbackComponent:type"
|
||||
"text": "ErrorComponents",
|
||||
"canonicalReference": "@tldraw/editor!~ErrorComponents:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n ShapeErrorFallback: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShapeErrorFallbackComponent",
|
||||
"canonicalReference": "@tldraw/editor!~TLShapeErrorFallbackComponent:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n ShapeIndicatorErrorFallback: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShapeIndicatorErrorFallbackComponent",
|
||||
"canonicalReference": "@tldraw/editor!~TLShapeIndicatorErrorFallbackComponent:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n}"
|
||||
"text": ">"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -38109,7 +38119,7 @@
|
|||
"name": "TLEditorComponents",
|
||||
"typeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 12
|
||||
"endIndex": 9
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -42979,6 +42989,59 @@
|
|||
"endIndex": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TypeAlias",
|
||||
"canonicalReference": "@tldraw/editor!TLSvgOptions:type",
|
||||
"docComment": "/**\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export type TLSvgOptions = "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n bounds: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Box2d",
|
||||
"canonicalReference": "@tldraw/editor!Box2d:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n scale: number;\n background: boolean;\n padding: number;\n darkMode?: boolean;\n preserveAspectRatio: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "React.SVGAttributes",
|
||||
"canonicalReference": "@types/react!React.SVGAttributes:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<"
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "SVGSVGElement",
|
||||
"canonicalReference": "!SVGSVGElement:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">['preserveAspectRatio'];\n}"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/editor/types/misc-types.ts",
|
||||
"releaseTag": "Public",
|
||||
"name": "TLSvgOptions",
|
||||
"typeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TypeAlias",
|
||||
"canonicalReference": "@tldraw/editor!TLTickEvent:type",
|
||||
|
|
|
@ -326,6 +326,11 @@ input,
|
|||
fill: var(--color-brush-fill);
|
||||
}
|
||||
|
||||
.tl-screenshot-brush {
|
||||
stroke: var(--color-text-0);
|
||||
fill: none;
|
||||
}
|
||||
|
||||
/* -------------------- Scribble -------------------- */
|
||||
|
||||
.tl-scribble {
|
||||
|
@ -1558,7 +1563,6 @@ it from receiving any pointer events or affecting the cursor. */
|
|||
overflow: auto;
|
||||
font-size: 12px;
|
||||
max-height: 320px;
|
||||
|
||||
}
|
||||
|
||||
.tl-error-boundary__content button {
|
||||
|
|
|
@ -241,7 +241,7 @@ export {
|
|||
type TLHistoryEntry,
|
||||
type TLHistoryMark,
|
||||
} from './lib/editor/types/history-types'
|
||||
export { type RequiredKeys } from './lib/editor/types/misc-types'
|
||||
export { type RequiredKeys, type TLSvgOptions } from './lib/editor/types/misc-types'
|
||||
export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types'
|
||||
export { useContainer } from './lib/hooks/useContainer'
|
||||
export { getCursor } from './lib/hooks/useCursor'
|
||||
|
@ -260,6 +260,7 @@ export {
|
|||
Box2d,
|
||||
ROTATE_CORNER_TO_SELECTION_CORNER,
|
||||
rotateSelectionHandle,
|
||||
type BoxLike,
|
||||
type RotateCorner,
|
||||
type SelectionCorner,
|
||||
type SelectionEdge,
|
||||
|
|
|
@ -86,7 +86,7 @@ export interface TldrawEditorBaseProps {
|
|||
/**
|
||||
* Overrides for the editor's components, such as handles, collaborator cursors, etc.
|
||||
*/
|
||||
components?: Partial<TLEditorComponents>
|
||||
components?: TLEditorComponents
|
||||
|
||||
/**
|
||||
* Called when the editor has mounted.
|
||||
|
|
|
@ -184,7 +184,7 @@ function ZoomBrushWrapper() {
|
|||
|
||||
if (!(ZoomBrush && zoomBrush)) return null
|
||||
|
||||
return <ZoomBrush className="tl-user-brush" brush={zoomBrush} />
|
||||
return <ZoomBrush className="tl-user-brush tl-zoom-brush" brush={zoomBrush} />
|
||||
}
|
||||
|
||||
function SnapLinesWrapper() {
|
||||
|
|
|
@ -12,7 +12,7 @@ export type TLBrushComponent = ComponentType<{
|
|||
}>
|
||||
|
||||
/** @public */
|
||||
export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
|
||||
export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity, className }) => {
|
||||
const rSvg = useRef<SVGSVGElement>(null)
|
||||
useTransform(rSvg, brush.x, brush.y)
|
||||
|
||||
|
@ -27,7 +27,7 @@ export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
|
|||
<rect width={w} height={h} fill="none" stroke={color} opacity={0.1} />
|
||||
</g>
|
||||
) : (
|
||||
<rect className="tl-brush tl-brush__default" width={w} height={h} />
|
||||
<rect className={`tl-brush tl-brush__default ${className}`} width={w} height={h} />
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
|
|
|
@ -129,7 +129,7 @@ import {
|
|||
} from './types/event-types'
|
||||
import { TLExternalAssetContent, TLExternalContent } from './types/external-content'
|
||||
import { TLCommandHistoryOptions } from './types/history-types'
|
||||
import { OptionalKeys, RequiredKeys } from './types/misc-types'
|
||||
import { OptionalKeys, RequiredKeys, TLSvgOptions } from './types/misc-types'
|
||||
import { TLResizeHandle } from './types/selection-types'
|
||||
|
||||
/** @public */
|
||||
|
@ -1127,13 +1127,14 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
this.root.transition(id, info)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* The current selected tool.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@computed getCurrentTool(): StateNode | undefined {
|
||||
return this.root.getCurrent()
|
||||
@computed getCurrentTool(): StateNode {
|
||||
return this.root.getCurrent()!
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1175,17 +1176,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
getStateDescendant(path: string): StateNode | undefined {
|
||||
getStateDescendant<T extends StateNode>(path: string): T | undefined {
|
||||
const ids = path.split('.').reverse()
|
||||
let state = this.root as StateNode
|
||||
while (ids.length > 0) {
|
||||
const id = ids.pop()
|
||||
if (!id) return state
|
||||
if (!id) return state as T
|
||||
const childState = state.children?.[id]
|
||||
if (!childState) return undefined
|
||||
state = childState
|
||||
}
|
||||
return state
|
||||
return state as T
|
||||
}
|
||||
|
||||
/* ---------------- Document Settings --------------- */
|
||||
|
@ -7550,6 +7551,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// Otherwise, just return an empty map.
|
||||
const currentTool = this.root.getCurrent()!
|
||||
const styles = new SharedStyleMap()
|
||||
|
||||
if (!currentTool) return styles
|
||||
|
||||
if (currentTool.shapeType) {
|
||||
for (const style of this.styleProps[currentTool.shapeType].keys()) {
|
||||
styles.applyValue(style, this.getStyleForNextShape(style))
|
||||
|
@ -8370,16 +8374,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
async getSvg(
|
||||
shapes: TLShapeId[] | TLShape[],
|
||||
opts = {} as Partial<{
|
||||
scale: number
|
||||
background: boolean
|
||||
padding: number
|
||||
darkMode?: boolean
|
||||
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
|
||||
}>
|
||||
) {
|
||||
async getSvg(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) {
|
||||
const ids =
|
||||
typeof shapes[0] === 'string'
|
||||
? (shapes as TLShapeId[])
|
||||
|
@ -8406,12 +8401,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
// --- Common bounding box of all shapes
|
||||
let bbox = null
|
||||
for (const { maskedPageBounds } of renderingShapes) {
|
||||
if (!maskedPageBounds) continue
|
||||
if (bbox) {
|
||||
bbox.union(maskedPageBounds)
|
||||
} else {
|
||||
bbox = maskedPageBounds.clone()
|
||||
if (opts.bounds) {
|
||||
bbox = opts.bounds
|
||||
} else {
|
||||
for (const { maskedPageBounds } of renderingShapes) {
|
||||
if (!maskedPageBounds) continue
|
||||
if (bbox) {
|
||||
bbox.union(maskedPageBounds)
|
||||
} else {
|
||||
bbox = maskedPageBounds.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,10 +12,8 @@ export class RootState extends StateNode {
|
|||
case 'KeyZ': {
|
||||
if (!(info.shiftKey || info.ctrlKey)) {
|
||||
const currentTool = this.getCurrent()
|
||||
if (currentTool && currentTool.getCurrent()?.id === 'idle') {
|
||||
if (this.children!['zoom']) {
|
||||
this.editor.setCurrentTool('zoom', { ...info, onInteractionEnd: currentTool.id })
|
||||
}
|
||||
if (currentTool && currentTool.getCurrent()?.id === 'idle' && this.children!['zoom']) {
|
||||
this.editor.setCurrentTool('zoom', { ...info, onInteractionEnd: currentTool.id })
|
||||
}
|
||||
}
|
||||
break
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
import { Box2d } from '../../primitives/Box2d'
|
||||
|
||||
/** @public */
|
||||
export type RequiredKeys<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>
|
||||
/** @public */
|
||||
export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
||||
|
||||
/** @public */
|
||||
export type TLSvgOptions = {
|
||||
bounds: Box2d
|
||||
scale: number
|
||||
background: boolean
|
||||
padding: number
|
||||
darkMode?: boolean
|
||||
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ export function useDocumentEvents() {
|
|||
if (
|
||||
e.altKey &&
|
||||
// todo: When should we allow the alt key to be used? Perhaps states should declare which keys matter to them?
|
||||
(editor.isIn('zoom') || !editor.root.getPath().endsWith('.idle')) &&
|
||||
(editor.isIn('zoom') || !editor.getPath().endsWith('.idle')) &&
|
||||
!isFocusingInput()
|
||||
) {
|
||||
// On windows the alt key opens the menu bar.
|
||||
|
|
|
@ -74,19 +74,24 @@ export interface BaseEditorComponents {
|
|||
InFrontOfTheCanvas: TLInFrontOfTheCanvas
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type TLEditorComponents = {
|
||||
[K in keyof BaseEditorComponents]: BaseEditorComponents[K] | null
|
||||
} & {
|
||||
// These will always have defaults
|
||||
type ErrorComponents = {
|
||||
ErrorFallback: TLErrorFallbackComponent
|
||||
ShapeErrorFallback: TLShapeErrorFallbackComponent
|
||||
ShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent
|
||||
}
|
||||
|
||||
const EditorComponentsContext = createContext({} as TLEditorComponents)
|
||||
/** @public */
|
||||
export type TLEditorComponents = Partial<
|
||||
{
|
||||
[K in keyof BaseEditorComponents]: BaseEditorComponents[K] | null
|
||||
} & ErrorComponents
|
||||
>
|
||||
|
||||
const EditorComponentsContext = createContext({} as TLEditorComponents & ErrorComponents)
|
||||
|
||||
type ComponentsContextProviderProps = {
|
||||
overrides?: Partial<TLEditorComponents>
|
||||
overrides?: TLEditorComponents
|
||||
children: any
|
||||
}
|
||||
|
||||
|
@ -99,6 +104,7 @@ export function EditorComponentsProvider({ overrides, children }: ComponentsCont
|
|||
SvgDefs: DefaultSvgDefs,
|
||||
Brush: DefaultBrush,
|
||||
ZoomBrush: DefaultBrush,
|
||||
ScreenshotBrush: DefaultBrush,
|
||||
CollaboratorBrush: DefaultBrush,
|
||||
Cursor: DefaultCursor,
|
||||
CollaboratorCursor: DefaultCursor,
|
||||
|
|
|
@ -2,6 +2,9 @@ import { Box2dModel } from '@tldraw/tlschema'
|
|||
import { Vec2d, VecLike } from './Vec2d'
|
||||
import { PI, PI2, toPrecision } from './utils'
|
||||
|
||||
/** @public */
|
||||
export type BoxLike = Box2dModel | Box2d
|
||||
|
||||
/** @public */
|
||||
export type SelectionEdge = 'top' | 'right' | 'bottom' | 'left'
|
||||
|
||||
|
|
|
@ -99,6 +99,7 @@ import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
|
|||
import { TLShapeUtilFlag } from '@tldraw/editor';
|
||||
import { TLStore } from '@tldraw/editor';
|
||||
import { TLStoreWithStatus } from '@tldraw/editor';
|
||||
import { TLSvgOptions } from '@tldraw/editor';
|
||||
import { TLTextShape } from '@tldraw/editor';
|
||||
import { TLUnknownShape } from '@tldraw/editor';
|
||||
import { TLVideoShape } from '@tldraw/editor';
|
||||
|
@ -286,6 +287,9 @@ export const ContextMenu: ({ children }: {
|
|||
children: any;
|
||||
}) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export function copyAs(editor: Editor, ids: TLShapeId[], format?: TLCopyType, opts?: Partial<TLSvgOptions>): Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const DEFAULT_ACCEPTED_IMG_TYPE: string[];
|
||||
|
||||
|
@ -461,8 +465,11 @@ export type EventsProviderProps = {
|
|||
children: any;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function exportAs(editor: Editor, ids: TLShapeId[], format?: TLExportType, opts?: Partial<TLSvgOptions>): Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function findMenuItem(menu: TLUiMenuSchema, path: string[]): TLUiMenuChild;
|
||||
export function findMenuItem(menu: TLUiMenuSchema, path: string[]): TLUiCustomMenuItem | TLUiMenuGroup | TLUiMenuItem | TLUiSubMenu<string>;
|
||||
|
||||
// @public (undocumented)
|
||||
function Footer({ className, children }: {
|
||||
|
@ -906,7 +913,7 @@ export function menuCustom(id: string, opts?: Partial<{
|
|||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export function menuGroup(id: string, ...children: (false | null | TLUiMenuChild)[]): null | TLUiMenuGroup;
|
||||
export function menuGroup(id: string, ...children: (false | TLUiMenuChild)[]): null | TLUiMenuGroup;
|
||||
|
||||
// @public (undocumented)
|
||||
export function menuItem(actionItem: TLUiActionItem | TLUiToolItem, opts?: Partial<{
|
||||
|
@ -915,7 +922,7 @@ export function menuItem(actionItem: TLUiActionItem | TLUiToolItem, opts?: Parti
|
|||
}>): TLUiMenuItem;
|
||||
|
||||
// @public (undocumented)
|
||||
export function menuSubmenu(id: string, label: TLUiTranslationKey, ...children: (false | null | TLUiMenuChild)[]): null | TLUiSubMenu;
|
||||
export function menuSubmenu(id: string, label: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey, ...children: (false | TLUiMenuChild)[]): null | TLUiSubMenu;
|
||||
|
||||
// @public (undocumented)
|
||||
export class NoteShapeTool extends StateNode {
|
||||
|
@ -1020,7 +1027,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
export function OfflineIndicator(): JSX.Element;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function parseAndLoadDocument(editor: Editor, document: string, msg: (id: TLUiTranslationKey) => string, addToast: TLUiToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise<void>;
|
||||
export function parseAndLoadDocument(editor: Editor, document: string, msg: (id: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey) => string, addToast: TLUiToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function parseTldrawJsonFile({ json, schema, }: {
|
||||
|
@ -1081,7 +1088,7 @@ function SubContent({ alignOffset, sideOffset, children, }: {
|
|||
|
||||
// @public (undocumented)
|
||||
function SubTrigger({ label, 'data-testid': testId, 'data-direction': dataDirection, }: {
|
||||
label: TLUiTranslationKey;
|
||||
label: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey;
|
||||
'data-testid'?: string;
|
||||
'data-direction'?: 'left' | 'right';
|
||||
}): JSX.Element;
|
||||
|
@ -1211,17 +1218,7 @@ function Title({ className, children }: {
|
|||
}): JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export function Tldraw(props: TldrawEditorBaseProps & ({
|
||||
store: TLStore | TLStoreWithStatus;
|
||||
} | {
|
||||
store?: undefined;
|
||||
persistenceKey?: string;
|
||||
sessionId?: string;
|
||||
defaultName?: string;
|
||||
snapshot?: StoreSnapshot<TLRecord>;
|
||||
}) & TldrawUiProps & Partial<TLExternalContentProps> & {
|
||||
assetUrls?: RecursivePartial<TLEditorAssetUrls>;
|
||||
}): JSX.Element;
|
||||
export function Tldraw(props: TldrawProps): JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const TLDRAW_FILE_EXTENSION: ".tldr";
|
||||
|
@ -1257,6 +1254,17 @@ export const TldrawHandles: TLHandlesComponent;
|
|||
// @public (undocumented)
|
||||
export const TldrawHoveredShapeIndicator: TLHoveredShapeIndicatorComponent;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TldrawProps = TldrawEditorBaseProps & ({
|
||||
store: TLStore | TLStoreWithStatus;
|
||||
} | {
|
||||
store?: undefined;
|
||||
persistenceKey?: string;
|
||||
sessionId?: string;
|
||||
defaultName?: string;
|
||||
snapshot?: StoreSnapshot<TLRecord>;
|
||||
}) & TldrawUiProps & Partial<TLExternalContentProps>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const TldrawScribble: TLScribbleComponent;
|
||||
|
||||
|
@ -1271,6 +1279,7 @@ export const TldrawUi: React_2.NamedExoticComponent<TldrawUiProps>;
|
|||
|
||||
// @public
|
||||
export interface TldrawUiBaseProps {
|
||||
assetUrls?: TLUiAssetUrlOverrides;
|
||||
children?: ReactNode;
|
||||
hideUi?: boolean;
|
||||
renderDebugMenuItems?: () => React_2.ReactNode;
|
||||
|
@ -1295,27 +1304,27 @@ export interface TldrawUiContextProviderProps {
|
|||
export type TldrawUiProps = TldrawUiBaseProps & TldrawUiContextProviderProps;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface TLUiActionItem {
|
||||
export interface TLUiActionItem<TransationKey extends string = string, IconType extends string = string> {
|
||||
// (undocumented)
|
||||
checkbox?: boolean;
|
||||
// (undocumented)
|
||||
contextMenuLabel?: TLUiTranslationKey;
|
||||
contextMenuLabel?: TransationKey;
|
||||
// (undocumented)
|
||||
icon?: TLUiIconType;
|
||||
icon?: IconType;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
kbd?: string;
|
||||
// (undocumented)
|
||||
label?: TLUiTranslationKey;
|
||||
label?: TransationKey;
|
||||
// (undocumented)
|
||||
menuLabel?: TLUiTranslationKey;
|
||||
menuLabel?: TransationKey;
|
||||
// (undocumented)
|
||||
onSelect: (source: TLUiEventSource) => Promise<void> | void;
|
||||
// (undocumented)
|
||||
readonlyOk: boolean;
|
||||
// (undocumented)
|
||||
shortcutsLabel?: TLUiTranslationKey;
|
||||
shortcutsLabel?: TransationKey;
|
||||
// (undocumented)
|
||||
title?: string;
|
||||
}
|
||||
|
@ -1326,14 +1335,17 @@ export type TLUiActionsContextType = Record<string, TLUiActionItem>;
|
|||
// @public (undocumented)
|
||||
export type TLUiActionsMenuSchemaContextType = TLUiMenuSchema;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLUiAssetUrlOverrides = RecursivePartial<TLUiAssetUrls>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface TLUiButtonProps extends React_3.HTMLAttributes<HTMLButtonElement> {
|
||||
// (undocumented)
|
||||
disabled?: boolean;
|
||||
// (undocumented)
|
||||
icon?: TLUiIconType;
|
||||
icon?: Exclude<string, TLUiIconType> | TLUiIconType;
|
||||
// (undocumented)
|
||||
iconLeft?: TLUiIconType;
|
||||
iconLeft?: Exclude<string, TLUiIconType> | TLUiIconType;
|
||||
// (undocumented)
|
||||
invertIcon?: boolean;
|
||||
// (undocumented)
|
||||
|
@ -1341,7 +1353,7 @@ export interface TLUiButtonProps extends React_3.HTMLAttributes<HTMLButtonElemen
|
|||
// (undocumented)
|
||||
kbd?: string;
|
||||
// (undocumented)
|
||||
label?: TLUiTranslationKey;
|
||||
label?: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey;
|
||||
// (undocumented)
|
||||
loading?: boolean;
|
||||
// (undocumented)
|
||||
|
@ -1419,7 +1431,7 @@ export interface TLUiIconProps extends React.HTMLProps<HTMLDivElement> {
|
|||
// (undocumented)
|
||||
crossOrigin?: 'anonymous' | 'use-credentials';
|
||||
// (undocumented)
|
||||
icon: TLUiIconType;
|
||||
icon: Exclude<string, TLUiIconType> | TLUiIconType;
|
||||
// (undocumented)
|
||||
invertIcon?: boolean;
|
||||
// (undocumented)
|
||||
|
@ -1444,11 +1456,11 @@ export interface TLUiInputProps {
|
|||
// (undocumented)
|
||||
disabled?: boolean;
|
||||
// (undocumented)
|
||||
icon?: TLUiIconType;
|
||||
icon?: Exclude<string, TLUiIconType> | TLUiIconType;
|
||||
// (undocumented)
|
||||
iconLeft?: TLUiIconType;
|
||||
iconLeft?: Exclude<string, TLUiIconType> | TLUiIconType;
|
||||
// (undocumented)
|
||||
label?: TLUiTranslationKey;
|
||||
label?: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey;
|
||||
// (undocumented)
|
||||
onBlur?: (value: string) => void;
|
||||
// (undocumented)
|
||||
|
@ -1477,7 +1489,7 @@ export type TLUiKeyboardShortcutsSchemaProviderProps = {
|
|||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLUiMenuChild = TLUiCustomMenuItem | TLUiMenuGroup | TLUiMenuItem | TLUiSubMenu;
|
||||
export type TLUiMenuChild<TranslationKey extends string = string> = null | TLUiCustomMenuItem | TLUiMenuGroup | TLUiMenuItem | TLUiSubMenu<TranslationKey>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLUiMenuGroup = {
|
||||
|
@ -1518,32 +1530,23 @@ export type TLUiMenuSchemaProviderProps = {
|
|||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface TLUiOverrides {
|
||||
// (undocumented)
|
||||
actions?: WithDefaultHelpers<NonNullable<ActionsProviderProps['overrides']>>;
|
||||
// (undocumented)
|
||||
actionsMenu?: WithDefaultHelpers<NonNullable<ActionsMenuSchemaProviderProps['overrides']>>;
|
||||
// (undocumented)
|
||||
contextMenu?: WithDefaultHelpers<NonNullable<TLUiContextMenuSchemaProviderProps['overrides']>>;
|
||||
// (undocumented)
|
||||
helpMenu?: WithDefaultHelpers<NonNullable<TLUiHelpMenuSchemaProviderProps['overrides']>>;
|
||||
// (undocumented)
|
||||
keyboardShortcutsMenu?: WithDefaultHelpers<NonNullable<TLUiKeyboardShortcutsSchemaProviderProps['overrides']>>;
|
||||
// (undocumented)
|
||||
menu?: WithDefaultHelpers<NonNullable<TLUiMenuSchemaProviderProps['overrides']>>;
|
||||
// (undocumented)
|
||||
toolbar?: WithDefaultHelpers<NonNullable<TLUiToolbarSchemaProviderProps['overrides']>>;
|
||||
// (undocumented)
|
||||
tools?: WithDefaultHelpers<NonNullable<TLUiToolsProviderProps['overrides']>>;
|
||||
// (undocumented)
|
||||
translations?: TLUiTranslationProviderProps['overrides'];
|
||||
}
|
||||
export type TLUiOverrides = Partial<{
|
||||
actionsMenu: WithDefaultHelpers<NonNullable<ActionsMenuSchemaProviderProps['overrides']>>;
|
||||
actions: WithDefaultHelpers<NonNullable<ActionsProviderProps['overrides']>>;
|
||||
contextMenu: WithDefaultHelpers<NonNullable<TLUiContextMenuSchemaProviderProps['overrides']>>;
|
||||
helpMenu: WithDefaultHelpers<NonNullable<TLUiHelpMenuSchemaProviderProps['overrides']>>;
|
||||
menu: WithDefaultHelpers<NonNullable<TLUiMenuSchemaProviderProps['overrides']>>;
|
||||
toolbar: WithDefaultHelpers<NonNullable<TLUiToolbarSchemaProviderProps['overrides']>>;
|
||||
keyboardShortcutsMenu: WithDefaultHelpers<NonNullable<TLUiKeyboardShortcutsSchemaProviderProps['overrides']>>;
|
||||
tools: WithDefaultHelpers<NonNullable<TLUiToolsProviderProps['overrides']>>;
|
||||
translations: TLUiTranslationProviderProps['overrides'];
|
||||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLUiSubMenu = {
|
||||
export type TLUiSubMenu<TranslationKey extends string = string> = {
|
||||
id: string;
|
||||
type: 'submenu';
|
||||
label: TLUiTranslationKey;
|
||||
label: TranslationKey;
|
||||
disabled: boolean;
|
||||
readonlyOk: boolean;
|
||||
children: TLUiMenuChild[];
|
||||
|
@ -1599,15 +1602,15 @@ export type TLUiToolbarItem = {
|
|||
export type TLUiToolbarSchemaContextType = TLUiToolbarItem[];
|
||||
|
||||
// @public (undocumented)
|
||||
export interface TLUiToolItem {
|
||||
export interface TLUiToolItem<TranslationKey extends string = string, IconType extends string = string> {
|
||||
// (undocumented)
|
||||
icon: TLUiIconType;
|
||||
icon: IconType;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
kbd?: string;
|
||||
// (undocumented)
|
||||
label: TLUiTranslationKey;
|
||||
label: TranslationKey;
|
||||
// (undocumented)
|
||||
meta?: {
|
||||
[key: string]: any;
|
||||
|
@ -1617,7 +1620,7 @@ export interface TLUiToolItem {
|
|||
// (undocumented)
|
||||
readonlyOk: boolean;
|
||||
// (undocumented)
|
||||
shortcutsLabel?: TLUiTranslationKey;
|
||||
shortcutsLabel?: TranslationKey;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -1681,7 +1684,7 @@ export function useCanUndo(): boolean;
|
|||
export function useContextMenuSchema(): TLUiMenuSchema;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useCopyAs(): (ids?: TLShapeId[], format?: TLCopyType) => void;
|
||||
export function useCopyAs(): (ids: TLShapeId[], format?: TLCopyType) => void;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useDefaultHelpers(): {
|
||||
|
@ -1696,7 +1699,7 @@ export function useDefaultHelpers(): {
|
|||
clearDialogs: () => void;
|
||||
removeDialog: (id: string) => string;
|
||||
updateDialog: (id: string, newDialogData: Partial<TLUiDialog>) => string;
|
||||
msg: (id: TLUiTranslationKey) => string;
|
||||
msg: (id: string) => string;
|
||||
isMobile: boolean;
|
||||
};
|
||||
|
||||
|
@ -1704,7 +1707,7 @@ export function useDefaultHelpers(): {
|
|||
export function useDialogs(): TLUiDialogsContextType;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useExportAs(): (ids?: TLShapeId[], format?: TLExportType) => Promise<void>;
|
||||
export function useExportAs(): (ids: TLShapeId[], format?: TLExportType) => void;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useHelpMenuSchema(): TLUiMenuSchema;
|
||||
|
@ -1747,7 +1750,7 @@ export function useToolbarSchema(): TLUiToolbarSchemaContextType;
|
|||
export function useTools(): TLUiToolsContextType;
|
||||
|
||||
// @public
|
||||
export function useTranslation(): (id: TLUiTranslationKey) => string;
|
||||
export function useTranslation(): (id: Exclude<string, TLUiTranslationKey> | string) => string;
|
||||
|
||||
// @public (undocumented)
|
||||
export function useUiEvents(): TLUiEventContextType;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,7 +2,7 @@
|
|||
|
||||
// eslint-disable-next-line local/no-export-star
|
||||
export * from '@tldraw/editor'
|
||||
export { Tldraw } from './lib/Tldraw'
|
||||
export { Tldraw, type TldrawProps } from './lib/Tldraw'
|
||||
export { TldrawCropHandles, type TldrawCropHandlesProps } from './lib/canvas/TldrawCropHandles'
|
||||
export { TldrawHandles } from './lib/canvas/TldrawHandles'
|
||||
export { TldrawHoveredShapeIndicator } from './lib/canvas/TldrawHoveredShapeIndicator'
|
||||
|
@ -43,7 +43,7 @@ export {
|
|||
TldrawUiContextProvider,
|
||||
type TldrawUiContextProviderProps,
|
||||
} from './lib/ui/TldrawUiContextProvider'
|
||||
export { setDefaultUiAssetUrls } from './lib/ui/assetUrls'
|
||||
export { setDefaultUiAssetUrls, type TLUiAssetUrlOverrides } from './lib/ui/assetUrls'
|
||||
export { ContextMenu, type TLUiContextMenuProps } from './lib/ui/components/ContextMenu'
|
||||
export { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator'
|
||||
export { Spinner } from './lib/ui/components/Spinner'
|
||||
|
@ -142,16 +142,22 @@ export {
|
|||
} from './lib/ui/hooks/useTranslation/useTranslation'
|
||||
export { type TLUiIconType } from './lib/ui/icon-types'
|
||||
export { useDefaultHelpers, type TLUiOverrides } from './lib/ui/overrides'
|
||||
export { setDefaultEditorAssetUrls } from './lib/utils/assetUrls'
|
||||
export {
|
||||
DEFAULT_ACCEPTED_IMG_TYPE,
|
||||
DEFAULT_ACCEPTED_VID_TYPE,
|
||||
containBoxSize,
|
||||
getResizedImageDataUrl,
|
||||
isGifAnimated,
|
||||
} from './lib/utils/assets'
|
||||
export { buildFromV1Document, type LegacyTldrawDocument } from './lib/utils/buildFromV1Document'
|
||||
export { getEmbedInfo } from './lib/utils/embeds'
|
||||
} from './lib/utils/assets/assets'
|
||||
export { getEmbedInfo } from './lib/utils/embeds/embeds'
|
||||
export { copyAs } from './lib/utils/export/copyAs'
|
||||
export { exportAs } from './lib/utils/export/exportAs'
|
||||
export { setDefaultEditorAssetUrls } from './lib/utils/static-assets/assetUrls'
|
||||
export { truncateStringWithEllipsis } from './lib/utils/text/text'
|
||||
export {
|
||||
buildFromV1Document,
|
||||
type LegacyTldrawDocument,
|
||||
} from './lib/utils/tldr/buildFromV1Document'
|
||||
export {
|
||||
TLDRAW_FILE_EXTENSION,
|
||||
parseAndLoadDocument,
|
||||
|
@ -159,8 +165,7 @@ export {
|
|||
serializeTldrawJson,
|
||||
serializeTldrawJsonBlob,
|
||||
type TldrawFile,
|
||||
} from './lib/utils/file'
|
||||
export { truncateStringWithEllipsis } from './lib/utils/text'
|
||||
} from './lib/utils/tldr/file'
|
||||
export { Dialog, DropdownMenu }
|
||||
import * as Dialog from './lib/ui/components/primitives/Dialog'
|
||||
import * as DropdownMenu from './lib/ui/components/primitives/DropdownMenu'
|
||||
|
|
|
@ -3,7 +3,6 @@ import {
|
|||
Editor,
|
||||
ErrorScreen,
|
||||
LoadingScreen,
|
||||
RecursivePartial,
|
||||
StoreSnapshot,
|
||||
TLOnMountHandler,
|
||||
TLRecord,
|
||||
|
@ -31,35 +30,31 @@ import { registerDefaultSideEffects } from './defaultSideEffects'
|
|||
import { defaultTools } from './defaultTools'
|
||||
import { TldrawUi, TldrawUiProps } from './ui/TldrawUi'
|
||||
import { ContextMenu } from './ui/components/ContextMenu'
|
||||
import { TLEditorAssetUrls, useDefaultEditorAssetsWithOverrides } from './utils/assetUrls'
|
||||
import { usePreloadAssets } from './utils/usePreloadAssets'
|
||||
import { usePreloadAssets } from './ui/hooks/usePreloadAssets'
|
||||
import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls'
|
||||
|
||||
/** @public */
|
||||
export function Tldraw(
|
||||
props: TldrawEditorBaseProps &
|
||||
(
|
||||
| {
|
||||
store: TLStore | TLStoreWithStatus
|
||||
}
|
||||
| {
|
||||
store?: undefined
|
||||
persistenceKey?: string
|
||||
sessionId?: string
|
||||
defaultName?: string
|
||||
/**
|
||||
* A snapshot to load for the store's initial data / schema.
|
||||
*/
|
||||
snapshot?: StoreSnapshot<TLRecord>
|
||||
}
|
||||
) &
|
||||
TldrawUiProps &
|
||||
Partial<TLExternalContentProps> & {
|
||||
/**
|
||||
* Urls for the editor to find fonts and other assets.
|
||||
*/
|
||||
assetUrls?: RecursivePartial<TLEditorAssetUrls>
|
||||
}
|
||||
) {
|
||||
export type TldrawProps = TldrawEditorBaseProps &
|
||||
(
|
||||
| {
|
||||
store: TLStore | TLStoreWithStatus
|
||||
}
|
||||
| {
|
||||
store?: undefined
|
||||
persistenceKey?: string
|
||||
sessionId?: string
|
||||
defaultName?: string
|
||||
/**
|
||||
* A snapshot to load for the store's initial data / schema.
|
||||
*/
|
||||
snapshot?: StoreSnapshot<TLRecord>
|
||||
}
|
||||
) &
|
||||
TldrawUiProps &
|
||||
Partial<TLExternalContentProps>
|
||||
|
||||
/** @public */
|
||||
export function Tldraw(props: TldrawProps) {
|
||||
const {
|
||||
children,
|
||||
maxImageDimension,
|
||||
|
|
|
@ -17,9 +17,9 @@ import {
|
|||
getHashForString,
|
||||
} from '@tldraw/editor'
|
||||
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from './shapes/shared/default-shape-constants'
|
||||
import { containBoxSize, getResizedImageDataUrl, isGifAnimated } from './utils/assets'
|
||||
import { getEmbedInfo } from './utils/embeds'
|
||||
import { cleanupText, isRightToLeftLanguage, truncateStringWithEllipsis } from './utils/text'
|
||||
import { containBoxSize, getResizedImageDataUrl, isGifAnimated } from './utils/assets/assets'
|
||||
import { getEmbedInfo } from './utils/embeds/embeds'
|
||||
import { cleanupText, isRightToLeftLanguage, truncateStringWithEllipsis } from './utils/text/text'
|
||||
|
||||
/** @public */
|
||||
export type TLExternalContentProps = {
|
||||
|
|
|
@ -26,7 +26,7 @@ export class Idle extends StateNode {
|
|||
) {
|
||||
this.editor.setCurrentTool('select')
|
||||
this.editor.setEditingShape(onlySelectedShape.id)
|
||||
this.editor.root.getCurrent()!.transition('editing_shape', {
|
||||
this.editor.root.getCurrent()?.transition('editing_shape', {
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape: onlySelectedShape,
|
||||
|
|
|
@ -16,9 +16,9 @@ import {
|
|||
stopEventPropagation,
|
||||
toDomPrecision,
|
||||
} from '@tldraw/editor'
|
||||
import { getRotatedBoxShadow } from '../../utils/rotated-box-shadow'
|
||||
import { truncateStringWithEllipsis } from '../../utils/text'
|
||||
import { truncateStringWithEllipsis } from '../../utils/text/text'
|
||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||
import { getRotatedBoxShadow } from '../shared/rotated-box-shadow'
|
||||
|
||||
/** @public */
|
||||
export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
|
||||
|
|
|
@ -15,9 +15,9 @@ import {
|
|||
useValue,
|
||||
} from '@tldraw/editor'
|
||||
import { useMemo } from 'react'
|
||||
import { getEmbedInfo, getEmbedInfoUnsafely } from '../../utils/embeds'
|
||||
import { getRotatedBoxShadow } from '../../utils/rotated-box-shadow'
|
||||
import { getEmbedInfo, getEmbedInfoUnsafely } from '../../utils/embeds/embeds'
|
||||
import { resizeBox } from '../shared/resizeBox'
|
||||
import { getRotatedBoxShadow } from '../shared/rotated-box-shadow'
|
||||
|
||||
const getSandboxPermissions = (permissions: TLEmbedShapePermissions) => {
|
||||
return Object.entries(permissions)
|
||||
|
|
|
@ -23,7 +23,7 @@ export class Idle extends StateNode {
|
|||
) {
|
||||
this.editor.setCurrentTool('select')
|
||||
this.editor.setEditingShape(onlySelectedShape.id)
|
||||
this.editor.root.getCurrent()!.transition('editing_shape', {
|
||||
this.editor.root.getCurrent()?.transition('editing_shape', {
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape: onlySelectedShape,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Vec2d } from '@tldraw/editor'
|
||||
|
||||
export const ROTATING_BOX_SHADOWS = [
|
||||
const ROTATING_BOX_SHADOWS = [
|
||||
{
|
||||
offsetX: 0,
|
||||
offsetY: 2,
|
|
@ -32,7 +32,7 @@ export class Idle extends StateNode {
|
|||
) {
|
||||
this.editor.setCurrentTool('select')
|
||||
this.editor.setEditingShape(onlySelectedShape.id)
|
||||
this.editor.root.getCurrent()!.transition('editing_shape', {
|
||||
this.editor.root.getCurrent()?.transition('editing_shape', {
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape: onlySelectedShape,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { StateNode } from '@tldraw/editor'
|
||||
import { Erasing } from './children/Erasing'
|
||||
import { Idle } from './children/Idle'
|
||||
import { Pointing } from './children/Pointing'
|
||||
import { Erasing } from './childStates/Erasing'
|
||||
import { Idle } from './childStates/Idle'
|
||||
import { Pointing } from './childStates/Pointing'
|
||||
|
||||
/** @public */
|
||||
export class EraserTool extends StateNode {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { EASINGS, StateNode, TLClickEvent } from '@tldraw/editor'
|
||||
import { Dragging } from './children/Dragging'
|
||||
import { Idle } from './children/Idle'
|
||||
import { Pointing } from './children/Pointing'
|
||||
import { Dragging } from './childStates/Dragging'
|
||||
import { Idle } from './childStates/Idle'
|
||||
import { Pointing } from './childStates/Pointing'
|
||||
|
||||
/** @public */
|
||||
export class HandTool extends StateNode {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { StateNode } from '@tldraw/editor'
|
||||
import { Idle } from './children/Idle'
|
||||
import { Lasering } from './children/Lasering'
|
||||
import { Idle } from './childStates/Idle'
|
||||
import { Lasering } from './childStates/Lasering'
|
||||
|
||||
/** @public */
|
||||
export class LaserTool extends StateNode {
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
import { StateNode } from '@tldraw/editor'
|
||||
import { Brushing } from './children/Brushing'
|
||||
import { Crop } from './children/Crop/Crop'
|
||||
import { Cropping } from './children/Cropping'
|
||||
import { DraggingHandle } from './children/DraggingHandle'
|
||||
import { EditingShape } from './children/EditingShape'
|
||||
import { Idle } from './children/Idle'
|
||||
import { PointingCanvas } from './children/PointingCanvas'
|
||||
import { PointingCropHandle } from './children/PointingCropHandle'
|
||||
import { PointingHandle } from './children/PointingHandle'
|
||||
import { PointingResizeHandle } from './children/PointingResizeHandle'
|
||||
import { PointingRotateHandle } from './children/PointingRotateHandle'
|
||||
import { PointingSelection } from './children/PointingSelection'
|
||||
import { PointingShape } from './children/PointingShape'
|
||||
import { Resizing } from './children/Resizing'
|
||||
import { Rotating } from './children/Rotating'
|
||||
import { ScribbleBrushing } from './children/ScribbleBrushing'
|
||||
import { Translating } from './children/Translating'
|
||||
import { Brushing } from './childStates/Brushing'
|
||||
import { Crop } from './childStates/Crop/Crop'
|
||||
import { Cropping } from './childStates/Cropping'
|
||||
import { DraggingHandle } from './childStates/DraggingHandle'
|
||||
import { EditingShape } from './childStates/EditingShape'
|
||||
import { Idle } from './childStates/Idle'
|
||||
import { PointingCanvas } from './childStates/PointingCanvas'
|
||||
import { PointingCropHandle } from './childStates/PointingCropHandle'
|
||||
import { PointingHandle } from './childStates/PointingHandle'
|
||||
import { PointingResizeHandle } from './childStates/PointingResizeHandle'
|
||||
import { PointingRotateHandle } from './childStates/PointingRotateHandle'
|
||||
import { PointingSelection } from './childStates/PointingSelection'
|
||||
import { PointingShape } from './childStates/PointingShape'
|
||||
import { Resizing } from './childStates/Resizing'
|
||||
import { Rotating } from './childStates/Rotating'
|
||||
import { ScribbleBrushing } from './childStates/ScribbleBrushing'
|
||||
import { Translating } from './childStates/Translating'
|
||||
|
||||
/** @public */
|
||||
export class SelectTool extends StateNode {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { StateNode, TLInterruptEvent, TLKeyboardEvent, TLPointerEventInfo } from '@tldraw/editor'
|
||||
import { Idle } from './children/Idle'
|
||||
import { Pointing } from './children/Pointing'
|
||||
import { ZoomBrushing } from './children/ZoomBrushing'
|
||||
import { Idle } from './childStates/Idle'
|
||||
import { Pointing } from './childStates/Pointing'
|
||||
import { ZoomBrushing } from './childStates/ZoomBrushing'
|
||||
|
||||
/** @public */
|
||||
export class ZoomTool extends StateNode {
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useEditor, useValue } from '@tldraw/editor'
|
|||
import classNames from 'classnames'
|
||||
import React, { ReactNode } from 'react'
|
||||
import { TldrawUiContextProvider, TldrawUiContextProviderProps } from './TldrawUiContextProvider'
|
||||
import { TLUiAssetUrlOverrides } from './assetUrls'
|
||||
import { BackToContent } from './components/BackToContent'
|
||||
import { DebugPanel } from './components/DebugPanel'
|
||||
import { Dialogs } from './components/Dialogs'
|
||||
|
@ -61,6 +62,9 @@ export interface TldrawUiBaseProps {
|
|||
* Additional items to add to the debug menu (will be deprecated)
|
||||
*/
|
||||
renderDebugMenuItems?: () => React.ReactNode
|
||||
|
||||
/** Asset URL override. */
|
||||
assetUrls?: TLUiAssetUrlOverrides
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { EMBED_DEFINITIONS, LANGUAGES, RecursivePartial } from '@tldraw/editor'
|
||||
import { version } from '../ui/version'
|
||||
import { TLEditorAssetUrls, defaultEditorAssetUrls } from '../utils/assetUrls'
|
||||
import { TLEditorAssetUrls, defaultEditorAssetUrls } from '../utils/static-assets/assetUrls'
|
||||
import { TLUiIconType, iconTypes } from './icon-types'
|
||||
|
||||
export type TLUiAssetUrls = TLEditorAssetUrls & {
|
||||
icons: Record<TLUiIconType, string>
|
||||
icons: Record<TLUiIconType | Exclude<string, TLUiIconType>, string>
|
||||
translations: Record<(typeof LANGUAGES)[number]['locale'], string>
|
||||
embedIcons: Record<(typeof EMBED_DEFINITIONS)[number]['type'], string>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type TLUiAssetUrlOverrides = RecursivePartial<TLUiAssetUrls>
|
||||
|
||||
export let defaultUiAssetUrls: TLUiAssetUrls = {
|
||||
...defaultEditorAssetUrls,
|
||||
icons: Object.fromEntries(
|
||||
|
|
|
@ -16,6 +16,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
|
|||
const isReadonly = useReadonly()
|
||||
|
||||
function getActionMenuItem(item: TLUiMenuChild) {
|
||||
if (!item) return null
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
||||
switch (item.type) {
|
||||
|
|
|
@ -7,7 +7,9 @@ import { useBreakpoint } from '../hooks/useBreakpoint'
|
|||
import { useContextMenuSchema } from '../hooks/useContextMenuSchema'
|
||||
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
|
||||
import { useReadonly } from '../hooks/useReadonly'
|
||||
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
import { TLUiIconType } from '../icon-types'
|
||||
import { MoveToPageMenu } from './MoveToPageMenu'
|
||||
import { Button } from './primitives/Button'
|
||||
import { Icon } from './primitives/Icon'
|
||||
|
@ -116,6 +118,7 @@ const ContextMenuContent = forwardRef(function ContextMenuContent() {
|
|||
parent: TLUiMenuChild | null,
|
||||
depth: number
|
||||
) {
|
||||
if (!item) return null
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
||||
switch (item.type) {
|
||||
|
@ -147,7 +150,7 @@ const ContextMenuContent = forwardRef(function ContextMenuContent() {
|
|||
<_ContextMenu.SubTrigger dir="ltr" disabled={item.disabled} asChild>
|
||||
<Button
|
||||
type="menu"
|
||||
label={item.label}
|
||||
label={item.label as TLUiTranslationKey}
|
||||
data-testid={`menu-item.${item.id}`}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
|
@ -165,7 +168,7 @@ const ContextMenuContent = forwardRef(function ContextMenuContent() {
|
|||
|
||||
const { id, checkbox, contextMenuLabel, label, onSelect, kbd, icon } = item.actionItem
|
||||
const labelToUse = contextMenuLabel ?? label
|
||||
const labelStr = labelToUse ? msg(labelToUse) : undefined
|
||||
const labelStr = labelToUse ? msg(labelToUse as TLUiTranslationKey) : undefined
|
||||
|
||||
if (checkbox) {
|
||||
// Item is in a checkbox group
|
||||
|
@ -199,9 +202,9 @@ const ContextMenuContent = forwardRef(function ContextMenuContent() {
|
|||
type="menu"
|
||||
data-testid={`menu-item.${id}`}
|
||||
kbd={kbd}
|
||||
label={labelToUse}
|
||||
label={labelToUse as TLUiTranslationKey}
|
||||
disabled={item.disabled}
|
||||
iconLeft={breakpoint < 3 && depth > 2 ? icon : undefined}
|
||||
iconLeft={breakpoint < 3 && depth > 2 ? (icon as TLUiIconType) : undefined}
|
||||
onClick={() => {
|
||||
if (disableClicks) {
|
||||
setDisableClicks(false)
|
||||
|
|
|
@ -65,7 +65,7 @@ export const DebugPanel = React.memo(function DebugPanel({
|
|||
|
||||
const CurrentState = track(function CurrentState() {
|
||||
const editor = useEditor()
|
||||
return <div className="tlui-debug-panel__current-state">{editor.root.getPath()}</div>
|
||||
return <div className="tlui-debug-panel__current-state">{editor.getPath()}</div>
|
||||
})
|
||||
|
||||
const ShapeCount = function ShapeCount() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { EMBED_DEFINITIONS, EmbedDefinition, track, useEditor } from '@tldraw/editor'
|
||||
import { useRef, useState } from 'react'
|
||||
import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds'
|
||||
import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds/embeds'
|
||||
import { useAssetUrls } from '../hooks/useAssetUrls'
|
||||
import { TLUiDialogProps } from '../hooks/useDialogsProvider'
|
||||
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||
|
|
|
@ -13,8 +13,8 @@ import { Button } from './primitives/Button'
|
|||
import * as M from './primitives/DropdownMenu'
|
||||
|
||||
interface HelpMenuLink {
|
||||
label: TLUiTranslationKey
|
||||
icon: TLUiIconType
|
||||
label: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
|
||||
icon: TLUiIconType | Exclude<string, TLUiIconType>
|
||||
url: string
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,7 @@ function HelpMenuContent() {
|
|||
const isReadonly = useReadonly()
|
||||
|
||||
function getHelpMenuItem(item: TLUiMenuChild) {
|
||||
if (!item) return null
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
||||
switch (item.type) {
|
||||
|
|
|
@ -12,6 +12,7 @@ export const KeyboardShortcutsDialog = () => {
|
|||
const shortcutsItems = useKeyboardShortcutsSchema()
|
||||
|
||||
function getKeyboardShortcutItem(item: TLUiMenuChild) {
|
||||
if (!item) return null
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
||||
switch (item.type) {
|
||||
|
@ -23,7 +24,7 @@ export const KeyboardShortcutsDialog = () => {
|
|||
</h2>
|
||||
<div className="tlui-shortcuts-dialog__group__content">
|
||||
{item.children
|
||||
.filter((item) => item.type === 'item' && item.actionItem.kbd)
|
||||
.filter((item) => item && item.type === 'item' && item.actionItem.kbd)
|
||||
.map(getKeyboardShortcutItem)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -45,6 +45,7 @@ function MenuContent() {
|
|||
parent: TLUiMenuChild | null,
|
||||
depth: number
|
||||
) {
|
||||
if (!item) return null
|
||||
switch (item.type) {
|
||||
case 'custom': {
|
||||
if (isReadonly && !item.readonlyOk) return null
|
||||
|
|
|
@ -11,9 +11,9 @@ import { StyleValuesForUi } from './styles'
|
|||
interface DoubleDropdownPickerProps<T extends string> {
|
||||
uiTypeA: string
|
||||
uiTypeB: string
|
||||
label: TLUiTranslationKey
|
||||
labelA: TLUiTranslationKey
|
||||
labelB: TLUiTranslationKey
|
||||
label: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
|
||||
labelA: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
|
||||
labelB: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
|
||||
itemsA: StyleValuesForUi<T>
|
||||
itemsB: StyleValuesForUi<T>
|
||||
styleA: StyleProp<T>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { StyleValuesForUi } from './styles'
|
|||
|
||||
interface DropdownPickerProps<T extends string> {
|
||||
id: string
|
||||
label?: TLUiTranslationKey
|
||||
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
|
||||
uiType: string
|
||||
style: StyleProp<T>
|
||||
value: SharedStyle<T>
|
||||
|
|
|
@ -11,10 +11,10 @@ import { Kbd } from './Kbd'
|
|||
export interface TLUiButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
loading?: boolean // TODO: loading spinner
|
||||
disabled?: boolean
|
||||
label?: TLUiTranslationKey
|
||||
icon?: TLUiIconType
|
||||
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
|
||||
icon?: TLUiIconType | Exclude<string, TLUiIconType>
|
||||
spinner?: boolean
|
||||
iconLeft?: TLUiIconType
|
||||
iconLeft?: TLUiIconType | Exclude<string, TLUiIconType>
|
||||
smallIcon?: boolean
|
||||
kbd?: string
|
||||
isChecked?: boolean
|
||||
|
|
|
@ -96,7 +96,7 @@ export function SubTrigger({
|
|||
'data-testid': testId,
|
||||
'data-direction': dataDirection,
|
||||
}: {
|
||||
label: TLUiTranslationKey
|
||||
label: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
|
||||
'data-testid'?: string
|
||||
'data-direction'?: 'left' | 'right'
|
||||
}) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { TLUiIconType } from '../../icon-types'
|
|||
|
||||
/** @public */
|
||||
export interface TLUiIconProps extends React.HTMLProps<HTMLDivElement> {
|
||||
icon: TLUiIconType
|
||||
icon: TLUiIconType | Exclude<string, TLUiIconType>
|
||||
small?: boolean
|
||||
color?: string
|
||||
children?: undefined
|
||||
|
@ -23,17 +23,21 @@ export const Icon = memo(function Icon({
|
|||
...props
|
||||
}: TLUiIconProps) {
|
||||
const assetUrls = useAssetUrls()
|
||||
const asset = assetUrls.icons[icon]
|
||||
const asset = assetUrls.icons[icon as TLUiIconType] ?? assetUrls.icons['question-mark-circle']
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!asset) {
|
||||
console.error(`Icon not found: ${icon}. Add it to the assetUrls.icons object.`)
|
||||
}
|
||||
|
||||
if (ref?.current) {
|
||||
// HACK: Fix for <https://linear.app/tldraw/issue/TLD-1700/dragging-around-with-the-handtool-makes-lots-of-requests-for-icons>
|
||||
// It seems that passing `WebkitMask` to react will cause a render on each call, no idea why... but this appears to be the fix.
|
||||
// @ts-ignore
|
||||
ref.current.style.webkitMask = `url(${asset}) center 100% / 100% no-repeat`
|
||||
}
|
||||
}, [ref, asset])
|
||||
}, [ref, asset, icon])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -9,9 +9,9 @@ import { Icon } from './Icon'
|
|||
/** @public */
|
||||
export interface TLUiInputProps {
|
||||
disabled?: boolean
|
||||
label?: TLUiTranslationKey
|
||||
icon?: TLUiIconType
|
||||
iconLeft?: TLUiIconType
|
||||
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
|
||||
icon?: TLUiIconType | Exclude<string, TLUiIconType>
|
||||
iconLeft?: TLUiIconType | Exclude<string, TLUiIconType>
|
||||
autofocus?: boolean
|
||||
autoselect?: boolean
|
||||
children?: any
|
||||
|
|
|
@ -13,7 +13,12 @@ import { TLUiToolItem } from './useTools'
|
|||
import { TLUiTranslationKey } from './useTranslation/TLUiTranslationKey'
|
||||
|
||||
/** @public */
|
||||
export type TLUiMenuChild = TLUiMenuItem | TLUiSubMenu | TLUiMenuGroup | TLUiCustomMenuItem
|
||||
export type TLUiMenuChild<TranslationKey extends string = string> =
|
||||
| TLUiMenuItem
|
||||
| TLUiSubMenu<TranslationKey>
|
||||
| TLUiMenuGroup
|
||||
| TLUiCustomMenuItem
|
||||
| null
|
||||
|
||||
/** @public */
|
||||
export type TLUiCustomMenuItem = {
|
||||
|
@ -44,10 +49,10 @@ export type TLUiMenuGroup = {
|
|||
}
|
||||
|
||||
/** @public */
|
||||
export type TLUiSubMenu = {
|
||||
export type TLUiSubMenu<TranslationKey extends string = string> = {
|
||||
id: string
|
||||
type: 'submenu'
|
||||
label: TLUiTranslationKey
|
||||
label: TranslationKey
|
||||
disabled: boolean
|
||||
readonlyOk: boolean
|
||||
children: TLUiMenuChild[]
|
||||
|
@ -64,7 +69,7 @@ export function compactMenuItems<T>(arr: T[]): Exclude<T, null | false | undefin
|
|||
/** @public */
|
||||
export function menuGroup(
|
||||
id: string,
|
||||
...children: (TLUiMenuChild | null | false)[]
|
||||
...children: (TLUiMenuChild | false)[]
|
||||
): TLUiMenuGroup | null {
|
||||
const childItems = compactMenuItems(children)
|
||||
|
||||
|
@ -83,8 +88,8 @@ export function menuGroup(
|
|||
/** @public */
|
||||
export function menuSubmenu(
|
||||
id: string,
|
||||
label: TLUiTranslationKey,
|
||||
...children: (TLUiMenuChild | null | false)[]
|
||||
label: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>,
|
||||
...children: (TLUiMenuChild | false)[]
|
||||
): TLUiSubMenu | null {
|
||||
const childItems = compactMenuItems(children)
|
||||
if (childItems.length === 0) return null
|
||||
|
@ -216,14 +221,11 @@ export function findMenuItem(menu: TLUiMenuSchema, path: string[]) {
|
|||
return item
|
||||
}
|
||||
|
||||
function _findMenuItem(
|
||||
menu: TLUiMenuSchema | TLUiMenuChild[],
|
||||
path: string[]
|
||||
): TLUiMenuChild | null {
|
||||
function _findMenuItem(menu: TLUiMenuSchema | TLUiMenuChild[], path: string[]): TLUiMenuChild {
|
||||
const [next, ...rest] = path
|
||||
if (!next) return null
|
||||
|
||||
const item = menu.find((item) => item.id === next)
|
||||
const item = menu.find((item) => item?.id === next)
|
||||
if (!item) return null
|
||||
|
||||
switch (item.type) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
useEditor,
|
||||
} from '@tldraw/editor'
|
||||
import * as React from 'react'
|
||||
import { getEmbedInfo } from '../../utils/embeds'
|
||||
import { getEmbedInfo } from '../../utils/embeds/embeds'
|
||||
import { EditLinkDialog } from '../components/EditLinkDialog'
|
||||
import { EmbedDialog } from '../components/EmbedDialog'
|
||||
import { TLUiIconType } from '../icon-types'
|
||||
|
@ -32,15 +32,18 @@ import { useToasts } from './useToastsProvider'
|
|||
import { TLUiTranslationKey } from './useTranslation/TLUiTranslationKey'
|
||||
|
||||
/** @public */
|
||||
export interface TLUiActionItem {
|
||||
icon?: TLUiIconType
|
||||
export interface TLUiActionItem<
|
||||
TransationKey extends string = string,
|
||||
IconType extends string = string
|
||||
> {
|
||||
icon?: IconType
|
||||
id: string
|
||||
kbd?: string
|
||||
title?: string
|
||||
label?: TLUiTranslationKey
|
||||
menuLabel?: TLUiTranslationKey
|
||||
shortcutsLabel?: TLUiTranslationKey
|
||||
contextMenuLabel?: TLUiTranslationKey
|
||||
label?: TransationKey
|
||||
menuLabel?: TransationKey
|
||||
shortcutsLabel?: TransationKey
|
||||
contextMenuLabel?: TransationKey
|
||||
readonlyOk: boolean
|
||||
checkbox?: boolean
|
||||
onSelect: (source: TLUiEventSource) => Promise<void> | void
|
||||
|
@ -98,7 +101,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
return editor.getSelectedShapeIds().length > 0
|
||||
}
|
||||
|
||||
const actions = makeActions([
|
||||
const actionItems: TLUiActionItem<TLUiTranslationKey, TLUiIconType>[] = [
|
||||
{
|
||||
id: 'edit-link',
|
||||
label: 'action.edit-link',
|
||||
|
@ -1094,7 +1097,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
editor.toggleLock(editor.getSelectedShapeIds())
|
||||
},
|
||||
},
|
||||
])
|
||||
]
|
||||
|
||||
const actions = makeActions(actionItems)
|
||||
|
||||
if (overrides) {
|
||||
return overrides(editor, actions, undefined)
|
||||
|
@ -1102,9 +1107,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
return actions
|
||||
}, [
|
||||
editor,
|
||||
trackEvent,
|
||||
overrides,
|
||||
editor,
|
||||
addDialog,
|
||||
insertMedia,
|
||||
exportAs,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Editor, TLShapeId, useEditor } from '@tldraw/editor'
|
||||
import { TLShapeId, useEditor } from '@tldraw/editor'
|
||||
import { useCallback } from 'react'
|
||||
import { TLCopyType, getSvgAsImage, getSvgAsString } from '../../utils/export'
|
||||
import { TLCopyType, copyAs } from '../../utils/export/copyAs'
|
||||
import { useToasts } from './useToastsProvider'
|
||||
import { useTranslation } from './useTranslation/useTranslation'
|
||||
|
||||
|
@ -11,140 +11,16 @@ export function useCopyAs() {
|
|||
const msg = useTranslation()
|
||||
|
||||
return useCallback(
|
||||
// it's important that this function itself isn't async - we need to
|
||||
// create the relevant `ClipboardItem`s synchronously to make sure
|
||||
// safari knows that the user _wants_ to copy:
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=222262
|
||||
//
|
||||
// this is fine for navigator.clipboard.write, but for fallbacks it's a
|
||||
// little awkward.
|
||||
function copyAs(ids: TLShapeId[] = editor.getSelectedShapeIds(), format: TLCopyType = 'svg') {
|
||||
if (ids.length === 0) {
|
||||
ids = [...editor.currentPageShapeIds]
|
||||
}
|
||||
|
||||
if (ids.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case 'svg': {
|
||||
if (window.navigator.clipboard) {
|
||||
if (window.navigator.clipboard.write) {
|
||||
window.navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'text/plain': getExportedSvgBlob(editor, ids),
|
||||
}),
|
||||
])
|
||||
} else {
|
||||
fallbackWriteTextAsync(async () =>
|
||||
getSvgAsString(await getExportSvgElement(editor, ids))
|
||||
)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'jpeg':
|
||||
case 'png': {
|
||||
const mimeType = format === 'jpeg' ? 'image/jpeg' : 'image/png'
|
||||
const blobPromise = getExportedImageBlob(editor, ids, format).then((blob) => {
|
||||
if (blob) {
|
||||
if (window.navigator.clipboard) {
|
||||
return blob
|
||||
}
|
||||
throw new Error('Copy not supported')
|
||||
} else {
|
||||
addToast({
|
||||
id: 'copy-fail',
|
||||
icon: 'warning-triangle',
|
||||
title: msg('toast.error.copy-fail.title'),
|
||||
description: msg('toast.error.copy-fail.desc'),
|
||||
})
|
||||
throw new Error('Copy not possible')
|
||||
}
|
||||
})
|
||||
|
||||
window.navigator.clipboard
|
||||
.write([
|
||||
new ClipboardItem({
|
||||
// Note: This needs to use the promise based approach for safari/ios to not bail on a permissions error.
|
||||
[mimeType]: blobPromise,
|
||||
}),
|
||||
])
|
||||
.catch((err: any) => {
|
||||
// Firefox will fail with the above if `dom.events.asyncClipboard.clipboardItem` is enabled.
|
||||
// See <https://github.com/tldraw/tldraw/issues/1325>
|
||||
if (!err.toString().match(/^TypeError: DOMString not supported/)) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
blobPromise.then((blob) => {
|
||||
window.navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
// Note: This needs to use the promise based approach for safari/ios to not bail on a permissions error.
|
||||
[mimeType]: blob,
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'json': {
|
||||
const data = editor.getContentFromCurrentPage(ids)
|
||||
|
||||
if (window.navigator.clipboard) {
|
||||
const jsonStr = JSON.stringify(data)
|
||||
|
||||
if (window.navigator.clipboard.write) {
|
||||
window.navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
'text/plain': new Blob([jsonStr], { type: 'text/plain' }),
|
||||
}),
|
||||
])
|
||||
} else {
|
||||
fallbackWriteTextAsync(async () => jsonStr)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Copy type ${format} not supported.`)
|
||||
}
|
||||
(ids: TLShapeId[], format: TLCopyType = 'svg') => {
|
||||
copyAs(editor, ids, format).catch(() => {
|
||||
addToast({
|
||||
id: 'copy-fail',
|
||||
icon: 'warning-triangle',
|
||||
title: msg('toast.error.copy-fail.title'),
|
||||
description: msg('toast.error.copy-fail.desc'),
|
||||
})
|
||||
})
|
||||
},
|
||||
[editor, addToast, msg]
|
||||
)
|
||||
}
|
||||
|
||||
async function getExportSvgElement(editor: Editor, ids: TLShapeId[]) {
|
||||
const svg = await editor.getSvg(ids, {
|
||||
scale: 1,
|
||||
background: editor.getInstanceState().exportBackground,
|
||||
})
|
||||
|
||||
if (!svg) throw new Error('Could not construct SVG.')
|
||||
|
||||
return svg
|
||||
}
|
||||
|
||||
async function getExportedSvgBlob(editor: Editor, ids: TLShapeId[]) {
|
||||
return new Blob([getSvgAsString(await getExportSvgElement(editor, ids))], {
|
||||
type: 'text/plain',
|
||||
})
|
||||
}
|
||||
|
||||
async function getExportedImageBlob(editor: Editor, ids: TLShapeId[], format: 'png' | 'jpeg') {
|
||||
return await getSvgAsImage(await getExportSvgElement(editor, ids), editor.environment.isSafari, {
|
||||
type: format,
|
||||
quality: 1,
|
||||
scale: 2,
|
||||
})
|
||||
}
|
||||
|
||||
async function fallbackWriteTextAsync(getText: () => Promise<string>) {
|
||||
if (!(navigator && navigator.clipboard)) return
|
||||
navigator.clipboard.writeText(await getText())
|
||||
}
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { TLFrameShape, TLShapeId, useEditor } from '@tldraw/editor'
|
||||
import { TLShapeId, useEditor } from '@tldraw/editor'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
TLExportType,
|
||||
downloadDataURLAsFile,
|
||||
getSvgAsDataUrl,
|
||||
getSvgAsImage,
|
||||
} from '../../utils/export'
|
||||
import { TLExportType, exportAs } from '../../utils/export/exportAs'
|
||||
import { useToasts } from './useToastsProvider'
|
||||
import { useTranslation } from './useTranslation/useTranslation'
|
||||
|
||||
|
@ -16,97 +11,20 @@ export function useExportAs() {
|
|||
const msg = useTranslation()
|
||||
|
||||
return useCallback(
|
||||
async function exportAs(
|
||||
ids: TLShapeId[] = editor.getSelectedShapeIds(),
|
||||
format: TLExportType = 'png'
|
||||
) {
|
||||
if (ids.length === 0) {
|
||||
ids = [...editor.currentPageShapeIds]
|
||||
}
|
||||
|
||||
if (ids.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const svg = await editor.getSvg(ids, {
|
||||
(ids: TLShapeId[], format: TLExportType = 'png') => {
|
||||
exportAs(editor, ids, format, {
|
||||
scale: 1,
|
||||
background: editor.getInstanceState().exportBackground,
|
||||
background: editor.instanceState.exportBackground,
|
||||
}).catch((e) => {
|
||||
console.error(e.message)
|
||||
addToast({
|
||||
id: 'export-fail',
|
||||
// icon: 'error',
|
||||
title: msg('toast.error.export-fail.title'),
|
||||
description: msg('toast.error.export-fail.desc'),
|
||||
})
|
||||
})
|
||||
|
||||
if (!svg) throw new Error('Could not construct SVG.')
|
||||
|
||||
let name = 'shapes' + getTimestamp()
|
||||
|
||||
if (ids.length === 1) {
|
||||
const first = editor.getShape(ids[0])!
|
||||
if (editor.isShapeOfType<TLFrameShape>(first, 'frame')) {
|
||||
name = first.props.name ?? 'frame'
|
||||
} else {
|
||||
name = first.id.replace(/:/, '_')
|
||||
}
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case 'svg': {
|
||||
const dataURL = await getSvgAsDataUrl(svg)
|
||||
downloadDataURLAsFile(dataURL, `${name}.svg`)
|
||||
return
|
||||
}
|
||||
case 'webp':
|
||||
case 'png': {
|
||||
const image = await getSvgAsImage(svg, editor.environment.isSafari, {
|
||||
type: format,
|
||||
quality: 1,
|
||||
scale: 2,
|
||||
})
|
||||
|
||||
if (!image) {
|
||||
addToast({
|
||||
id: 'export-fail',
|
||||
// icon: 'error',
|
||||
title: msg('toast.error.export-fail.title'),
|
||||
description: msg('toast.error.export-fail.desc'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const dataURL = URL.createObjectURL(image)
|
||||
|
||||
downloadDataURLAsFile(dataURL, `${name}.${format}`)
|
||||
|
||||
URL.revokeObjectURL(dataURL)
|
||||
return
|
||||
}
|
||||
|
||||
case 'json': {
|
||||
const data = editor.getContentFromCurrentPage(ids)
|
||||
const dataURL = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(data, null, 4)], { type: 'application/json' })
|
||||
)
|
||||
|
||||
downloadDataURLAsFile(dataURL, `${name || 'shapes'}.json`)
|
||||
|
||||
URL.revokeObjectURL(dataURL)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Export type ${format} not supported.`)
|
||||
}
|
||||
},
|
||||
[editor, addToast, msg]
|
||||
)
|
||||
}
|
||||
|
||||
function getTimestamp() {
|
||||
const now = new Date()
|
||||
|
||||
const year = String(now.getFullYear()).slice(2)
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const hours = String(now.getHours()).padStart(2, '0')
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||
|
||||
return ` at ${year}-${month}-${day} ${hours}.${minutes}.${seconds}`
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Editor, TLBookmarkShape, TLEmbedShape, useEditor, useValue } from '@tldraw/editor'
|
||||
import React, { useMemo } from 'react'
|
||||
import { getEmbedInfo } from '../../utils/embeds'
|
||||
import { getEmbedInfo } from '../../utils/embeds/embeds'
|
||||
import {
|
||||
TLUiMenuSchema,
|
||||
compactMenuItems,
|
||||
|
@ -111,7 +111,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
|
|||
)
|
||||
|
||||
const menuSchema = useMemo<TLUiMenuSchema>(() => {
|
||||
const menuSchema = compactMenuItems([
|
||||
const menuSchema: TLUiMenuSchema = compactMenuItems([
|
||||
menuGroup(
|
||||
'menu',
|
||||
menuSubmenu(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { TLEditorAssetUrls } from './assetUrls'
|
||||
import { TLEditorAssetUrls } from '../../utils/static-assets/assetUrls'
|
||||
|
||||
export type TLTypeFace = {
|
||||
url: string
|
|
@ -69,9 +69,9 @@ export function ToolbarSchemaProvider({ overrides, children }: TLUiToolbarSchema
|
|||
toolbarItem(tools['arrow-up']),
|
||||
toolbarItem(tools['arrow-down']),
|
||||
toolbarItem(tools['arrow-right']),
|
||||
toolbarItem(tools.frame),
|
||||
toolbarItem(tools.line),
|
||||
toolbarItem(tools.highlight),
|
||||
toolbarItem(tools.frame),
|
||||
toolbarItem(tools.laser),
|
||||
])
|
||||
|
||||
|
|
|
@ -8,11 +8,14 @@ import { useInsertMedia } from './useInsertMedia'
|
|||
import { TLUiTranslationKey } from './useTranslation/TLUiTranslationKey'
|
||||
|
||||
/** @public */
|
||||
export interface TLUiToolItem {
|
||||
export interface TLUiToolItem<
|
||||
TranslationKey extends string = string,
|
||||
IconType extends string = string
|
||||
> {
|
||||
id: string
|
||||
label: TLUiTranslationKey
|
||||
shortcutsLabel?: TLUiTranslationKey
|
||||
icon: TLUiIconType
|
||||
label: TranslationKey
|
||||
shortcutsLabel?: TranslationKey
|
||||
icon: IconType
|
||||
onSelect: (source: TLUiEventSource) => void
|
||||
kbd?: string
|
||||
readonlyOk: boolean
|
||||
|
@ -46,7 +49,7 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
|
|||
const insertMedia = useInsertMedia()
|
||||
|
||||
const tools = React.useMemo<TLUiToolsContextType>(() => {
|
||||
const toolsArray: TLUiToolItem[] = [
|
||||
const toolsArray: TLUiToolItem<TLUiTranslationKey, TLUiIconType>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
label: 'tool.select',
|
||||
|
|
|
@ -105,8 +105,8 @@ export const TranslationProvider = track(function TranslationProvider({
|
|||
export function useTranslation() {
|
||||
const translation = useCurrentTranslation()
|
||||
return React.useCallback(
|
||||
function msg(id: TLUiTranslationKey) {
|
||||
return translation.messages[id] ?? id
|
||||
function msg(id: Exclude<string, TLUiTranslationKey> | string) {
|
||||
return translation.messages[id as TLUiTranslationKey] ?? id
|
||||
},
|
||||
[translation]
|
||||
)
|
||||
|
|
|
@ -58,31 +58,31 @@ type WithDefaultHelpers<T extends TLUiOverride<any, any>> = T extends TLUiOverri
|
|||
: never
|
||||
|
||||
/** @public */
|
||||
export interface TLUiOverrides {
|
||||
actionsMenu?: WithDefaultHelpers<NonNullable<ActionsMenuSchemaProviderProps['overrides']>>
|
||||
actions?: WithDefaultHelpers<NonNullable<ActionsProviderProps['overrides']>>
|
||||
contextMenu?: WithDefaultHelpers<NonNullable<TLUiContextMenuSchemaProviderProps['overrides']>>
|
||||
helpMenu?: WithDefaultHelpers<NonNullable<TLUiHelpMenuSchemaProviderProps['overrides']>>
|
||||
menu?: WithDefaultHelpers<NonNullable<TLUiMenuSchemaProviderProps['overrides']>>
|
||||
toolbar?: WithDefaultHelpers<NonNullable<TLUiToolbarSchemaProviderProps['overrides']>>
|
||||
keyboardShortcutsMenu?: WithDefaultHelpers<
|
||||
export type TLUiOverrides = Partial<{
|
||||
actionsMenu: WithDefaultHelpers<NonNullable<ActionsMenuSchemaProviderProps['overrides']>>
|
||||
actions: WithDefaultHelpers<NonNullable<ActionsProviderProps['overrides']>>
|
||||
contextMenu: WithDefaultHelpers<NonNullable<TLUiContextMenuSchemaProviderProps['overrides']>>
|
||||
helpMenu: WithDefaultHelpers<NonNullable<TLUiHelpMenuSchemaProviderProps['overrides']>>
|
||||
menu: WithDefaultHelpers<NonNullable<TLUiMenuSchemaProviderProps['overrides']>>
|
||||
toolbar: WithDefaultHelpers<NonNullable<TLUiToolbarSchemaProviderProps['overrides']>>
|
||||
keyboardShortcutsMenu: WithDefaultHelpers<
|
||||
NonNullable<TLUiKeyboardShortcutsSchemaProviderProps['overrides']>
|
||||
>
|
||||
tools?: WithDefaultHelpers<NonNullable<TLUiToolsProviderProps['overrides']>>
|
||||
translations?: TLUiTranslationProviderProps['overrides']
|
||||
}
|
||||
tools: WithDefaultHelpers<NonNullable<TLUiToolsProviderProps['overrides']>>
|
||||
translations: TLUiTranslationProviderProps['overrides']
|
||||
}>
|
||||
|
||||
export interface TLUiOverridesWithoutDefaults {
|
||||
actionsMenu?: ActionsMenuSchemaProviderProps['overrides']
|
||||
actions?: ActionsProviderProps['overrides']
|
||||
contextMenu?: TLUiContextMenuSchemaProviderProps['overrides']
|
||||
helpMenu?: TLUiHelpMenuSchemaProviderProps['overrides']
|
||||
menu?: TLUiMenuSchemaProviderProps['overrides']
|
||||
toolbar?: TLUiToolbarSchemaProviderProps['overrides']
|
||||
keyboardShortcutsMenu?: TLUiKeyboardShortcutsSchemaProviderProps['overrides']
|
||||
tools?: TLUiToolsProviderProps['overrides']
|
||||
translations?: TLUiTranslationProviderProps['overrides']
|
||||
}
|
||||
export type TLUiOverridesWithoutDefaults = Partial<{
|
||||
actionsMenu: ActionsMenuSchemaProviderProps['overrides']
|
||||
actions: ActionsProviderProps['overrides']
|
||||
contextMenu: TLUiContextMenuSchemaProviderProps['overrides']
|
||||
helpMenu: TLUiHelpMenuSchemaProviderProps['overrides']
|
||||
menu: TLUiMenuSchemaProviderProps['overrides']
|
||||
toolbar: TLUiToolbarSchemaProviderProps['overrides']
|
||||
keyboardShortcutsMenu: TLUiKeyboardShortcutsSchemaProviderProps['overrides']
|
||||
tools: TLUiToolsProviderProps['overrides']
|
||||
translations: TLUiTranslationProviderProps['overrides']
|
||||
}>
|
||||
|
||||
export function mergeOverrides(
|
||||
overrides: TLUiOverrides[],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import downscale from 'downscale'
|
||||
import { getBrowserCanvasMaxSize } from '../shapes/shared/getBrowserCanvasMaxSize'
|
||||
import { isAnimated } from './is-gif-animated'
|
||||
import { isAnimated } from './assets/is-gif-animated'
|
||||
|
||||
type BoxWidthHeight = {
|
||||
w: number
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue