Browse Source

refactor: handling DataTransfer

Peng Xiao 3 years ago
parent
commit
803ec01b24

+ 40 - 36
tldraw/apps/tldraw-logseq/src/app.tsx

@@ -61,27 +61,13 @@ interface LogseqTldrawProps {
   onPersist?: TLReactCallbacks<Shape>['onPersist']
 }
 
-export const App = function App({
+const AppInner = ({
   onPersist,
-  handlers,
-  renderers,
   model,
   ...rest
-}: LogseqTldrawProps): JSX.Element {
-  const memoRenders: any = React.useMemo(() => {
-    return Object.fromEntries(
-      Object.entries(renderers).map(([key, comp]) => {
-        return [key, React.memo(comp)]
-      })
-    )
-  }, [])
-  const contextValue = {
-    renderers: memoRenders,
-    handlers: handlers,
-  }
-
-  const onDrop = useDrop(contextValue)
-  const onPaste = usePaste(contextValue)
+}: Omit<LogseqTldrawProps, 'renderers' | 'handlers'>) => {
+  const onDrop = useDrop()
+  const onPaste = usePaste()
   const onQuickAdd = useQuickAdd()
   const ref = React.useRef<HTMLDivElement>(null)
 
@@ -94,26 +80,44 @@ export const App = function App({
     [model]
   )
 
+  return (
+    <AppProvider
+      Shapes={shapes}
+      Tools={tools}
+      onDrop={onDrop}
+      onPaste={onPaste}
+      onCanvasDBClick={onQuickAdd}
+      onPersist={onPersistOnDiff}
+      model={model}
+      {...rest}
+    >
+      <ContextMenu collisionRef={ref}>
+        <div ref={ref} className="logseq-tldraw logseq-tldraw-wrapper">
+          <AppCanvas components={components}>
+            <AppUI />
+          </AppCanvas>
+        </div>
+      </ContextMenu>
+    </AppProvider>
+  )
+}
+
+export const App = function App({ renderers, handlers, ...rest }: LogseqTldrawProps): JSX.Element {
+  const memoRenders: any = React.useMemo(() => {
+    return Object.fromEntries(
+      Object.entries(renderers).map(([key, comp]) => {
+        return [key, React.memo(comp)]
+      })
+    )
+  }, [])
+  const contextValue = {
+    renderers: memoRenders,
+    handlers: handlers,
+  }
+
   return (
     <LogseqContext.Provider value={contextValue}>
-      <AppProvider
-        Shapes={shapes}
-        Tools={tools}
-        onDrop={onDrop}
-        onPaste={onPaste}
-        onCanvasDBClick={onQuickAdd}
-        onPersist={onPersistOnDiff}
-        model={model}
-        {...rest}
-      >
-        <ContextMenu collisionRef={ref}>
-          <div ref={ref} className="logseq-tldraw logseq-tldraw-wrapper">
-            <AppCanvas components={components}>
-              <AppUI />
-            </AppCanvas>
-          </div>
-        </ContextMenu>
-      </AppProvider>
+      <AppInner {...rest} />
     </LogseqContext.Provider>
   )
 }

+ 5 - 1
tldraw/apps/tldraw-logseq/src/components/Devtools/Devtools.tsx

@@ -25,7 +25,11 @@ const HistoryStack = observer(function HistoryStack() {
   }, [])
 
   React.useEffect(() => {
-    anchorRef.current?.querySelector(`[data-item-index="${app.history.pointer}"]`)?.scrollIntoView()
+    requestIdleCallback(() => {
+      anchorRef.current
+        ?.querySelector(`[data-item-index="${app.history.pointer}"]`)
+        ?.scrollIntoView()
+    })
   }, [app.history.pointer])
 
   return anchorRef.current

+ 8 - 6
tldraw/apps/tldraw-logseq/src/hooks/useDrop.ts

@@ -1,12 +1,14 @@
 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 useDrop(context: LogseqContextValue) {
-  const handlePaste = usePaste(context)
-  return React.useCallback<TLReactCallbacks<Shape>['onDrop']>(async (app, { dataTransfer, point }) => {
-    handlePaste(app, { point, shiftKey: false, dataTransfer })
-  }, [])
+export function useDrop() {
+  const handlePaste = usePaste()
+  return React.useCallback<TLReactCallbacks<Shape>['onDrop']>(
+    async (app, { dataTransfer, point }) => {
+      handlePaste(app, { point, shiftKey: false, dataTransfer })
+    },
+    []
+  )
 }

+ 279 - 269
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -1,7 +1,7 @@
-import { Item, Label } from '@radix-ui/react-select'
 import {
   BoundsUtils,
   getSizeFromSrc,
+  isNonNullable,
   TLAsset,
   TLBinding,
   TLCursor,
@@ -14,17 +14,15 @@ import Vec from '@tldraw/vec'
 import * as React from 'react'
 import { NIL as NIL_UUID } from 'uuid'
 import {
-  type Shape,
   HTMLShape,
-  YouTubeShape,
+  IFrameShape,
+  ImageShape,
   LogseqPortalShape,
   VideoShape,
-  TextShape,
-  LineShape,
-  ImageShape,
-  IFrameShape,
+  YouTubeShape,
+  type Shape,
 } from '../lib'
-import type { LogseqContextValue } from '../lib/logseq-context'
+import { LogseqContext } from '../lib/logseq-context'
 
 const isValidURL = (url: string) => {
   try {
@@ -43,197 +41,218 @@ const safeParseJson = (json: string) => {
   }
 }
 
+interface VideoImageAsset extends TLAsset {
+  size?: number[]
+}
+
 const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
 const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.ogg']
 
+function getFileType(filename: string) {
+  // Get extension, verify that it's an image
+  const extensionMatch = filename.match(/\.[0-9a-z]+$/i)
+  if (!extensionMatch) {
+    return 'unknown'
+  }
+  const extension = extensionMatch[0].toLowerCase()
+  if (IMAGE_EXTENSIONS.includes(extension)) {
+    return 'image'
+  }
+  if (VIDEO_EXTENSIONS.includes(extension)) {
+    return 'video'
+  }
+  return 'unknown'
+}
+
+type MaybeShapes = Shape['props'][] | null | undefined
+
+type CreateShapeFN<Args extends any[]> = (...args: Args) => Promise<MaybeShapes> | MaybeShapes
+
+/**
+ * Try create a shape from a list of create shape functions. If one of the functions returns a
+ * shape, return it, otherwise try again for the next one until all have been tried.
+ */
+async function tryCreateShapeHelper<Args extends any[]>(fns: CreateShapeFN<Args>[], ...args: Args) {
+  for (const fn of fns) {
+    const result = await fn(...(args as any))
+    if (result && result.length > 0) {
+      return result
+    }
+  }
+  return null
+}
+
 // FIXME: for assets, we should prompt the user a loading spinner
-export function usePaste(context: LogseqContextValue) {
-  const { handlers } = context
+export function usePaste() {
+  const { handlers } = React.useContext(LogseqContext)
 
   return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(
     async (app, { point, shiftKey, dataTransfer }) => {
-      interface VideoImageAsset extends TLAsset {
-        size: number[]
-      }
-
-      const imageAssetsToCreate: VideoImageAsset[] = []
+      let imageAssetsToCreate: VideoImageAsset[] = []
       let assetsToClone: TLAsset[] = []
       const shapesToCreate: Shape['props'][] = []
       const bindingsToCreate: TLBinding[] = []
 
-      async function createAsset(file: File): Promise<string | null> {
-        return await handlers.saveAsset(file)
-      }
-
-      async function handleAssetUrl(url: string, isVideo: boolean) {
+      async function createAssetsFromURL(url: string, isVideo: boolean): Promise<VideoImageAsset> {
         // Do we already have an asset for this image?
         const existingAsset = Object.values(app.assets).find(asset => asset.src === url)
         if (existingAsset) {
-          imageAssetsToCreate.push(existingAsset as VideoImageAsset)
-          return true
+          return existingAsset as VideoImageAsset
         } else {
-          try {
-            // Create a new asset for this image
-            const asset: VideoImageAsset = {
-              id: uniqueId(),
-              type: isVideo ? 'video' : 'image',
-              src: url,
-              size: await getSizeFromSrc(handlers.makeAssetUrl(url), isVideo),
-            }
-            imageAssetsToCreate.push(asset)
-            return true
-          } catch {
-            return false
+          // Create a new asset for this image
+          const asset: VideoImageAsset = {
+            id: uniqueId(),
+            type: isVideo ? 'video' : 'image',
+            src: url,
+            size: await getSizeFromSrc(handlers.makeAssetUrl(url), isVideo),
           }
+          return asset
         }
       }
 
-      // TODO: handle PDF?
-      async function handleFiles(files: File[]) {
-        let added = false
-        for (const file of files) {
-          // Get extension, verify that it's an image
-          const extensionMatch = file.name.match(/\.[0-9a-z]+$/i)
-          if (!extensionMatch) {
-            continue
-          }
-          const extension = extensionMatch[0].toLowerCase()
-          if (![...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS].includes(extension)) {
-            continue
-          }
-          const isVideo = VIDEO_EXTENSIONS.includes(extension)
-          try {
-            // Turn the image into a base64 dataurl
-            const dataurl = await createAsset(file)
-            if (!dataurl) {
-              continue
-            }
-            if (await handleAssetUrl(dataurl, isVideo)) {
-              added = true
+      async function createAssetsFromFiles(files: File[]) {
+        const tasks = files
+          .filter(file => getFileType(file.name) !== 'unknown')
+          .map(async file => {
+            try {
+              const dataurl = await handlers.saveAsset(file)
+              return await createAssetsFromURL(dataurl, getFileType(file.name) === 'video')
+            } catch (err) {
+              console.error(err)
             }
-          } catch (error) {
-            console.error(error)
-          }
-        }
-        return added
+            return null
+          })
+        return (await Promise.all(tasks)).filter(isNonNullable)
       }
 
-      async function handleItems(items: any) {
-        for (const item of items) {
-          if (await handleDroppedItem(item)) {
-            const lineId = uniqueId()
-
-            const startBinding: TLBinding = {
-              id: uniqueId(),
-              distance: 200,
-              handleId: 'start',
-              fromId: lineId,
-              toId: app.selectedShapesArray[app.selectedShapesArray.length - 1].id,
-              point: [point[0], point[1]],
-            }
-            bindingsToCreate.push(startBinding)
-
-            const endBinding: TLBinding = {
-              id: uniqueId(),
-              distance: 200,
-              handleId: 'end',
-              fromId: lineId,
-              toId: shapesToCreate[shapesToCreate.length - 1].id,
-              point: [point[0], point[1]],
-            }
-            bindingsToCreate.push(endBinding)
-
-            shapesToCreate.push({
-              ...LineShape.defaultProps,
-              id: lineId,
-              handles: {
-                start: { id: 'start', canBind: true, point: app.selectedShapesArray[0].getCenter(), bindingId: startBinding.id },
-                end: { id: 'end', canBind: true, point: [point[0], point[1]], bindingId: endBinding.id },
-              }
-            })
-
-            return true
-          }
+      function createHTMLShape(text: string) {
+        return {
+          ...HTMLShape.defaultProps,
+          html: text,
+          point: [point[0], point[1]],
         }
-        return false
       }
 
-      async function handleTransfer(dataTransfer: DataTransfer) {
-        let added = false
-
-        if ((dataTransfer.files?.length && await handleFiles(Array.from(dataTransfer.files))) ||
-            (dataTransfer.items?.length && await handleItems(Array.from(dataTransfer.items).map((item: any) => ({type: item.type, text: dataTransfer.getData(item.type)}))))  ) {
-          added = true
-        }
-
-        return added
+      // async function handleItems(items: any) {
+      //   for (const item of items) {
+      //     if (await handleDroppedItem(item)) {
+      //       const lineId = uniqueId()
+
+      //       const startBinding: TLBinding = {
+      //         id: uniqueId(),
+      //         distance: 200,
+      //         handleId: 'start',
+      //         fromId: lineId,
+      //         toId: app.selectedShapesArray[app.selectedShapesArray.length - 1].id,
+      //         point: [point[0], point[1]],
+      //       }
+      //       bindingsToCreate.push(startBinding)
+
+      //       const endBinding: TLBinding = {
+      //         id: uniqueId(),
+      //         distance: 200,
+      //         handleId: 'end',
+      //         fromId: lineId,
+      //         toId: shapesToCreate[shapesToCreate.length - 1].id,
+      //         point: [point[0], point[1]],
+      //       }
+      //       bindingsToCreate.push(endBinding)
+
+      //       shapesToCreate.push({
+      //         ...LineShape.defaultProps,
+      //         id: lineId,
+      //         handles: {
+      //           start: {
+      //             id: 'start',
+      //             canBind: true,
+      //             point: app.selectedShapesArray[0].getCenter(),
+      //             bindingId: startBinding.id,
+      //           },
+      //           end: {
+      //             id: 'end',
+      //             canBind: true,
+      //             point: [point[0], point[1]],
+      //             bindingId: endBinding.id,
+      //           },
+      //         },
+      //       })
+
+      //       return true
+      //     }
+      //   }
+      //   return false
+      // }
+
+      async function tryCreateShapesFromDataTransfer(dataTransfer: DataTransfer) {
+        return tryCreateShapeHelper(
+          [tryCreateShapeFromFiles, tryCreateShapeFromTextHTML, tryCreateShapeFromTextPlain],
+          dataTransfer
+        )
       }
 
-      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,
-            point: [point[0], point[1]],
-          })
-          return true
-        }
-        return false
-      }
+      async function tryCreateShapeFromFiles(dataTransfer: DataTransfer) {
+        const files = Array.from(dataTransfer.files)
+        if (files.length > 0) {
+          const assets = await createAssetsFromFiles(files)
+          // ? could we get rid of this side effect?
+          imageAssetsToCreate = assets
+
+          return assets.map((asset, i) => {
+            const defaultProps =
+              asset.type === 'video' ? VideoShape.defaultProps : ImageShape.defaultProps
+            const newShape = {
+              ...defaultProps,
+              // TODO: Should be place near the last edited shape
+              assetId: asset.id,
+              opacity: 1,
+            }
 
-      async function handleDroppedItem(item: any) {
-        switch(item.type) {
-          case 'text/html':
-            shapesToCreate.push({
-              ...HTMLShape.defaultProps,
-              html: item.text,
-              point: [point[0], point[1]],
-            })
-            return true
-          case 'text/plain':
-            if (await handleURL(item.text)) {
-              return true
+            if (asset.size) {
+              Object.assign(newShape, {
+                point: [
+                  point[0] - asset.size[0] / 4 + i * 16,
+                  point[1] - asset.size[1] / 4 + i * 16,
+                ],
+                size: Vec.div(asset.size, 2),
+              })
             }
 
-            shapesToCreate.push({
-              ...TextShape.defaultProps,
-              text: item.text,
-              point: [point[0], point[1]],
-            })
-            return true
-          default:
-            return false
+            return newShape
+          })
         }
+        return null
       }
 
-      async function handleTextPlain(item: ClipboardItem) {
-        if (item.types.includes('text/plain')) {
-          const blob = await item.getType('text/plain')
-          const rawText = (await blob.text()).trim()
-
-          if (await handleURL(rawText)) {
-            return true
-          }
+      function tryCreateShapeFromTextHTML(dataTransfer: DataTransfer) {
+        if (dataTransfer.types.includes('text/html') && !shiftKey) {
+          const html = dataTransfer.getData('text/html')
 
-          if (handleIframe(rawText)) {
-            return true
+          if (html) {
+            return [createHTMLShape(html)]
           }
+        }
+        return null
+      }
 
-          if (handleTldrawShapes(rawText)) {
-            return true
-          }
-          if (await handleLogseqPortalShapes(rawText)) {
-            return true
-          }
+      async function tryCreateShapeFromTextPlain(dataTransfer: DataTransfer) {
+        if (dataTransfer.types.includes('text/plain')) {
+          const text = dataTransfer.getData('text/plain').trim()
+
+          return tryCreateShapeHelper(
+            [
+              tryCreateShapeFromURL,
+              tryCreateShapeFromIframeString,
+              tryCreateClonedShapesFromJSON,
+              tryCreateLogseqPortalShapesFromString,
+            ],
+            text
+          )
         }
 
-        return false
+        return null
       }
 
-      function handleTldrawShapes(rawText: string) {
+      function tryCreateClonedShapesFromJSON(rawText: string) {
         const data = safeParseJson(rawText)
         try {
           if (data?.type === 'logseq/whiteboard-shapes') {
@@ -249,7 +268,7 @@ export function usePaste(context: LogseqContextValue) {
                 maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
               }))
             )
-            const clonedShapes = shapes.map(shape => {
+            const shapesToCreate = shapes.map(shape => {
               return {
                 ...shape,
                 point: [
@@ -258,49 +277,48 @@ export function usePaste(context: LogseqContextValue) {
                 ],
               }
             })
-            // @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]
-                    // FIXME: if copy from a different whiteboard, the binding info
-                    // will not be available
-                    if (binding) {
-                      // 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
-                      }
+            shapesToCreate
+              .flatMap(s => Object.values(s.handles ?? {}))
+              .forEach(h => {
+                if (!h.bindingId) {
+                  return
+                }
+                // try to bind the new shape
+                const binding = app.currentPage.bindings[h.bindingId]
+                // FIXME: if copy from a different whiteboard, the binding info
+                // will not be available
+                if (binding) {
+                  // 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
                   }
-                })
-              }
-            })
+                } else {
+                  console.warn('binding not found', h.bindingId)
+                }
+              })
 
-            return true
+            return shapesToCreate as Shape['props'][]
           }
         } catch (err) {
           console.error(err)
         }
-        return false
+        return null
       }
 
-      async function handleURL(rawText: string) {
+      async function tryCreateShapeFromURL(rawText: string) {
         if (isValidURL(rawText)) {
           const isYoutubeUrl = (url: string) => {
             const youtubeRegex =
@@ -308,116 +326,108 @@ export function usePaste(context: LogseqContextValue) {
             return youtubeRegex.test(url)
           }
           if (isYoutubeUrl(rawText)) {
-            shapesToCreate.push({
-              ...YouTubeShape.defaultProps,
-              url: rawText,
-              point: [point[0], point[1]],
-            })
-            return true
-          }
-          const extension = rawText.match(/\.[0-9a-z]+$/i)?.[0].toLowerCase()
-          if (
-            extension &&
-            [...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS].includes(extension) &&
-            (await handleAssetUrl(rawText, VIDEO_EXTENSIONS.includes(extension)))
-          ) {
-            return true
+            return [
+              {
+                ...YouTubeShape.defaultProps,
+                url: rawText,
+                point: [point[0], point[1]],
+              },
+            ]
           }
 
-          shapesToCreate.push({
-            ...IFrameShape.defaultProps,
-            url: rawText,
-            point: [point[0], point[1]],
-          })
-          return true
+          return [
+            {
+              ...IFrameShape.defaultProps,
+              url: rawText,
+              point: [point[0], point[1]],
+            },
+          ]
         }
-        return false
+        return null
       }
 
-      function handleIframe(rawText: string) {
+      function tryCreateShapeFromIframeString(rawText: string) {
         // if rawText is iframe text
         if (rawText.startsWith('<iframe')) {
-          shapesToCreate.push({
-            ...HTMLShape.defaultProps,
-            html: rawText,
-            point: [point[0], point[1]],
-          })
-          return true
+          return [
+            {
+              ...HTMLShape.defaultProps,
+              html: rawText,
+              point: [point[0], point[1]],
+            },
+          ]
         }
-        return false
+        return null
       }
 
-      async function handleLogseqPortalShapes(rawText: string) {
+      async function tryCreateLogseqPortalShapesFromString(rawText: string) {
         if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
           const blockRef = rawText.slice(2, -2)
           if (validUUID(blockRef)) {
-            shapesToCreate.push({
+            return [
+              {
+                ...LogseqPortalShape.defaultProps,
+                point: [point[0], point[1]],
+                size: [400, 0], // use 0 here to enable auto-resize
+                pageId: blockRef,
+                blockType: 'B' as 'B',
+              },
+            ]
+          }
+        }
+        // [[page name]] ?
+        else if (/^\[\[.*\]\]$/.test(rawText)) {
+          const pageName = rawText.slice(2, -2)
+          return [
+            {
               ...LogseqPortalShape.defaultProps,
               point: [point[0], point[1]],
               size: [400, 0], // use 0 here to enable auto-resize
-              pageId: blockRef,
-              blockType: 'B',
-            })
-            return true
-          }
-        } else if (/^\[\[.*\]\]$/.test(rawText)) {
-          const pageName = rawText.slice(2, -2)
-          shapesToCreate.push({
-            ...LogseqPortalShape.defaultProps,
-            point: [point[0], point[1]],
-            size: [400, 0], // use 0 here to enable auto-resize
-            pageId: pageName,
-            blockType: 'P',
-          })
-          return true
+              pageId: pageName,
+              blockType: 'P' as 'P',
+            },
+          ]
         }
 
+        // Otherwise, creating a new block that belongs to the current whiteboard
         const uuid = handlers?.addNewBlock(rawText)
         if (uuid) {
           // create text shape
-          shapesToCreate.push({
-            ...LogseqPortalShape.defaultProps,
-            id: uniqueId(),
-            size: [400, 0], // use 0 here to enable auto-resize
-            point: [point[0], point[1]],
-            pageId: uuid,
-            blockType: 'B',
-            compact: true,
-          })
-          return true
+          return [
+            {
+              ...LogseqPortalShape.defaultProps,
+              id: uniqueId(),
+              size: [400, 0], // use 0 here to enable auto-resize
+              point: [point[0], point[1]],
+              pageId: uuid,
+              blockType: 'B' as 'B',
+              compact: true,
+            },
+          ]
         }
-        return false
+
+        return null
       }
 
       app.cursors.setCursor(TLCursor.Progress)
 
       try {
-        if (dataTransfer) {
-          await handleTransfer(dataTransfer)
+        const shapesFromDataTransfer = dataTransfer
+          ? await tryCreateShapesFromDataTransfer(dataTransfer)
+          : null
+        if (shapesFromDataTransfer) {
+          shapesToCreate.push(...shapesFromDataTransfer)
         } else {
-          for (const item of await navigator.clipboard.read()) {
-            let handled = !shiftKey ? await handleHTML(item) : false
-            if (!handled) {
-              await handleTextPlain(item)
-            }
-          }
+          // from Clipboard app or Shift copy etc
+          // in this case, we do not have the dataTransfer object
         }
       } catch (error) {
         console.error(error)
       }
 
-      const allShapesToAdd: TLShapeModel[] = [
-        // assets to images
-        ...imageAssetsToCreate.map((asset, i) => ({
-          ...(asset.type === 'video' ? VideoShape : ImageShape).defaultProps,
-          // TODO: Should be place near the last edited shape
-          point: [point[0] - asset.size[0] / 4 + i * 16, point[1] - asset.size[1] / 4 + i * 16],
-          size: Vec.div(asset.size, 2),
-          assetId: asset.id,
-          opacity: 1,
-        })),
-        ...shapesToCreate,
-      ].map(shape => {
+      console.log(bindingsToCreate)
+
+      const allShapesToAdd: TLShapeModel[] = shapesToCreate.map(shape => {
         return {
           ...shape,
           parentId: app.currentPageId,

+ 1 - 1
tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts

@@ -36,4 +36,4 @@ export interface LogseqContextValue {
   }
 }
 
-export const LogseqContext = React.createContext<Partial<LogseqContextValue>>({})
+export const LogseqContext = React.createContext<LogseqContextValue>({} as LogseqContextValue)

+ 1 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -105,8 +105,7 @@ const useSearch = (q: string, searchFilter: 'B' | 'P' | null) => {
 
   React.useEffect(() => {
     let canceled = false
-    const searchHandler = handlers?.search
-    if (q.length > 0 && searchHandler) {
+    if (q.length > 0) {
       const filter = { 'pages?': true, 'blocks?': true, 'files?': false }
       if (searchFilter === 'B') {
         filter['pages?'] = false

+ 1 - 1
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -439,7 +439,7 @@ export class TLApp<
       this.notify('paste', {
         point: this.inputs.currentPoint,
         shiftKey: !!shiftKey,
-        dataTransfer: e?.clipboardData,
+        dataTransfer: e?.clipboardData ?? undefined,
       })
     }
   }