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 './LineIcon'
|
||||
export * from './QuestionMarkIcon'
|
||||
export * from './ImageIcon'
|
||||
|
|
|
@ -12,7 +12,7 @@ import { useTldrawApp } from '~hooks'
|
|||
import { ToolButtonWithTooltip } from '~components/Primitives/ToolButton'
|
||||
import { Panel } from '~components/Primitives/Panel'
|
||||
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 toolLockedSelector = (s: TDSnapshot) => s.appState.isToolLocked
|
||||
|
@ -51,6 +51,10 @@ export const PrimaryTools = React.memo(function PrimaryTools() {
|
|||
app.selectTool(TDShapeType.Sticky)
|
||||
}, [app])
|
||||
|
||||
const uploadMedias = React.useCallback(async () => {
|
||||
app.openAsset()
|
||||
}, [app])
|
||||
|
||||
const panelStyle = dockPosition === 'bottom' || dockPosition === 'top' ? 'row' : 'column'
|
||||
|
||||
return (
|
||||
|
@ -112,6 +116,9 @@ export const PrimaryTools = React.memo(function PrimaryTools() {
|
|||
>
|
||||
<Pencil2Icon />
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip label="Image" onClick={uploadMedias} id="TD-PrimaryTools-Image">
|
||||
<ImageIcon />
|
||||
</ToolButtonWithTooltip>
|
||||
</Panel>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
ExitIcon,
|
||||
GitHubLogoIcon,
|
||||
HamburgerMenuIcon,
|
||||
HeartFilledIcon,
|
||||
TwitterLogoIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
import { PreferencesMenu } from '../PreferencesMenu'
|
||||
|
@ -18,9 +12,7 @@ import {
|
|||
} from '~components/Primitives/DropdownMenu'
|
||||
import { SmallIcon } from '~components/Primitives/SmallIcon'
|
||||
import { useFileSystemHandlers } from '~hooks'
|
||||
import { HeartIcon } from '~components/Primitives/icons/HeartIcon'
|
||||
import { preventEvent } from '~components/preventEvent'
|
||||
import { DiscordIcon } from '~components/Primitives/icons'
|
||||
import { TDExportType, TDSnapshot } from '~types'
|
||||
import { Divider } from '~components/Primitives/Divider'
|
||||
import { FormattedMessage, useIntl } from 'react-intl'
|
||||
|
|
|
@ -110,7 +110,7 @@ describe('TldrawTestApp', () => {
|
|||
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()
|
||||
|
||||
app
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
ArrowShape,
|
||||
TDExportType,
|
||||
TldrawPatch,
|
||||
AlignStyle,
|
||||
} from '~types'
|
||||
import {
|
||||
migrate,
|
||||
|
@ -49,7 +50,7 @@ import {
|
|||
loadFileHandle,
|
||||
openFromFileSystem,
|
||||
saveToFileSystem,
|
||||
openAssetFromFileSystem,
|
||||
openAssetsFromFileSystem,
|
||||
fileToBase64,
|
||||
fileToText,
|
||||
getImageSizeFromSrc,
|
||||
|
@ -84,6 +85,7 @@ import { clearPrevSize } from './shapes/shared/getTextSize'
|
|||
import { getClipboard, setClipboard } from './IdbClipboard'
|
||||
import { deepCopy } from './StateManager/copy'
|
||||
import { getTranslation } from '~translations'
|
||||
import { TextUtil } from './shapes/TextUtil'
|
||||
|
||||
const uuid = Utils.uniqueId()
|
||||
|
||||
|
@ -260,11 +262,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
center: [0, 0],
|
||||
}
|
||||
|
||||
pasteInfo = {
|
||||
center: [0, 0],
|
||||
offset: [0, 0],
|
||||
}
|
||||
|
||||
constructor(id?: string, callbacks = {} as TDCallbacks) {
|
||||
super(TldrawApp.defaultState, id, TldrawApp.version, (prev, next, prevVersion) => {
|
||||
return migrate(
|
||||
|
@ -1221,7 +1218,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
resetDocument = (): this => {
|
||||
if (this.session) return this
|
||||
this.session = undefined
|
||||
this.pasteInfo.offset = [0, 0]
|
||||
this.currentTool = this.tools.select
|
||||
|
||||
const doc = TldrawApp.defaultDocument
|
||||
|
@ -1534,9 +1530,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
openAsset = async () => {
|
||||
if (!this.disableAssets)
|
||||
try {
|
||||
const file = await openAssetFromFileSystem()
|
||||
if (!file) return
|
||||
this.addMediaFromFile(file)
|
||||
const file = await openAssetsFromFileSystem()
|
||||
if (Array.isArray(file)) {
|
||||
this.addMediaFromFiles(file, this.centerPoint)
|
||||
} else {
|
||||
if (!file) return
|
||||
this.addMediaFromFiles([file])
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
|
@ -1839,9 +1839,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
])
|
||||
}
|
||||
|
||||
this.pasteInfo.offset = [0, 0]
|
||||
this.pasteInfo.center = [0, 0]
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1852,13 +1849,21 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
paste = async (point?: number[], e?: ClipboardEvent) => {
|
||||
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')
|
||||
div.innerHTML = text
|
||||
const svg = div.firstChild as SVGSVGElement
|
||||
|
||||
svg.style.setProperty('background-color', 'transparent')
|
||||
|
||||
console.log(text)
|
||||
|
||||
const imageBlob = await TLDR.getImageForSvg(svg, TDExportType.SVG, {
|
||||
scale: 1,
|
||||
quality: 1,
|
||||
|
@ -1866,28 +1871,33 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
if (imageBlob) {
|
||||
const file = new File([imageBlob], 'image.svg')
|
||||
this.addMediaFromFile(file)
|
||||
filesToPaste.push(file)
|
||||
} else {
|
||||
pasteTextAsShape(text)
|
||||
getShapeFromText(text)
|
||||
}
|
||||
}
|
||||
|
||||
const pasteTextAsShape = (text: string) => {
|
||||
const shapeId = Utils.uniqueId()
|
||||
const getShapeFromText = (text: string) => {
|
||||
const pagePoint = this.getPagePoint(point ?? this.centerPoint, this.currentPageId)
|
||||
|
||||
this.createShapes({
|
||||
id: shapeId,
|
||||
type: TDShapeType.Text,
|
||||
parentId: this.appState.currentPageId,
|
||||
text: TLDR.normalizeText(text.trim()),
|
||||
point: this.getPagePoint(this.centerPoint, this.currentPageId),
|
||||
style: { ...this.appState.currentStyle },
|
||||
})
|
||||
const isMultiline = text.includes('\n')
|
||||
|
||||
this.select(shapeId)
|
||||
shapesToCreate.push(
|
||||
TLDR.getShapeUtil(TDShapeType.Text).getShape({
|
||||
id: Utils.uniqueId(),
|
||||
type: TDShapeType.Text,
|
||||
parentId: this.appState.currentPageId,
|
||||
text: TLDR.normalizeText(text.trim()),
|
||||
point: pagePoint,
|
||||
style: {
|
||||
...this.appState.currentStyle,
|
||||
textAlign: isMultiline ? AlignStyle.Start : this.appState.currentStyle.textAlign,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const pasteAsHTML = (html: string) => {
|
||||
const getShapeFromHtml = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1]
|
||||
|
||||
|
@ -1900,115 +1910,103 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
assets: TDAsset[]
|
||||
} = JSON.parse(maybeJson)
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
this.insertContent(json, { point, select: true })
|
||||
clipboardData = json
|
||||
return
|
||||
} else {
|
||||
throw Error('Not tldraw data!')
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html)
|
||||
return
|
||||
getShapeFromText(html)
|
||||
}
|
||||
}
|
||||
|
||||
if (e !== undefined) {
|
||||
const items = e.clipboardData?.items ?? []
|
||||
for (const index in items) {
|
||||
const item = items[index]
|
||||
const items = Array.from(e.clipboardData?.items ?? [])
|
||||
|
||||
// TODO
|
||||
// We could eventually support pasting multiple files / images,
|
||||
// and tiling them out on the canvas. At the moment, let's just
|
||||
// support pasting one file / image.
|
||||
await Promise.all(
|
||||
items.map(async (item) => {
|
||||
const { type, kind } = item
|
||||
|
||||
if (item.type === 'text/html') {
|
||||
item.getAsString(async (text) => {
|
||||
pasteAsHTML(text)
|
||||
})
|
||||
return
|
||||
} else {
|
||||
switch (item.kind) {
|
||||
switch (kind) {
|
||||
case 'string': {
|
||||
item.getAsString(async (text) => {
|
||||
if (text.startsWith('<svg')) {
|
||||
pasteTextAsSvg(text)
|
||||
} else {
|
||||
pasteTextAsShape(text)
|
||||
const str: string = await new Promise((resolve) => item.getAsString(resolve))
|
||||
|
||||
switch (type) {
|
||||
case 'text/html': {
|
||||
if (str.match(/<tldraw>(.*)<\/tldraw>/)?.[1]) {
|
||||
getShapeFromHtml(str)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
return
|
||||
case 'text/plain': {
|
||||
console.log(str)
|
||||
if (str.startsWith('<svg')) {
|
||||
getSvgFromText(str)
|
||||
} else {
|
||||
getShapeFromText(str)
|
||||
}
|
||||
// return
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'file': {
|
||||
const file = item.getAsFile()
|
||||
if (file) {
|
||||
this.addMediaFromFile(file)
|
||||
return
|
||||
}
|
||||
if (file) filesToPaste.push(file)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
getClipboard().then((clipboard) => {
|
||||
if (clipboard) {
|
||||
pasteAsHTML(clipboard)
|
||||
}
|
||||
})
|
||||
if (clipboardData) {
|
||||
this.insertContent(clipboardData, { point, select: true })
|
||||
return this
|
||||
}
|
||||
|
||||
if (navigator.clipboard) {
|
||||
const items = 'read' in navigator.clipboard ? await navigator.clipboard.read() : []
|
||||
if (filesToPaste.length) {
|
||||
this.addMediaFromFiles(filesToPaste, point)
|
||||
return this
|
||||
}
|
||||
|
||||
if (items.length === 0) return
|
||||
if (shapesToCreate.length) {
|
||||
const pagePoint = this.getPagePoint(point ?? this.centerPoint, this.currentPageId)
|
||||
|
||||
try {
|
||||
for (const item of items) {
|
||||
// look for png data.
|
||||
const currentPoint = Vec.add(pagePoint, [0, 0])
|
||||
|
||||
const pngData = await item.getType('text/png')
|
||||
shapesToCreate.forEach((shape, i) => {
|
||||
const bounds = TLDR.getBounds(shape)
|
||||
|
||||
if (pngData) {
|
||||
const file = new File([pngData], 'image.png')
|
||||
this.addMediaFromFile(file)
|
||||
return
|
||||
}
|
||||
|
||||
// look for svg data.
|
||||
|
||||
const svgData = await item.getType('image/svg+xml')
|
||||
|
||||
if (svgData) {
|
||||
const file = new File([svgData], 'image.svg')
|
||||
this.addMediaFromFile(file)
|
||||
return
|
||||
}
|
||||
|
||||
// look for plain text data.
|
||||
|
||||
const textData = await item.getType('text/plain')
|
||||
|
||||
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 (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
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if (this.clipboard) {
|
||||
// try to get clipboard data from the scene itself
|
||||
this.insertContent(this.clipboard)
|
||||
} else {
|
||||
TLDR.warn('This browser does not support the Clipboard API!')
|
||||
if (this.clipboard) {
|
||||
this.insertContent(this.clipboard, { point, select: true })
|
||||
}
|
||||
// last chance to get the clipboard data, is it in storage?
|
||||
getClipboard().then((text) => {
|
||||
if (text) getShapeFromHtml(text)
|
||||
})
|
||||
}
|
||||
|
||||
return this
|
||||
|
@ -3015,13 +3013,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
return this
|
||||
}
|
||||
|
||||
createImageOrVideoShapeAtPoint(
|
||||
getImageOrVideoShapeAtPoint(
|
||||
id: string,
|
||||
type: TDShapeType.Image | TDShapeType.Video,
|
||||
point: number[],
|
||||
size: number[],
|
||||
assetId: string
|
||||
): this {
|
||||
) {
|
||||
const {
|
||||
shapes,
|
||||
appState: { currentPageId, currentStyle },
|
||||
|
@ -3066,13 +3064,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
assetId,
|
||||
})
|
||||
|
||||
const bounds = Shape.getBounds(newShape as never)
|
||||
|
||||
newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2])
|
||||
|
||||
this.createShapes(newShape)
|
||||
|
||||
return this
|
||||
return newShape
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3394,93 +3386,135 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
return this
|
||||
}
|
||||
|
||||
addMediaFromFile = async (file: File, point = this.centerPoint) => {
|
||||
addMediaFromFiles = async (files: File[], point = this.centerPoint) => {
|
||||
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 extension = file.name.match(/\.[0-9a-z]+$/i)
|
||||
|
||||
if (!extension) throw Error('No extension')
|
||||
for (const file of files) {
|
||||
const id = Utils.uniqueId()
|
||||
const extension = file.name.match(/\.[0-9a-z]+$/i)
|
||||
|
||||
const isImage = IMAGE_EXTENSIONS.includes(extension[0].toLowerCase())
|
||||
const isVideo = VIDEO_EXTENSIONS.includes(extension[0].toLowerCase())
|
||||
if (!extension) throw Error('No extension')
|
||||
|
||||
if (!(isImage || isVideo)) throw Error('Wrong extension')
|
||||
const isImage = IMAGE_EXTENSIONS.includes(extension[0].toLowerCase())
|
||||
const isVideo = VIDEO_EXTENSIONS.includes(extension[0].toLowerCase())
|
||||
|
||||
const shapeType = isImage ? TDShapeType.Image : TDShapeType.Video
|
||||
const assetType = isImage ? TDAssetType.Image : TDAssetType.Video
|
||||
if (!(isImage || isVideo)) throw Error('Wrong extension')
|
||||
|
||||
let src: string | ArrayBuffer | null
|
||||
const shapeType = isImage ? TDShapeType.Image : TDShapeType.Video
|
||||
const assetType = isImage ? TDAssetType.Image : TDAssetType.Video
|
||||
|
||||
try {
|
||||
if (this.callbacks.onAssetCreate) {
|
||||
const result = await this.callbacks.onAssetCreate(this, file, id)
|
||||
let src: string | ArrayBuffer | null
|
||||
|
||||
if (!result) throw Error('Asset creation callback returned false')
|
||||
try {
|
||||
if (this.callbacks.onAssetCreate) {
|
||||
const result = await this.callbacks.onAssetCreate(this, file, id)
|
||||
|
||||
src = result
|
||||
} else {
|
||||
src = await fileToBase64(file)
|
||||
}
|
||||
if (!result) throw Error('Asset creation callback returned false')
|
||||
|
||||
if (typeof src === 'string') {
|
||||
let size = [0, 0]
|
||||
src = result
|
||||
} else {
|
||||
src = await fileToBase64(file)
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
// attempt to get actual svg size from viewBox attribute as
|
||||
if (extension[0] == '.svg') {
|
||||
let viewBox: string[]
|
||||
const svgString = await fileToText(file)
|
||||
const viewBoxAttribute = this.getViewboxFromSVG(svgString)
|
||||
if (typeof src === 'string') {
|
||||
let size = [0, 0]
|
||||
|
||||
if (viewBoxAttribute) {
|
||||
viewBox = viewBoxAttribute.split(' ')
|
||||
size[0] = parseFloat(viewBox[2])
|
||||
size[1] = parseFloat(viewBox[3])
|
||||
if (isImage) {
|
||||
// attempt to get actual svg size from viewBox attribute as
|
||||
if (extension[0] == '.svg') {
|
||||
let viewBox: string[]
|
||||
const svgString = await fileToText(file)
|
||||
const viewBoxAttribute = this.getViewboxFromSVG(svgString)
|
||||
|
||||
if (viewBoxAttribute) {
|
||||
viewBox = viewBoxAttribute.split(' ')
|
||||
size[0] = parseFloat(viewBox[2])
|
||||
size[1] = parseFloat(viewBox[3])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Vec.isEqual(size, [0, 0])) {
|
||||
size = await getImageSizeFromSrc(src)
|
||||
}
|
||||
} else {
|
||||
size = await getVideoSizeFromSrc(src)
|
||||
}
|
||||
|
||||
const match = Object.values(this.document.assets).find(
|
||||
(asset) => asset.type === assetType && asset.src === src
|
||||
)
|
||||
|
||||
let assetId: string
|
||||
|
||||
if (!match) {
|
||||
assetId = Utils.uniqueId()
|
||||
|
||||
const asset = {
|
||||
id: assetId,
|
||||
type: assetType,
|
||||
name: file.name,
|
||||
src,
|
||||
size,
|
||||
if (Vec.isEqual(size, [0, 0])) {
|
||||
size = await getImageSizeFromSrc(src)
|
||||
}
|
||||
} else {
|
||||
size = await getVideoSizeFromSrc(src)
|
||||
}
|
||||
|
||||
this.patchState({
|
||||
document: {
|
||||
assets: {
|
||||
[assetId]: asset,
|
||||
const match = Object.values(this.document.assets).find(
|
||||
(asset) => asset.type === assetType && asset.src === src
|
||||
)
|
||||
|
||||
let assetId: string
|
||||
|
||||
if (!match) {
|
||||
assetId = Utils.uniqueId()
|
||||
|
||||
const asset = {
|
||||
id: assetId,
|
||||
type: assetType,
|
||||
name: file.name,
|
||||
src,
|
||||
size,
|
||||
}
|
||||
|
||||
this.patchState({
|
||||
document: {
|
||||
assets: {
|
||||
[assetId]: asset,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
assetId = match.id
|
||||
})
|
||||
} else {
|
||||
assetId = match.id
|
||||
}
|
||||
|
||||
shapesToCreate.push(this.getImageOrVideoShapeAtPoint(id, shapeType, point, size, assetId))
|
||||
}
|
||||
} catch (error) {
|
||||
// Even if one shape errors, keep going (we might have had other shapes that didn't error)
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
this.createImageOrVideoShapeAtPoint(id, shapeType, pagePoint, size, assetId)
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
this.setIsLoading(false)
|
||||
return this
|
||||
}
|
||||
|
||||
this.setIsLoading(false)
|
||||
|
@ -3677,8 +3711,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
e.preventDefault()
|
||||
if (this.disableAssets) return this
|
||||
if (e.dataTransfer.files?.length) {
|
||||
const file = e.dataTransfer.files[0]
|
||||
this.addMediaFromFile(file, [e.clientX, e.clientY])
|
||||
this.addMediaFromFiles(Object.values(e.dataTransfer.files), [e.clientX, e.clientY])
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
|
|
@ -224,7 +224,7 @@ TldrawTestApp {
|
|||
},
|
||||
},
|
||||
"_status": "ready",
|
||||
"addMediaFromFile": [Function],
|
||||
"addMediaFromFiles": [Function],
|
||||
"addToSelectHistory": [Function],
|
||||
"align": [Function],
|
||||
"altKey": false,
|
||||
|
@ -482,16 +482,6 @@ TldrawTestApp {
|
|||
],
|
||||
"pan": [Function],
|
||||
"paste": [Function],
|
||||
"pasteInfo": Object {
|
||||
"center": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"offset": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"patchCreate": [Function],
|
||||
"patchState": [Function],
|
||||
"persist": [Function],
|
||||
|
@ -828,156 +818,3 @@ TldrawTestApp {
|
|||
"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
|
||||
const browserFS = await import('./browser-fs-access')
|
||||
const fileOpen = browserFS.fileOpen
|
||||
return fileOpen({
|
||||
description: 'Image or Video',
|
||||
extensions: [...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS],
|
||||
multiple: false,
|
||||
multiple: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue