Feature/upload multiple media (#830)
This commit is contained in:
parent
223391afe5
commit
7a353c7d9f
8 changed files with 255 additions and 364 deletions
|
@ -0,0 +1,21 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export function ImageIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
|
@ -16,3 +16,4 @@ export * from './MultiplayerIcon'
|
||||||
export * from './DiscordIcon'
|
export * from './DiscordIcon'
|
||||||
export * from './LineIcon'
|
export * from './LineIcon'
|
||||||
export * from './QuestionMarkIcon'
|
export * from './QuestionMarkIcon'
|
||||||
|
export * from './ImageIcon'
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { useTldrawApp } from '~hooks'
|
||||||
import { ToolButtonWithTooltip } from '~components/Primitives/ToolButton'
|
import { ToolButtonWithTooltip } from '~components/Primitives/ToolButton'
|
||||||
import { Panel } from '~components/Primitives/Panel'
|
import { Panel } from '~components/Primitives/Panel'
|
||||||
import { ShapesMenu } from './ShapesMenu'
|
import { ShapesMenu } from './ShapesMenu'
|
||||||
import { EraserIcon } from '~components/Primitives/icons'
|
import { EraserIcon, ImageIcon } from '~components/Primitives/icons'
|
||||||
|
|
||||||
const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool
|
const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool
|
||||||
const toolLockedSelector = (s: TDSnapshot) => s.appState.isToolLocked
|
const toolLockedSelector = (s: TDSnapshot) => s.appState.isToolLocked
|
||||||
|
@ -51,6 +51,10 @@ export const PrimaryTools = React.memo(function PrimaryTools() {
|
||||||
app.selectTool(TDShapeType.Sticky)
|
app.selectTool(TDShapeType.Sticky)
|
||||||
}, [app])
|
}, [app])
|
||||||
|
|
||||||
|
const uploadMedias = React.useCallback(async () => {
|
||||||
|
app.openAsset()
|
||||||
|
}, [app])
|
||||||
|
|
||||||
const panelStyle = dockPosition === 'bottom' || dockPosition === 'top' ? 'row' : 'column'
|
const panelStyle = dockPosition === 'bottom' || dockPosition === 'top' ? 'row' : 'column'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -112,6 +116,9 @@ export const PrimaryTools = React.memo(function PrimaryTools() {
|
||||||
>
|
>
|
||||||
<Pencil2Icon />
|
<Pencil2Icon />
|
||||||
</ToolButtonWithTooltip>
|
</ToolButtonWithTooltip>
|
||||||
|
<ToolButtonWithTooltip label="Image" onClick={uploadMedias} id="TD-PrimaryTools-Image">
|
||||||
|
<ImageIcon />
|
||||||
|
</ToolButtonWithTooltip>
|
||||||
</Panel>
|
</Panel>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {
|
import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
|
||||||
ExitIcon,
|
|
||||||
GitHubLogoIcon,
|
|
||||||
HamburgerMenuIcon,
|
|
||||||
HeartFilledIcon,
|
|
||||||
TwitterLogoIcon,
|
|
||||||
} from '@radix-ui/react-icons'
|
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { useTldrawApp } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { PreferencesMenu } from '../PreferencesMenu'
|
import { PreferencesMenu } from '../PreferencesMenu'
|
||||||
|
@ -18,9 +12,7 @@ import {
|
||||||
} from '~components/Primitives/DropdownMenu'
|
} from '~components/Primitives/DropdownMenu'
|
||||||
import { SmallIcon } from '~components/Primitives/SmallIcon'
|
import { SmallIcon } from '~components/Primitives/SmallIcon'
|
||||||
import { useFileSystemHandlers } from '~hooks'
|
import { useFileSystemHandlers } from '~hooks'
|
||||||
import { HeartIcon } from '~components/Primitives/icons/HeartIcon'
|
|
||||||
import { preventEvent } from '~components/preventEvent'
|
import { preventEvent } from '~components/preventEvent'
|
||||||
import { DiscordIcon } from '~components/Primitives/icons'
|
|
||||||
import { TDExportType, TDSnapshot } from '~types'
|
import { TDExportType, TDSnapshot } from '~types'
|
||||||
import { Divider } from '~components/Primitives/Divider'
|
import { Divider } from '~components/Primitives/Divider'
|
||||||
import { FormattedMessage, useIntl } from 'react-intl'
|
import { FormattedMessage, useIntl } from 'react-intl'
|
||||||
|
|
|
@ -110,7 +110,7 @@ describe('TldrawTestApp', () => {
|
||||||
expect(app.bindings.length).toBe(2)
|
expect(app.bindings.length).toBe(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes bindings from copied shape handles', () => {
|
it.only('removes bindings from copied shape handles', () => {
|
||||||
const app = new TldrawTestApp()
|
const app = new TldrawTestApp()
|
||||||
|
|
||||||
app
|
app
|
||||||
|
|
|
@ -42,6 +42,7 @@ import {
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
TDExportType,
|
TDExportType,
|
||||||
TldrawPatch,
|
TldrawPatch,
|
||||||
|
AlignStyle,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
import {
|
import {
|
||||||
migrate,
|
migrate,
|
||||||
|
@ -49,7 +50,7 @@ import {
|
||||||
loadFileHandle,
|
loadFileHandle,
|
||||||
openFromFileSystem,
|
openFromFileSystem,
|
||||||
saveToFileSystem,
|
saveToFileSystem,
|
||||||
openAssetFromFileSystem,
|
openAssetsFromFileSystem,
|
||||||
fileToBase64,
|
fileToBase64,
|
||||||
fileToText,
|
fileToText,
|
||||||
getImageSizeFromSrc,
|
getImageSizeFromSrc,
|
||||||
|
@ -84,6 +85,7 @@ import { clearPrevSize } from './shapes/shared/getTextSize'
|
||||||
import { getClipboard, setClipboard } from './IdbClipboard'
|
import { getClipboard, setClipboard } from './IdbClipboard'
|
||||||
import { deepCopy } from './StateManager/copy'
|
import { deepCopy } from './StateManager/copy'
|
||||||
import { getTranslation } from '~translations'
|
import { getTranslation } from '~translations'
|
||||||
|
import { TextUtil } from './shapes/TextUtil'
|
||||||
|
|
||||||
const uuid = Utils.uniqueId()
|
const uuid = Utils.uniqueId()
|
||||||
|
|
||||||
|
@ -260,11 +262,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
}
|
}
|
||||||
|
|
||||||
pasteInfo = {
|
|
||||||
center: [0, 0],
|
|
||||||
offset: [0, 0],
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(id?: string, callbacks = {} as TDCallbacks) {
|
constructor(id?: string, callbacks = {} as TDCallbacks) {
|
||||||
super(TldrawApp.defaultState, id, TldrawApp.version, (prev, next, prevVersion) => {
|
super(TldrawApp.defaultState, id, TldrawApp.version, (prev, next, prevVersion) => {
|
||||||
return migrate(
|
return migrate(
|
||||||
|
@ -1221,7 +1218,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
resetDocument = (): this => {
|
resetDocument = (): this => {
|
||||||
if (this.session) return this
|
if (this.session) return this
|
||||||
this.session = undefined
|
this.session = undefined
|
||||||
this.pasteInfo.offset = [0, 0]
|
|
||||||
this.currentTool = this.tools.select
|
this.currentTool = this.tools.select
|
||||||
|
|
||||||
const doc = TldrawApp.defaultDocument
|
const doc = TldrawApp.defaultDocument
|
||||||
|
@ -1534,9 +1530,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
openAsset = async () => {
|
openAsset = async () => {
|
||||||
if (!this.disableAssets)
|
if (!this.disableAssets)
|
||||||
try {
|
try {
|
||||||
const file = await openAssetFromFileSystem()
|
const file = await openAssetsFromFileSystem()
|
||||||
|
if (Array.isArray(file)) {
|
||||||
|
this.addMediaFromFiles(file, this.centerPoint)
|
||||||
|
} else {
|
||||||
if (!file) return
|
if (!file) return
|
||||||
this.addMediaFromFile(file)
|
this.addMediaFromFiles([file])
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -1839,9 +1839,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pasteInfo.offset = [0, 0]
|
|
||||||
this.pasteInfo.center = [0, 0]
|
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1852,13 +1849,21 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
paste = async (point?: number[], e?: ClipboardEvent) => {
|
paste = async (point?: number[], e?: ClipboardEvent) => {
|
||||||
if (this.readOnly) return
|
if (this.readOnly) return
|
||||||
|
|
||||||
const pasteTextAsSvg = async (text: string) => {
|
const shapesToCreate: TDShape[] = []
|
||||||
|
|
||||||
|
const filesToPaste: File[] = []
|
||||||
|
|
||||||
|
let clipboardData: any
|
||||||
|
|
||||||
|
const getSvgFromText = async (text: string) => {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
div.innerHTML = text
|
div.innerHTML = text
|
||||||
const svg = div.firstChild as SVGSVGElement
|
const svg = div.firstChild as SVGSVGElement
|
||||||
|
|
||||||
svg.style.setProperty('background-color', 'transparent')
|
svg.style.setProperty('background-color', 'transparent')
|
||||||
|
|
||||||
|
console.log(text)
|
||||||
|
|
||||||
const imageBlob = await TLDR.getImageForSvg(svg, TDExportType.SVG, {
|
const imageBlob = await TLDR.getImageForSvg(svg, TDExportType.SVG, {
|
||||||
scale: 1,
|
scale: 1,
|
||||||
quality: 1,
|
quality: 1,
|
||||||
|
@ -1866,28 +1871,33 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
|
|
||||||
if (imageBlob) {
|
if (imageBlob) {
|
||||||
const file = new File([imageBlob], 'image.svg')
|
const file = new File([imageBlob], 'image.svg')
|
||||||
this.addMediaFromFile(file)
|
filesToPaste.push(file)
|
||||||
} else {
|
} else {
|
||||||
pasteTextAsShape(text)
|
getShapeFromText(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pasteTextAsShape = (text: string) => {
|
const getShapeFromText = (text: string) => {
|
||||||
const shapeId = Utils.uniqueId()
|
const pagePoint = this.getPagePoint(point ?? this.centerPoint, this.currentPageId)
|
||||||
|
|
||||||
this.createShapes({
|
const isMultiline = text.includes('\n')
|
||||||
id: shapeId,
|
|
||||||
|
shapesToCreate.push(
|
||||||
|
TLDR.getShapeUtil(TDShapeType.Text).getShape({
|
||||||
|
id: Utils.uniqueId(),
|
||||||
type: TDShapeType.Text,
|
type: TDShapeType.Text,
|
||||||
parentId: this.appState.currentPageId,
|
parentId: this.appState.currentPageId,
|
||||||
text: TLDR.normalizeText(text.trim()),
|
text: TLDR.normalizeText(text.trim()),
|
||||||
point: this.getPagePoint(this.centerPoint, this.currentPageId),
|
point: pagePoint,
|
||||||
style: { ...this.appState.currentStyle },
|
style: {
|
||||||
|
...this.appState.currentStyle,
|
||||||
|
textAlign: isMultiline ? AlignStyle.Start : this.appState.currentStyle.textAlign,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
)
|
||||||
this.select(shapeId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pasteAsHTML = (html: string) => {
|
const getShapeFromHtml = (html: string) => {
|
||||||
try {
|
try {
|
||||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1]
|
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1]
|
||||||
|
|
||||||
|
@ -1900,115 +1910,103 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
assets: TDAsset[]
|
assets: TDAsset[]
|
||||||
} = JSON.parse(maybeJson)
|
} = JSON.parse(maybeJson)
|
||||||
if (json.type === 'tldr/clipboard') {
|
if (json.type === 'tldr/clipboard') {
|
||||||
this.insertContent(json, { point, select: true })
|
clipboardData = json
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
throw Error('Not tldraw data!')
|
throw Error('Not tldraw data!')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
pasteTextAsShape(html)
|
getShapeFromText(html)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e !== undefined) {
|
if (e !== undefined) {
|
||||||
const items = e.clipboardData?.items ?? []
|
const items = Array.from(e.clipboardData?.items ?? [])
|
||||||
for (const index in items) {
|
|
||||||
const item = items[index]
|
|
||||||
|
|
||||||
// TODO
|
await Promise.all(
|
||||||
// We could eventually support pasting multiple files / images,
|
items.map(async (item) => {
|
||||||
// and tiling them out on the canvas. At the moment, let's just
|
const { type, kind } = item
|
||||||
// support pasting one file / image.
|
|
||||||
|
|
||||||
if (item.type === 'text/html') {
|
switch (kind) {
|
||||||
item.getAsString(async (text) => {
|
|
||||||
pasteAsHTML(text)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
switch (item.kind) {
|
|
||||||
case 'string': {
|
case 'string': {
|
||||||
item.getAsString(async (text) => {
|
const str: string = await new Promise((resolve) => item.getAsString(resolve))
|
||||||
if (text.startsWith('<svg')) {
|
|
||||||
pasteTextAsSvg(text)
|
switch (type) {
|
||||||
} else {
|
case 'text/html': {
|
||||||
pasteTextAsShape(text)
|
if (str.match(/<tldraw>(.*)<\/tldraw>/)?.[1]) {
|
||||||
}
|
getShapeFromHtml(str)
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'text/plain': {
|
||||||
|
console.log(str)
|
||||||
|
if (str.startsWith('<svg')) {
|
||||||
|
getSvgFromText(str)
|
||||||
|
} else {
|
||||||
|
getShapeFromText(str)
|
||||||
|
}
|
||||||
|
// return
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
case 'file': {
|
case 'file': {
|
||||||
const file = item.getAsFile()
|
const file = item.getAsFile()
|
||||||
if (file) {
|
if (file) filesToPaste.push(file)
|
||||||
this.addMediaFromFile(file)
|
break
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getClipboard().then((clipboard) => {
|
|
||||||
if (clipboard) {
|
|
||||||
pasteAsHTML(clipboard)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
)
|
||||||
if (navigator.clipboard) {
|
|
||||||
const items = 'read' in navigator.clipboard ? await navigator.clipboard.read() : []
|
|
||||||
|
|
||||||
if (items.length === 0) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const item of items) {
|
|
||||||
// look for png data.
|
|
||||||
|
|
||||||
const pngData = await item.getType('text/png')
|
|
||||||
|
|
||||||
if (pngData) {
|
|
||||||
const file = new File([pngData], 'image.png')
|
|
||||||
this.addMediaFromFile(file)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// look for svg data.
|
if (clipboardData) {
|
||||||
|
this.insertContent(clipboardData, { point, select: true })
|
||||||
const svgData = await item.getType('image/svg+xml')
|
return this
|
||||||
|
|
||||||
if (svgData) {
|
|
||||||
const file = new File([svgData], 'image.svg')
|
|
||||||
this.addMediaFromFile(file)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// look for plain text data.
|
if (filesToPaste.length) {
|
||||||
|
this.addMediaFromFiles(filesToPaste, point)
|
||||||
const textData = await item.getType('text/plain')
|
return this
|
||||||
|
|
||||||
if (textData) {
|
|
||||||
// TODO: Paste as an SVG image if the incoming data is an SVG.
|
|
||||||
const text = await textData.text()
|
|
||||||
text.trim()
|
|
||||||
|
|
||||||
if (text.startsWith('<svg')) {
|
|
||||||
pasteTextAsSvg(text)
|
|
||||||
} else {
|
|
||||||
pasteTextAsShape(text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
if (shapesToCreate.length) {
|
||||||
|
const pagePoint = this.getPagePoint(point ?? this.centerPoint, this.currentPageId)
|
||||||
|
|
||||||
|
const currentPoint = Vec.add(pagePoint, [0, 0])
|
||||||
|
|
||||||
|
shapesToCreate.forEach((shape, i) => {
|
||||||
|
const bounds = TLDR.getBounds(shape)
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
// For the first shape, offset the current point so
|
||||||
|
// that the first shape's center is at the page point
|
||||||
|
currentPoint[0] -= bounds.width / 2
|
||||||
|
currentPoint[1] -= bounds.height / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the shape's point the current point
|
||||||
|
shape.point = [...currentPoint]
|
||||||
|
|
||||||
|
// Then bump the page current point by this shape's width
|
||||||
|
currentPoint[0] += bounds.width
|
||||||
|
})
|
||||||
|
|
||||||
|
this.createShapes(...shapesToCreate)
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TLDR.warn('This browser does not support the Clipboard API!')
|
|
||||||
if (this.clipboard) {
|
if (this.clipboard) {
|
||||||
this.insertContent(this.clipboard, { point, select: true })
|
// try to get clipboard data from the scene itself
|
||||||
}
|
this.insertContent(this.clipboard)
|
||||||
|
} else {
|
||||||
|
// last chance to get the clipboard data, is it in storage?
|
||||||
|
getClipboard().then((text) => {
|
||||||
|
if (text) getShapeFromHtml(text)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
|
@ -3015,13 +3013,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
createImageOrVideoShapeAtPoint(
|
getImageOrVideoShapeAtPoint(
|
||||||
id: string,
|
id: string,
|
||||||
type: TDShapeType.Image | TDShapeType.Video,
|
type: TDShapeType.Image | TDShapeType.Video,
|
||||||
point: number[],
|
point: number[],
|
||||||
size: number[],
|
size: number[],
|
||||||
assetId: string
|
assetId: string
|
||||||
): this {
|
) {
|
||||||
const {
|
const {
|
||||||
shapes,
|
shapes,
|
||||||
appState: { currentPageId, currentStyle },
|
appState: { currentPageId, currentStyle },
|
||||||
|
@ -3066,13 +3064,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
assetId,
|
assetId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const bounds = Shape.getBounds(newShape as never)
|
return newShape
|
||||||
|
|
||||||
newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2])
|
|
||||||
|
|
||||||
this.createShapes(newShape)
|
|
||||||
|
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3394,11 +3386,20 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
addMediaFromFile = async (file: File, point = this.centerPoint) => {
|
addMediaFromFiles = async (files: File[], point = this.centerPoint) => {
|
||||||
this.setIsLoading(true)
|
this.setIsLoading(true)
|
||||||
|
|
||||||
const id = Utils.uniqueId()
|
// Rather than creating each shape individually (which will produce undo / redo entries
|
||||||
|
// for each shape), create an array of all the shapes that we'll need to create. We'll
|
||||||
|
// iterate through these at the bottom of the function to set their points, then create
|
||||||
|
// them through a single call to `createShapes`.
|
||||||
|
|
||||||
|
const shapesToCreate: TDShape[] = []
|
||||||
|
|
||||||
const pagePoint = this.getPagePoint(point)
|
const pagePoint = this.getPagePoint(point)
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const id = Utils.uniqueId()
|
||||||
const extension = file.name.match(/\.[0-9a-z]+$/i)
|
const extension = file.name.match(/\.[0-9a-z]+$/i)
|
||||||
|
|
||||||
if (!extension) throw Error('No extension')
|
if (!extension) throw Error('No extension')
|
||||||
|
@ -3475,12 +3476,45 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
assetId = match.id
|
assetId = match.id
|
||||||
}
|
}
|
||||||
|
|
||||||
this.createImageOrVideoShapeAtPoint(id, shapeType, pagePoint, size, assetId)
|
shapesToCreate.push(this.getImageOrVideoShapeAtPoint(id, shapeType, point, size, assetId))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Even if one shape errors, keep going (we might have had other shapes that didn't error)
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
this.setIsLoading(false)
|
}
|
||||||
return this
|
}
|
||||||
|
|
||||||
|
if (shapesToCreate.length) {
|
||||||
|
const currentPoint = Vec.add(pagePoint, [0, 0])
|
||||||
|
|
||||||
|
shapesToCreate.forEach((shape, i) => {
|
||||||
|
const bounds = TLDR.getBounds(shape)
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
// For the first shape, offset the current point so
|
||||||
|
// that the first shape's center is at the page point
|
||||||
|
currentPoint[0] -= bounds.width / 2
|
||||||
|
currentPoint[1] -= bounds.height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the shape's point the current point
|
||||||
|
shape.point = [...currentPoint]
|
||||||
|
|
||||||
|
// Then bump the page current point by this shape's width
|
||||||
|
currentPoint[0] += bounds.width
|
||||||
|
})
|
||||||
|
|
||||||
|
const commonBounds = Utils.getCommonBounds(shapesToCreate.map(TLDR.getBounds))
|
||||||
|
|
||||||
|
this.createShapes(...shapesToCreate)
|
||||||
|
|
||||||
|
// Are the common bounds too big for the viewport?
|
||||||
|
if (!Utils.boundsContain(this.viewport, commonBounds)) {
|
||||||
|
this.zoomToSelection()
|
||||||
|
if (this.zoom > 1) {
|
||||||
|
this.resetZoom()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setIsLoading(false)
|
this.setIsLoading(false)
|
||||||
|
@ -3677,8 +3711,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (this.disableAssets) return this
|
if (this.disableAssets) return this
|
||||||
if (e.dataTransfer.files?.length) {
|
if (e.dataTransfer.files?.length) {
|
||||||
const file = e.dataTransfer.files[0]
|
this.addMediaFromFiles(Object.values(e.dataTransfer.files), [e.clientX, e.clientY])
|
||||||
this.addMediaFromFile(file, [e.clientX, e.clientY])
|
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
|
@ -224,7 +224,7 @@ TldrawTestApp {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"_status": "ready",
|
"_status": "ready",
|
||||||
"addMediaFromFile": [Function],
|
"addMediaFromFiles": [Function],
|
||||||
"addToSelectHistory": [Function],
|
"addToSelectHistory": [Function],
|
||||||
"align": [Function],
|
"align": [Function],
|
||||||
"altKey": false,
|
"altKey": false,
|
||||||
|
@ -482,16 +482,6 @@ TldrawTestApp {
|
||||||
],
|
],
|
||||||
"pan": [Function],
|
"pan": [Function],
|
||||||
"paste": [Function],
|
"paste": [Function],
|
||||||
"pasteInfo": Object {
|
|
||||||
"center": Array [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
"offset": Array [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"patchCreate": [Function],
|
"patchCreate": [Function],
|
||||||
"patchState": [Function],
|
"patchState": [Function],
|
||||||
"persist": [Function],
|
"persist": [Function],
|
||||||
|
@ -828,156 +818,3 @@ TldrawTestApp {
|
||||||
"zoomToSelection": [Function],
|
"zoomToSelection": [Function],
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`TldrawTestApp Exposes undo/redo stack: history 1`] = `
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"after": Object {
|
|
||||||
"document": Object {
|
|
||||||
"pageStates": Object {
|
|
||||||
"page1": Object {
|
|
||||||
"selectedIds": Array [
|
|
||||||
"rect1",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"pages": Object {
|
|
||||||
"page1": Object {
|
|
||||||
"bindings": Object {},
|
|
||||||
"shapes": Object {
|
|
||||||
"rect1": Object {
|
|
||||||
"childIndex": 1,
|
|
||||||
"id": "rect1",
|
|
||||||
"label": "",
|
|
||||||
"labelPoint": Array [
|
|
||||||
0.5,
|
|
||||||
0.5,
|
|
||||||
],
|
|
||||||
"name": "Rectangle",
|
|
||||||
"parentId": "page1",
|
|
||||||
"point": Array [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
"rotation": 0,
|
|
||||||
"size": Array [
|
|
||||||
100,
|
|
||||||
200,
|
|
||||||
],
|
|
||||||
"style": Object {
|
|
||||||
"color": "black",
|
|
||||||
"dash": "draw",
|
|
||||||
"isFilled": false,
|
|
||||||
"scale": 1,
|
|
||||||
"size": "small",
|
|
||||||
},
|
|
||||||
"type": "rectangle",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"before": Object {
|
|
||||||
"document": Object {
|
|
||||||
"pageStates": Object {
|
|
||||||
"page1": Object {
|
|
||||||
"selectedIds": Array [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"pages": Object {
|
|
||||||
"page1": Object {
|
|
||||||
"bindings": Object {},
|
|
||||||
"shapes": Object {
|
|
||||||
"rect1": undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id": "create",
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"after": Object {
|
|
||||||
"document": Object {
|
|
||||||
"pageStates": Object {
|
|
||||||
"page1": Object {
|
|
||||||
"selectedIds": Array [
|
|
||||||
"rect2",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"pages": Object {
|
|
||||||
"page1": Object {
|
|
||||||
"bindings": Object {},
|
|
||||||
"shapes": Object {
|
|
||||||
"rect2": Object {
|
|
||||||
"childIndex": 1,
|
|
||||||
"id": "rect2",
|
|
||||||
"label": "",
|
|
||||||
"labelPoint": Array [
|
|
||||||
0.5,
|
|
||||||
0.5,
|
|
||||||
],
|
|
||||||
"name": "Rectangle",
|
|
||||||
"parentId": "page1",
|
|
||||||
"point": Array [
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
"rotation": 0,
|
|
||||||
"size": Array [
|
|
||||||
100,
|
|
||||||
200,
|
|
||||||
],
|
|
||||||
"style": Object {
|
|
||||||
"color": "black",
|
|
||||||
"dash": "draw",
|
|
||||||
"isFilled": false,
|
|
||||||
"scale": 1,
|
|
||||||
"size": "small",
|
|
||||||
},
|
|
||||||
"type": "rectangle",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"before": Object {
|
|
||||||
"document": Object {
|
|
||||||
"pageStates": Object {
|
|
||||||
"page1": Object {
|
|
||||||
"selectedIds": Array [
|
|
||||||
"rect1",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"pages": Object {
|
|
||||||
"page1": Object {
|
|
||||||
"bindings": Object {},
|
|
||||||
"shapes": Object {
|
|
||||||
"rect2": undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id": "create",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`TldrawTestApp Selection When selecting all selects all: selected all 1`] = `
|
|
||||||
Array [
|
|
||||||
"rect1",
|
|
||||||
"rect2",
|
|
||||||
"rect3",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`TldrawTestApp When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 232 232\\" width=\\"200\\" height=\\"200\\" style=\\"background-color: rgb(248, 249, 250);\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block');</style></defs><g/></svg>"`;
|
|
||||||
|
|
||||||
exports[`TldrawTestApp When copying to SVG Copies shapes.: copied svg 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 236.74 236.74\\" width=\\"204.74\\" height=\\"204.74\\" style=\\"background-color: rgb(248, 249, 250);\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block');</style></defs></svg>"`;
|
|
||||||
|
|
||||||
exports[`TldrawTestApp When copying to SVG Respects child index: copied svg with reordered elements 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 232 232\\" width=\\"200\\" height=\\"200\\" style=\\"background-color: rgb(248, 249, 250);\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block');</style></defs></svg>"`;
|
|
||||||
|
|
|
@ -106,14 +106,14 @@ export async function openFromFileSystem(): Promise<null | {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openAssetFromFileSystem() {
|
export async function openAssetsFromFileSystem() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const browserFS = await import('./browser-fs-access')
|
const browserFS = await import('./browser-fs-access')
|
||||||
const fileOpen = browserFS.fileOpen
|
const fileOpen = browserFS.fileOpen
|
||||||
return fileOpen({
|
return fileOpen({
|
||||||
description: 'Image or Video',
|
description: 'Image or Video',
|
||||||
extensions: [...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS],
|
extensions: [...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS],
|
||||||
multiple: false,
|
multiple: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue