Browse Source

fix: copy files into whiteboard

Peng Xiao 3 years ago
parent
commit
2dea98d038

+ 1 - 1
tldraw/apps/tldraw-logseq/src/app.tsx

@@ -67,7 +67,7 @@ export const App = function App(props: LogseqTldrawProps): JSX.Element {
     handlers: props.handlers,
   }
 
-  const onFileDrop = useFileDrop()
+  const onFileDrop = useFileDrop(contextValue)
   const onPaste = usePaste(contextValue)
   const onQuickAdd = useQuickAdd()
 

+ 5 - 47
tldraw/apps/tldraw-logseq/src/hooks/useFileDrop.ts

@@ -2,54 +2,12 @@ import { fileToBase64, getSizeFromSrc, TLAsset, uniqueId } from '@tldraw/core'
 import type { TLReactCallbacks } from '@tldraw/react'
 import * as React from 'react'
 import type { Shape } from '~lib'
+import type { LogseqContextValue } from '~lib/logseq-context'
+import { usePaste } from './usePaste'
 
-export function useFileDrop() {
+export function useFileDrop(context: LogseqContextValue) {
+  const handlePaste = usePaste(context)
   return React.useCallback<TLReactCallbacks<Shape>['onFileDrop']>(async (app, { files, point }) => {
-    const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
-    const assetId = uniqueId()
-    interface ImageAsset extends TLAsset {
-      size: number[]
-    }
-    const assetsToCreate: ImageAsset[] = []
-    for (const file of files) {
-      try {
-        // Get extension, verify that it's an image
-        const extensionMatch = file.name.match(/\.[0-9a-z]+$/i)
-        if (!extensionMatch) throw Error('No extension.')
-        const extension = extensionMatch[0].toLowerCase()
-        if (!IMAGE_EXTENSIONS.includes(extension)) continue
-        // Turn the image into a base64 dataurl
-        const dataurl = await fileToBase64(file)
-        if (typeof dataurl !== 'string') continue
-        // Do we already have an asset for this image?
-        const existingAsset = Object.values(app.assets).find(asset => asset.src === dataurl)
-        if (existingAsset) {
-          assetsToCreate.push(existingAsset as ImageAsset)
-          continue
-        }
-        // Create a new asset for this image
-        const asset: ImageAsset = {
-          id: assetId,
-          type: 'image',
-          src: dataurl,
-          size: await getSizeFromSrc(dataurl),
-        }
-        assetsToCreate.push(asset)
-      } catch (error) {
-        console.error(error)
-      }
-    }
-    app.createAssets(assetsToCreate)
-    app.createShapes(
-      assetsToCreate.map((asset, i) => ({
-        id: uniqueId(),
-        type: 'image',
-        parentId: app.currentPageId,
-        point: [point[0] - asset.size[0] / 2 + i * 16, point[1] - asset.size[1] / 2 + i * 16],
-        size: asset.size,
-        assetId: asset.id,
-        opacity: 1,
-      }))
-    )
+    handlePaste(app, { point, shiftKey: false, files })
   }, [])
 }

+ 214 - 181
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -31,59 +31,72 @@ const getYoutubeId = (url: string) => {
 export function usePaste(context: LogseqContextValue) {
   const { handlers } = context
 
-  return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(async (app, { point, shiftKey }) => {
-    const assetId = uniqueId()
-    interface ImageAsset extends TLAsset {
-      size: number[]
-    }
+  return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(
+    async (app, { point, shiftKey, files }) => {
+      const assetId = uniqueId()
+      interface ImageAsset extends TLAsset {
+        size: number[]
+      }
 
-    const assetsToCreate: ImageAsset[] = []
-    const shapesToCreate: Shape['props'][] = []
-    const bindingsToCreate: TLBinding[] = []
+      const assetsToCreate: ImageAsset[] = []
+      const shapesToCreate: Shape['props'][] = []
+      const bindingsToCreate: TLBinding[] = []
 
-    async function handleImage(item: ClipboardItem) {
-      const firstImageType = item.types.find(type => type.startsWith('image'))
-      if (firstImageType) {
-        const blob = await item.getType(firstImageType)
-        const dataurl = await fileToBase64(blob)
-        if (typeof dataurl !== 'string') return false
-        const existingAsset = Object.values(app.assets).find(asset => asset.src === dataurl)
-        if (existingAsset) {
-          assetsToCreate.push(existingAsset as ImageAsset)
-          return false
-        }
-        // Create a new asset for this image
-        const asset: ImageAsset = {
-          id: assetId,
-          type: 'image',
-          src: dataurl,
-          size: await getSizeFromSrc(dataurl),
-        }
-        assetsToCreate.push(asset)
-        return true
+      async function handleFiles(files: File[]) {
+        const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
+
+        const blobs = files.filter(file => {
+          // Get extension, verify that it's an image
+          const extensionMatch = file.name.match(/\.[0-9a-z]+$/i)
+          if (!extensionMatch) throw Error('No extension.')
+          const extension = extensionMatch[0].toLowerCase()
+          return IMAGE_EXTENSIONS.includes(extension)
+        })
+
+        return handleBlobs(blobs)
       }
-      return false
-    }
 
-    async function handleHTML(item: ClipboardItem) {
-      if (item.types.includes('text/html')) {
-        const blob = await item.getType('text/html')
-        const rawText = (await blob.text()).trim()
+      async function handleBlobs(blobs: Blob[]) {
+        for (const blob of blobs) {
+          try {
+            // Turn the image into a base64 dataurl
+            const dataurl = await fileToBase64(blob)
+            if (typeof dataurl !== 'string') continue
+            // Do we already have an asset for this image?
+            const existingAsset = Object.values(app.assets).find(asset => asset.src === dataurl)
+            if (existingAsset) {
+              assetsToCreate.push(existingAsset as ImageAsset)
+              continue
+            }
+            // Create a new asset for this image
+            const asset: ImageAsset = {
+              id: assetId,
+              type: 'image',
+              src: dataurl,
+              size: await getSizeFromSrc(dataurl),
+            }
+            assetsToCreate.push(asset)
+          } catch (error) {
+            console.error(error)
+          }
+        }
+      }
 
-        shapesToCreate.push({
-          ...HTMLShape.defaultProps,
-          html: rawText,
-          parentId: app.currentPageId,
-          point: [point[0], point[1]],
-        })
-        return true
+      async function handleImage(item: ClipboardItem) {
+        const firstImageType = item.types.find(type => type.startsWith('image'))
+        if (firstImageType) {
+          const blob = await item.getType(firstImageType)
+          await handleBlobs([blob])
+          return true
+        }
+        return false
       }
 
-      if (item.types.includes('text/plain')) {
-        const blob = await item.getType('text/plain')
-        const rawText = (await blob.text()).trim()
-        // if rawText is iframe text
-        if (rawText.startsWith('<iframe')) {
+      async function handleHTML(item: ClipboardItem) {
+        if (item.types.includes('text/html')) {
+          const blob = await item.getType('text/html')
+          const rawText = (await blob.text()).trim()
+
           shapesToCreate.push({
             ...HTMLShape.defaultProps,
             html: rawText,
@@ -92,171 +105,191 @@ export function usePaste(context: LogseqContextValue) {
           })
           return true
         }
+
+        if (item.types.includes('text/plain')) {
+          const blob = await item.getType('text/plain')
+          const rawText = (await blob.text()).trim()
+          // if rawText is iframe text
+          if (rawText.startsWith('<iframe')) {
+            shapesToCreate.push({
+              ...HTMLShape.defaultProps,
+              html: rawText,
+              parentId: app.currentPageId,
+              point: [point[0], point[1]],
+            })
+            return true
+          }
+        }
+        return false
       }
-      return false
-    }
 
-    async function handleLogseqShapes(item: ClipboardItem) {
-      if (item.types.includes('text/plain')) {
-        const blob = await item.getType('text/plain')
-        const rawText = (await blob.text()).trim()
+      async function handleLogseqShapes(item: ClipboardItem) {
+        if (item.types.includes('text/plain')) {
+          const blob = await item.getType('text/plain')
+          const rawText = (await blob.text()).trim()
 
-        try {
-          const data = JSON.parse(rawText)
-          if (data.type === 'logseq/whiteboard-shapes') {
-            const shapes = data.shapes as TLShapeModel[]
-            const commonBounds = BoundsUtils.getCommonBounds(
-              shapes.map(shape => ({
-                minX: shape.point?.[0] ?? point[0],
-                minY: shape.point?.[1] ?? point[1],
-                width: shape.size?.[0] ?? 4,
-                height: shape.size?.[1] ?? 4,
-                maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
-                maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
-              }))
-            )
-            const clonedShapes = shapes.map(shape => {
-              return {
-                ...shape,
-                parentId: app.currentPageId,
-                point: [
-                  point[0] + shape.point![0] - commonBounds.minX,
-                  point[1] + shape.point![1] - commonBounds.minY,
-                ],
-              }
-            })
-            // @ts-expect-error - This is a valid shape
-            shapesToCreate.push(...clonedShapes)
+          try {
+            const data = JSON.parse(rawText)
+            if (data.type === 'logseq/whiteboard-shapes') {
+              const shapes = data.shapes as TLShapeModel[]
+              const commonBounds = BoundsUtils.getCommonBounds(
+                shapes.map(shape => ({
+                  minX: shape.point?.[0] ?? point[0],
+                  minY: shape.point?.[1] ?? point[1],
+                  width: shape.size?.[0] ?? 4,
+                  height: shape.size?.[1] ?? 4,
+                  maxX: (shape.point?.[0] ?? point[0]) + (shape.size?.[0] ?? 4),
+                  maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
+                }))
+              )
+              const clonedShapes = shapes.map(shape => {
+                return {
+                  ...shape,
+                  parentId: app.currentPageId,
+                  point: [
+                    point[0] + shape.point![0] - commonBounds.minX,
+                    point[1] + shape.point![1] - commonBounds.minY,
+                  ],
+                }
+              })
+              // @ts-expect-error - This is a valid shape
+              shapesToCreate.push(...clonedShapes)
 
-            // Try to rebinding the shapes to the new assets
-            shapesToCreate.forEach((s, idx) => {
-              if (s.handles) {
-                Object.values(s.handles).forEach(h => {
-                  if (h.bindingId) {
-                    // try to bind the new shape
-                    const binding = app.currentPage.bindings[h.bindingId]
-                    // if the copied binding from/to is in the source
-                    const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
-                    const oldToIdx = shapes.findIndex(s => s.id === binding.toId)
-                    if (binding && oldFromIdx !== -1 && oldToIdx !== -1) {
-                      const newBinding: TLBinding = {
-                        ...binding,
-                        id: uniqueId(),
-                        fromId: shapesToCreate[oldFromIdx].id,
-                        toId: shapesToCreate[oldToIdx].id,
+              // Try to rebinding the shapes to the new assets
+              shapesToCreate.forEach((s, idx) => {
+                if (s.handles) {
+                  Object.values(s.handles).forEach(h => {
+                    if (h.bindingId) {
+                      // try to bind the new shape
+                      const binding = app.currentPage.bindings[h.bindingId]
+                      // if the copied binding from/to is in the source
+                      const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
+                      const oldToIdx = shapes.findIndex(s => s.id === binding.toId)
+                      if (binding && oldFromIdx !== -1 && oldToIdx !== -1) {
+                        const newBinding: TLBinding = {
+                          ...binding,
+                          id: uniqueId(),
+                          fromId: shapesToCreate[oldFromIdx].id,
+                          toId: shapesToCreate[oldToIdx].id,
+                        }
+                        bindingsToCreate.push(newBinding)
+                        h.bindingId = newBinding.id
+                      } else {
+                        h.bindingId = undefined
                       }
-                      bindingsToCreate.push(newBinding)
-                      h.bindingId = newBinding.id
-                    } else {
-                      h.bindingId = undefined
                     }
-                  }
+                  })
+                }
+              })
+              return true
+            }
+          } catch {
+            if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
+              const blockRef = rawText.slice(2, -2)
+              if (validUUID(blockRef)) {
+                shapesToCreate.push({
+                  ...LogseqPortalShape.defaultProps,
+                  parentId: app.currentPageId,
+                  point: [point[0], point[1]],
+                  size: [400, 0], // use 0 here to enable auto-resize
+                  pageId: blockRef,
+                  blockType: 'B',
                 })
+                return true
               }
-            })
-            return true
-          }
-        } catch {
-          if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
-            const blockRef = rawText.slice(2, -2)
-            if (validUUID(blockRef)) {
+            } else if (/^\[\[.*\]\]$/.test(rawText)) {
+              const pageName = rawText.slice(2, -2)
               shapesToCreate.push({
                 ...LogseqPortalShape.defaultProps,
                 parentId: app.currentPageId,
                 point: [point[0], point[1]],
                 size: [400, 0], // use 0 here to enable auto-resize
-                pageId: blockRef,
-                blockType: 'B',
+                pageId: pageName,
+                blockType: 'P',
               })
               return true
+            } else if (isValidURL(rawText)) {
+              const youtubeId = getYoutubeId(rawText)
+              if (youtubeId) {
+                shapesToCreate.push({
+                  ...YouTubeShape.defaultProps,
+                  embedId: youtubeId,
+                  parentId: app.currentPageId,
+                  point: [point[0], point[1]],
+                })
+                return true
+              }
+              // ??? deal with normal URLs?
             }
-          } else if (/^\[\[.*\]\]$/.test(rawText)) {
-            const pageName = rawText.slice(2, -2)
-            shapesToCreate.push({
-              ...LogseqPortalShape.defaultProps,
-              parentId: app.currentPageId,
-              point: [point[0], point[1]],
-              size: [400, 0], // use 0 here to enable auto-resize
-              pageId: pageName,
-              blockType: 'P',
-            })
-            return true
-          } else if (isValidURL(rawText)) {
-            const youtubeId = getYoutubeId(rawText)
-            if (youtubeId) {
+            const uuid = handlers?.addNewBlock(rawText)
+            if (uuid) {
+              // create text shape
               shapesToCreate.push({
-                ...YouTubeShape.defaultProps,
-                embedId: youtubeId,
+                ...LogseqPortalShape.defaultProps,
+                id: uniqueId(),
                 parentId: app.currentPageId,
+                size: [400, 0], // use 0 here to enable auto-resize
                 point: [point[0], point[1]],
+                pageId: uuid,
+                blockType: 'B',
+                compact: true,
               })
               return true
             }
-            // ??? deal with normal URLs?
-          }
-          const uuid = handlers?.addNewBlock(rawText)
-          if (uuid) {
-            // create text shape
-            shapesToCreate.push({
-              ...LogseqPortalShape.defaultProps,
-              id: uniqueId(),
-              parentId: app.currentPageId,
-              size: [400, 0], // use 0 here to enable auto-resize
-              point: [point[0], point[1]],
-              pageId: uuid,
-              blockType: 'B',
-              compact: true,
-            })
-            return true
           }
         }
+        return false
       }
-      return false
-    }
 
-    for (const item of await navigator.clipboard.read()) {
-      try {
-        let handled = await handleImage(item)
+      if (files) {
+        await handleFiles(files)
+      } else {
+        for (const item of await navigator.clipboard.read()) {
+          try {
+            let handled = await handleImage(item)
 
-        if (!handled && !shiftKey) {
-          handled = await handleHTML(item)
-        }
+            if (!handled && !shiftKey) {
+              handled = await handleHTML(item)
+            }
 
-        if (!handled) {
-          await handleLogseqShapes(item)
+            if (!handled) {
+              await handleLogseqShapes(item)
+            }
+          } catch (error) {
+            console.error(error)
+          }
         }
-      } catch (error) {
-        console.error(error)
       }
-    }
 
-    const allShapesToAdd: TLShapeModel[] = [
-      ...assetsToCreate.map((asset, i) => ({
-        type: 'image',
-        parentId: app.currentPageId,
-        // TODO: Should be place near the last edited shape
-        point: [point[0] - asset.size[0] / 2 + i * 16, point[1] - asset.size[1] / 2 + i * 16],
-        size: asset.size,
-        assetId: asset.id,
-        opacity: 1,
-      })),
-      ...shapesToCreate,
-    ].map(shape => {
-      return {
-        ...shape,
-        id: uniqueId(),
-      }
-    })
+      const allShapesToAdd: TLShapeModel[] = [
+        ...assetsToCreate.map((asset, i) => ({
+          type: 'image',
+          parentId: app.currentPageId,
+          // TODO: Should be place near the last edited shape
+          point: [point[0] - asset.size[0] / 2 + i * 16, point[1] - asset.size[1] / 2 + i * 16],
+          size: asset.size,
+          assetId: asset.id,
+          opacity: 1,
+        })),
+        ...shapesToCreate,
+      ].map(shape => {
+        return {
+          ...shape,
+          id: uniqueId(),
+        }
+      })
 
-    app.wrapUpdate(() => {
-      if (assetsToCreate.length > 0) {
-        app.createAssets(assetsToCreate)
-      }
-      if (allShapesToAdd.length > 0) {
-        app.createShapes(allShapesToAdd)
-      }
-      app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
-      app.setSelectedShapes(allShapesToAdd.map(s => s.id))
-    })
-  }, [])
+      app.wrapUpdate(() => {
+        if (assetsToCreate.length > 0) {
+          app.createAssets(assetsToCreate)
+        }
+        if (allShapesToAdd.length > 0) {
+          app.createShapes(allShapesToAdd)
+        }
+        app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
+        app.setSelectedShapes(allShapesToAdd.map(s => s.id))
+      })
+    },
+    []
+  )
 }

+ 2 - 0
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -420,9 +420,11 @@ export class TLApp<
 
   paste = (e?: ClipboardEvent, shiftKey?: boolean) => {
     if (!this.editingShape) {
+      const fileList = e?.clipboardData?.files
       this.notify('paste', {
         point: this.inputs.currentPoint,
         shiftKey: !!shiftKey,
+        files: fileList ? Array.from(fileList) : undefined,
       })
     }
   }

+ 1 - 1
tldraw/packages/core/src/types/types.ts

@@ -158,7 +158,7 @@ export type TLSubscriptionEvent =
     }
   | {
       event: 'paste'
-      info: { point: number[]; shiftKey: boolean }
+      info: { point: number[]; shiftKey: boolean; files?: File[] }
     }
   | {
       event: 'create-assets'