فهرست منبع

fix: focusing shape id even in the same whiteboard & display block ref correctly

Peng Xiao 2 سال پیش
والد
کامیت
68ea04af52

+ 18 - 10
src/main/frontend/extensions/tldraw.cljs

@@ -34,7 +34,13 @@
   (block/breadcrumb {:preview? true}
                     (state/get-current-repo)
                     (uuid (gobj/get props "blockId"))
-                    {:level-limit (gobj/get props "levelLimit" 3)}))
+                    {:end-separator? (gobj/get props "endSeparator")
+                     :level-limit (gobj/get props "levelLimit" 3)}))
+
+(rum/defc block-reference
+  [props]
+  (println "page-name-linkpage-name-linkpage-name-linkpage-name-link" props)
+  (block/block-reference {} (gobj/get props "blockId") nil))
 
 (rum/defc page-name-link
   [props]
@@ -66,8 +72,9 @@
 (def tldraw-renderers {:Page page-cp
                        :Block block-cp
                        :Breadcrumb breadcrumb
-                       :PageNameLink page-name-link
-                       :ReferencesCount references-count})
+                       :PageName page-name-link
+                       :ReferencesCount references-count
+                       :BlockReference block-reference})
 
 (defn get-tldraw-handlers [current-whiteboard-name]
   {:search search-handler
@@ -95,16 +102,17 @@
 (rum/defc tldraw-app
   [page-name block-id]
   (let [populate-onboarding?  (whiteboard-handler/should-populate-onboarding-whiteboard? page-name)
-        data (whiteboard-handler/page-name->tldr! page-name block-id)
-        [loaded? set-loaded?] (rum/use-state false)
+        data (whiteboard-handler/page-name->tldr! page-name)
+        [loaded-app set-loaded-app] (rum/use-state nil)
         on-mount (fn [tln]
                    (when-let [^js api (gobj/get tln "api")]
                      (p/then (when populate-onboarding?
                                (whiteboard-handler/populate-onboarding-whiteboard api))
-                             #(do (when (and block-id (parse-uuid block-id))
-                                    (. api selectShapes block-id)
-                                    (. api zoomToSelection))
-                                  (set-loaded? true)))))]
+                             #(do (state/focus-whiteboard-shape tln block-id)
+                                  (set-loaded-app tln)))))]
+    (rum/use-effect! (fn [] (when (and loaded-app block-id)
+                              (state/focus-whiteboard-shape loaded-app block-id)) #())
+                     [block-id loaded-app])
 
     (when data
       [:div.draw.tldraw.whiteboard.relative.w-full.h-full
@@ -116,7 +124,7 @@
         :on-wheel util/stop-propagation}
 
        (when
-        (and populate-onboarding? (not loaded?))
+        (and populate-onboarding? (not loaded-app))
          [:div.absolute.inset-0.flex.items-center.justify-center
           {:style {:z-index 200}}
           (ui/loading "Loading onboarding whiteboard ...")])

+ 7 - 5
src/main/frontend/handler/route.cljs

@@ -3,12 +3,12 @@
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
-            [frontend.handler.ui :as ui-handler]
             [frontend.handler.recent :as recent-handler]
             [frontend.handler.search :as search-handler]
+            [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
-            [logseq.graph-parser.text :as text]
             [frontend.util :as util]
+            [logseq.graph-parser.text :as text]
             [reitit.frontend.easy :as rfe]))
 
 (defn redirect!
@@ -65,9 +65,11 @@
    (redirect-to-whiteboard! name nil))
   ([name {:keys [block-id]}]
    (recent-handler/add-page-to-recent! (state/get-current-repo) name false)
-   (redirect! {:to :whiteboard
-               :path-params {:name (str name)}
-               :query-params (merge {:block-id block-id})})))
+   (if (= name (state/get-current-whiteboard))
+     (state/focus-whiteboard-shape block-id)
+     (redirect! {:to :whiteboard
+                 :path-params {:name (str name)}
+                 :query-params (merge {:block-id block-id})}))))
 
 (defn get-title
   [name path-params]

+ 3 - 5
src/main/frontend/handler/whiteboard.cljs

@@ -82,7 +82,7 @@
           blocks (model/get-page-blocks-no-cache page-name)]
       [page-block blocks])))
 
-(defn- whiteboard-clj->tldr [page-block blocks shape-id]
+(defn- whiteboard-clj->tldr [page-block blocks]
   (let [id (str (:block/uuid page-block))
         shapes (->> blocks
                     (filter gp-whiteboard/shape-block?)
@@ -93,7 +93,7 @@
         tldr-page (dissoc tldr-page :assets)]
     (clj->js {:currentPageId id
               :assets (or assets #js[])
-              :selectedIds (if (not-empty shape-id) #js[shape-id] #js[])
+              :selectedIds #js[]
               :pages [(merge tldr-page
                              {:id id
                               :name "page"
@@ -173,11 +173,9 @@
 
 (defn page-name->tldr!
   ([page-name]
-   (page-name->tldr! page-name nil))
-  ([page-name shape-id]
    (if page-name
      (if-let [[page-block blocks] (get-whiteboard-clj page-name)]
-       (whiteboard-clj->tldr page-block blocks shape-id)
+       (whiteboard-clj->tldr page-block blocks)
        (create-new-whiteboard-page! page-name))
      (create-new-whiteboard-page! nil))))
 

+ 9 - 0
src/main/frontend/state.cljs

@@ -1976,3 +1976,12 @@ Similar to re-frame subscriptions"
       (when (apply not= (map :identity [inflated-file (get-current-pdf)]))
         (set-state! :pdf/current nil)
         (js/setTimeout #(settle-file!) 16)))))
+
+(defn focus-whiteboard-shape
+  ([shape-id]
+   (focus-whiteboard-shape (active-tldraw-app) shape-id))
+  ([tln shape-id]
+   (when-let [^js api (gobj/get tln "api")]
+     (when (and shape-id (parse-uuid shape-id))
+       (. api selectShapes shape-id)
+       (. api zoomToSelection)))))

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

@@ -129,6 +129,7 @@ export const App = function App({ renderers, handlers, ...rest }: LogseqTldrawPr
       })
     )
   }, [])
+
   const contextValue = {
     renderers: memoRenders,
     handlers: handlers,

+ 6 - 3
tldraw/apps/tldraw-logseq/src/components/BlockLink/BlockLink.tsx

@@ -6,7 +6,7 @@ import { TablerIcon } from '../icons'
 export const BlockLink = ({ id }: { id: string }) => {
   const {
     handlers: { isWhiteboardPage, redirectToPage, sidebarAddBlock, queryBlockByUUID },
-    renderers: { Breadcrumb, PageNameLink },
+    renderers: { Breadcrumb, PageName, BlockReference },
   } = React.useContext(LogseqContext)
 
   let iconName = ''
@@ -41,9 +41,12 @@ export const BlockLink = ({ id }: { id: string }) => {
       <TablerIcon name={iconName} />
       <span className="pointer-events-none">
         {linkType === 'P' ? (
-          <PageNameLink pageName={id} />
+          <PageName pageName={id} />
         ) : (
-          <Breadcrumb levelLimit={1} blockId={id} />
+          <span className="inline-flex items-center">
+            <Breadcrumb levelLimit={1} blockId={id} endSeparator />
+            <BlockReference blockId={id} />
+          </span>
         )}
       </span>
     </button>

+ 1 - 1
tldraw/apps/tldraw-logseq/src/components/QuickLinks/QuickLinks.tsx

@@ -18,7 +18,7 @@ export const QuickLinks: TLQuickLinksComponent<Shape> = observer(({ id, shape })
   if (links.length === 0) return null
 
   return (
-    <div className="tl-quick-links">
+    <div className="tl-quick-links" title="Shape Quick Links">
       {links.map(ref => {
         return (
           <div key={ref} className="tl-quick-links-row">

+ 2 - 39
tldraw/apps/tldraw-logseq/src/components/inputs/ShapeLinksInput.tsx

@@ -1,8 +1,6 @@
 import type { Side } from '@radix-ui/react-popper'
-import { MOD_KEY, validUUID } from '@tldraw/core'
-import Mousetrap from 'mousetrap'
+import { validUUID } from '@tldraw/core'
 import React from 'react'
-import { NIL as NIL_UUID } from 'uuid'
 
 import { observer } from 'mobx-react-lite'
 import { LogseqContext } from '../../lib/logseq-context'
@@ -55,7 +53,7 @@ function ShapeLinkItem({
           type="button"
           onClick={onRemove}
         >
-          <TablerIcon name="x" className='!translate-y-0' />
+          <TablerIcon name="x" className="!translate-y-0" />
         </Button>
       )}
     </div>
@@ -80,41 +78,6 @@ export const ShapeLinksInput = observer(function ShapeLinksInput({
     }
   }
 
-  React.useEffect(() => {
-    const callback = (keyboardEvent: Mousetrap.ExtendedKeyboardEvent, combo: string) => {
-      keyboardEvent.preventDefault()
-      keyboardEvent.stopPropagation()
-      ;(async () => {
-        // TODO: thinking about how to make this more generic with usePaste hook
-        // TODO: handle whiteboard shapes?
-        const items = await navigator.clipboard.read()
-        if (items.length > 0) {
-          const blob = await items[0].getType('text/plain')
-          const rawText = (await blob.text()).trim()
-
-          if (rawText) {
-            let newValue: string | undefined
-            if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
-              const blockRef = rawText.slice(2, -2)
-              if (validUUID(blockRef)) {
-                newValue = blockRef
-              }
-            } else if (/^\[\[.*\]\]$/.test(rawText)) {
-              newValue = rawText.slice(2, -2)
-            }
-            addNewRef(newValue)
-          }
-        }
-      })()
-    }
-
-    Mousetrap.bind(`mod+shift+v`, callback, 'keydown')
-
-    return () => {
-      Mousetrap.unbind(`mod+shift+v`, 'keydown')
-    }
-  }, [])
-
   const showReferencePanel = !!(pageId && portalType)
 
   return (

+ 325 - 289
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -4,11 +4,12 @@ import {
   TLAsset,
   TLBinding,
   TLCursor,
+  TLPasteEventInfo,
   TLShapeModel,
   uniqueId,
   validUUID,
 } from '@tldraw/core'
-import type { TLReactCallbacks } from '@tldraw/react'
+import type { TLReactApp, TLReactCallbacks } from '@tldraw/react'
 import Vec from '@tldraw/vec'
 import * as React from 'react'
 import { NIL as NIL_UUID } from 'uuid'
@@ -21,7 +22,7 @@ import {
   YouTubeShape,
   type Shape,
 } from '../lib'
-import { LogseqContext } from '../lib/logseq-context'
+import { LogseqContext, LogseqContextValue } from '../lib/logseq-context'
 
 const isValidURL = (url: string) => {
   try {
@@ -87,323 +88,358 @@ async function getDataFromType(item: DataTransfer | ClipboardItem, type: `text/$
   return await blob.text()
 }
 
-// FIXME: for assets, we should prompt the user a loading spinner
-export function usePaste() {
-  const { handlers } = React.useContext(LogseqContext)
-
-  return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(
-    async (app, { point, shiftKey, dataTransfer, fromDrop }) => {
-      let imageAssetsToCreate: VideoImageAsset[] = []
-      let assetsToClone: TLAsset[] = []
-      const bindingsToCreate: TLBinding[] = []
-
-      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) {
-          return existingAsset as VideoImageAsset
-        } else {
-          // 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
-        }
-      }
-
-      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)
-            }
-            return null
-          })
-        return (await Promise.all(tasks)).filter(isNonNullable)
+const handleCreatingShapes = async (
+  app: TLReactApp<Shape>,
+  { point, shiftKey, dataTransfer, fromDrop }: TLPasteEventInfo,
+  handlers: LogseqContextValue['handlers']
+) => {
+  let imageAssetsToCreate: VideoImageAsset[] = []
+  let assetsToClone: TLAsset[] = []
+  const bindingsToCreate: TLBinding[] = []
+
+  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) {
+      return existingAsset as VideoImageAsset
+    } else {
+      // 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
+    }
+  }
 
-      function createHTMLShape(text: string) {
-        return [
-          {
-            ...HTMLShape.defaultProps,
-            html: text,
-            point: [point[0], point[1]],
-          },
-        ]
-      }
+  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)
+        }
+        return null
+      })
+    return (await Promise.all(tasks)).filter(isNonNullable)
+  }
 
-      async function tryCreateShapesFromDataTransfer(dataTransfer: DataTransfer) {
-        return tryCreateShapeHelper(
-          tryCreateShapeFromFiles,
-          tryCreateShapeFromTextHTML,
-          tryCreateShapeFromTextPlain,
-          tryCreateShapeFromBlockUUID
-        )(dataTransfer)
-      }
+  function createHTMLShape(text: string) {
+    return [
+      {
+        ...HTMLShape.defaultProps,
+        html: text,
+        point: [point[0], point[1]],
+      },
+    ]
+  }
 
-      async function tryCreateShapesFromClipboard() {
-        const items = await navigator.clipboard.read()
-        const createShapesFn = tryCreateShapeHelper(
-          tryCreateShapeFromTextHTML,
-          tryCreateShapeFromTextPlain
-        )
-        const allShapes = (await Promise.all(items.map(item => createShapesFn(item))))
-          .flat()
-          .filter(isNonNullable)
-
-        return allShapes
-      }
+  async function tryCreateShapesFromDataTransfer(dataTransfer: DataTransfer) {
+    return tryCreateShapeHelper(
+      tryCreateShapeFromFiles,
+      tryCreateShapeFromTextHTML,
+      tryCreateShapeFromTextPlain,
+      tryCreateShapeFromBlockUUID
+    )(dataTransfer)
+  }
 
-      async function tryCreateShapeFromFiles(item: DataTransfer) {
-        const files = Array.from(item.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,
-              id: uniqueId(),
-              // TODO: Should be place near the last edited shape
-              assetId: asset.id,
-              opacity: 1,
-            }
+  async function tryCreateShapesFromClipboard() {
+    const items = await navigator.clipboard.read()
+    const createShapesFn = tryCreateShapeHelper(
+      tryCreateShapeFromTextHTML,
+      tryCreateShapeFromTextPlain
+    )
+    const allShapes = (await Promise.all(items.map(item => createShapesFn(item))))
+      .flat()
+      .filter(isNonNullable)
+
+    return allShapes
+  }
 
-            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),
-              })
-            }
+  async function tryCreateShapeFromFiles(item: DataTransfer) {
+    const files = Array.from(item.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,
+          id: uniqueId(),
+          // TODO: Should be place near the last edited shape
+          assetId: asset.id,
+          opacity: 1,
+        }
 
-            return newShape
+        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),
           })
         }
-        return null
-      }
 
-      async function tryCreateShapeFromTextHTML(item: DataTransfer | ClipboardItem) {
-        // skips if it's a drop event or using shift key
-        if (item.types.includes('text/plain') && (shiftKey || fromDrop)) {
-          return null
-        }
-        const rawText = await getDataFromType(item, 'text/html')
-        if (rawText) {
-          return tryCreateShapeHelper(tryCreateClonedShapesFromJSON, createHTMLShape)(rawText)
-        }
-        return null
-      }
+        return newShape
+      })
+    }
+    return null
+  }
 
-      async function tryCreateShapeFromBlockUUID(dataTransfer: DataTransfer) {
-        // This is a Logseq custom data type defined in frontend.components.block
-        const rawText = dataTransfer.getData('block-uuid')
-        if (rawText) {
-          const text = rawText.trim()
-          const allSelectedBlocks = window.logseq?.api?.get_selected_blocks?.()
-          const blockUUIDs =
-            allSelectedBlocks && allSelectedBlocks?.length > 1
-              ? allSelectedBlocks.map(b => b.uuid)
-              : [text]
-          // ensure all uuid in blockUUIDs is persisted
-          window.logseq?.api?.set_blocks_id?.(blockUUIDs)
-          const tasks = blockUUIDs.map(uuid => tryCreateLogseqPortalShapesFromString(`((${uuid}))`))
-          const newShapes = (await Promise.all(tasks)).flat().filter(isNonNullable)
-          return newShapes.map((s, idx) => {
-            // if there are multiple shapes, shift them to the right
-            return {
-              ...s,
-              // TODO: use better alignment?
-              point: [point[0] + (LogseqPortalShape.defaultProps.size[0] + 16) * idx, point[1]],
-            }
-          })
-        }
-        return null
-      }
+  async function tryCreateShapeFromTextHTML(item: DataTransfer | ClipboardItem) {
+    // skips if it's a drop event or using shift key
+    if (item.types.includes('text/plain') && (shiftKey || fromDrop)) {
+      return null
+    }
+    const rawText = await getDataFromType(item, 'text/html')
+    if (rawText) {
+      return tryCreateShapeHelper(tryCreateClonedShapesFromJSON, createHTMLShape)(rawText)
+    }
+    return null
+  }
 
-      async function tryCreateShapeFromTextPlain(item: DataTransfer | ClipboardItem) {
-        const rawText = await getDataFromType(item, 'text/plain')
-        if (rawText) {
-          const text = rawText.trim()
-          return tryCreateShapeHelper(
-            tryCreateShapeFromURL,
-            tryCreateShapeFromIframeString,
-            tryCreateLogseqPortalShapesFromString
-          )(text)
+  async function tryCreateShapeFromBlockUUID(dataTransfer: DataTransfer) {
+    // This is a Logseq custom data type defined in frontend.components.block
+    const rawText = dataTransfer.getData('block-uuid')
+    if (rawText) {
+      const text = rawText.trim()
+      const allSelectedBlocks = window.logseq?.api?.get_selected_blocks?.()
+      const blockUUIDs =
+        allSelectedBlocks && allSelectedBlocks?.length > 1
+          ? allSelectedBlocks.map(b => b.uuid)
+          : [text]
+      // ensure all uuid in blockUUIDs is persisted
+      window.logseq?.api?.set_blocks_id?.(blockUUIDs)
+      const tasks = blockUUIDs.map(uuid => tryCreateLogseqPortalShapesFromString(`((${uuid}))`))
+      const newShapes = (await Promise.all(tasks)).flat().filter(isNonNullable)
+      return newShapes.map((s, idx) => {
+        // if there are multiple shapes, shift them to the right
+        return {
+          ...s,
+          // TODO: use better alignment?
+          point: [point[0] + (LogseqPortalShape.defaultProps.size[0] + 16) * idx, point[1]],
         }
+      })
+    }
+    return null
+  }
 
-        return null
-      }
+  async function tryCreateShapeFromTextPlain(item: DataTransfer | ClipboardItem) {
+    const rawText = await getDataFromType(item, 'text/plain')
+    if (rawText) {
+      const text = rawText.trim()
+      return tryCreateShapeHelper(
+        tryCreateShapeFromURL,
+        tryCreateShapeFromIframeString,
+        tryCreateLogseqPortalShapesFromString
+      )(text)
+    }
 
-      function tryCreateClonedShapesFromJSON(rawText: string) {
-        const result = app.api.getClonedShapesFromTldrString(decodeURIComponent(rawText), point)
-        if (result) {
-          const { shapes, assets, bindings } = result
-          assetsToClone.push(...assets)
-          bindingsToCreate.push(...bindings)
-          return shapes
-        }
-        return null
-      }
+    return null
+  }
 
-      async function tryCreateShapeFromURL(rawText: string) {
-        if (isValidURL(rawText) && !(shiftKey || fromDrop)) {
-          const isYoutubeUrl = (url: string) => {
-            const youtubeRegex =
-              /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
-            return youtubeRegex.test(url)
-          }
-          if (isYoutubeUrl(rawText)) {
-            return [
-              {
-                ...YouTubeShape.defaultProps,
-                url: rawText,
-                point: [point[0], point[1]],
-              },
-            ]
-          }
+  function tryCreateClonedShapesFromJSON(rawText: string) {
+    const result = app.api.getClonedShapesFromTldrString(decodeURIComponent(rawText), point)
+    if (result) {
+      const { shapes, assets, bindings } = result
+      assetsToClone.push(...assets)
+      bindingsToCreate.push(...bindings)
+      return shapes
+    }
+    return null
+  }
 
-          return [
-            {
-              ...IFrameShape.defaultProps,
-              url: rawText,
-              point: [point[0], point[1]],
-            },
-          ]
-        }
-        return null
+  async function tryCreateShapeFromURL(rawText: string) {
+    if (isValidURL(rawText) && !(shiftKey || fromDrop)) {
+      const isYoutubeUrl = (url: string) => {
+        const youtubeRegex =
+          /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
+        return youtubeRegex.test(url)
       }
-
-      function tryCreateShapeFromIframeString(rawText: string) {
-        // if rawText is iframe text
-        if (rawText.startsWith('<iframe')) {
-          return [
-            {
-              ...HTMLShape.defaultProps,
-              html: rawText,
-              point: [point[0], point[1]],
-            },
-          ]
-        }
-        return null
+      if (isYoutubeUrl(rawText)) {
+        return [
+          {
+            ...YouTubeShape.defaultProps,
+            url: rawText,
+            point: [point[0], point[1]],
+          },
+        ]
       }
 
-      async function tryCreateLogseqPortalShapesFromString(rawText: string) {
-        if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
-          const blockRef = rawText.slice(2, -2)
-          if (validUUID(blockRef)) {
-            return [
-              {
-                ...LogseqPortalShape.defaultProps,
-                point: [point[0], point[1]],
-                size: [400, 0], // use 0 here to enable auto-resize
-                pageId: blockRef,
-                fill: app.settings.color,
-                stroke: app.settings.color,
-                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: pageName,
-              fill: app.settings.color,
-              stroke: app.settings.color,
-              blockType: 'P' as 'P',
-            },
-          ]
-        }
+      return [
+        {
+          ...IFrameShape.defaultProps,
+          url: rawText,
+          point: [point[0], point[1]],
+        },
+      ]
+    }
+    return null
+  }
 
-        // Otherwise, creating a new block that belongs to the current whiteboard
-        const uuid = handlers?.addNewBlock(rawText)
-        if (uuid) {
-          // create text shape
-          return [
-            {
-              ...LogseqPortalShape.defaultProps,
-              size: [400, 0], // use 0 here to enable auto-resize
-              point: [point[0], point[1]],
-              pageId: uuid,
-              fill: app.settings.color,
-              stroke: app.settings.color,
-              blockType: 'B' as 'B',
-              compact: true,
-            },
-          ]
-        }
+  function tryCreateShapeFromIframeString(rawText: string) {
+    // if rawText is iframe text
+    if (rawText.startsWith('<iframe')) {
+      return [
+        {
+          ...HTMLShape.defaultProps,
+          html: rawText,
+          point: [point[0], point[1]],
+        },
+      ]
+    }
+    return null
+  }
 
-        return null
+  async function tryCreateLogseqPortalShapesFromString(rawText: string) {
+    if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
+      const blockRef = rawText.slice(2, -2)
+      if (validUUID(blockRef)) {
+        return [
+          {
+            ...LogseqPortalShape.defaultProps,
+            point: [point[0], point[1]],
+            size: [400, 0], // use 0 here to enable auto-resize
+            pageId: blockRef,
+            fill: app.settings.color,
+            stroke: app.settings.color,
+            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: pageName,
+          fill: app.settings.color,
+          stroke: app.settings.color,
+          blockType: 'P' as 'P',
+        },
+      ]
+    }
 
-      app.cursors.setCursor(TLCursor.Progress)
+    // Otherwise, creating a new block that belongs to the current whiteboard
+    const uuid = handlers?.addNewBlock(rawText)
+    if (uuid) {
+      // create text shape
+      return [
+        {
+          ...LogseqPortalShape.defaultProps,
+          size: [400, 0], // use 0 here to enable auto-resize
+          point: [point[0], point[1]],
+          pageId: uuid,
+          fill: app.settings.color,
+          stroke: app.settings.color,
+          blockType: 'B' as 'B',
+          compact: true,
+        },
+      ]
+    }
 
-      let newShapes: TLShapeModel[] = []
-      try {
-        if (dataTransfer) {
-          newShapes.push(...((await tryCreateShapesFromDataTransfer(dataTransfer)) ?? []))
-        } else {
-          // from Clipboard app or Shift copy etc
-          // in this case, we do not have the dataTransfer object
-          newShapes.push(...((await tryCreateShapesFromClipboard()) ?? []))
-        }
-      } catch (error) {
-        console.error(error)
-      }
+    return null
+  }
 
-      const allShapesToAdd: TLShapeModel[] = newShapes.map(shape => {
-        return {
-          ...shape,
-          parentId: app.currentPageId,
-          id: validUUID(shape.id) ? shape.id : uniqueId(),
-        }
-      })
+  app.cursors.setCursor(TLCursor.Progress)
 
-      const filesOnly = dataTransfer?.types.every(t => t === 'Files')
+  let newShapes: TLShapeModel[] = []
+  try {
+    if (dataTransfer) {
+      newShapes.push(...((await tryCreateShapesFromDataTransfer(dataTransfer)) ?? []))
+    } else {
+      // from Clipboard app or Shift copy etc
+      // in this case, we do not have the dataTransfer object
+      newShapes.push(...((await tryCreateShapesFromClipboard()) ?? []))
+    }
+  } catch (error) {
+    console.error(error)
+  }
 
-      app.wrapUpdate(() => {
-        const allAssets = [...imageAssetsToCreate, ...assetsToClone]
-        if (allAssets.length > 0) {
-          app.createAssets(allAssets)
-        }
-        if (allShapesToAdd.length > 0) {
-          app.createShapes(allShapesToAdd)
-        }
-        app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
+  const allShapesToAdd: TLShapeModel<Shape['props']>[] = newShapes.map(shape => {
+    return {
+      ...shape,
+      parentId: app.currentPageId,
+      id: validUUID(shape.id) ? shape.id : uniqueId(),
+    }
+  })
 
-        if (app.selectedShapesArray.length === 1 && allShapesToAdd.length === 1 && !fromDrop) {
-          const source = app.selectedShapesArray[0]
-          const target = app.getShapeById(allShapesToAdd[0].id!)!
-          app.createNewLineBinding(source, target)
-        }
+  const filesOnly = dataTransfer?.types.every(t => t === 'Files')
+
+  app.wrapUpdate(() => {
+    const allAssets = [...imageAssetsToCreate, ...assetsToClone]
+    if (allAssets.length > 0) {
+      app.createAssets(allAssets)
+    }
+    if (allShapesToAdd.length > 0) {
+      app.createShapes(allShapesToAdd)
+    }
+    app.currentPage.updateBindings(Object.fromEntries(bindingsToCreate.map(b => [b.id, b])))
+
+    if (app.selectedShapesArray.length === 1 && allShapesToAdd.length === 1 && !fromDrop) {
+      const source = app.selectedShapesArray[0]
+      const target = app.getShapeById(allShapesToAdd[0].id!)!
+      app.createNewLineBinding(source, target)
+    }
 
-        app.setSelectedShapes(allShapesToAdd.map(s => s.id))
-        app.selectedTool.transition('idle') // clears possible editing states
-        app.cursors.setCursor(TLCursor.Default)
+    app.setSelectedShapes(allShapesToAdd.map(s => s.id))
+    app.selectedTool.transition('idle') // clears possible editing states
+    app.cursors.setCursor(TLCursor.Default)
 
-        if (fromDrop || filesOnly) {
-          app.packIntoRectangle()
+    if (fromDrop || filesOnly) {
+      app.packIntoRectangle()
+    }
+  })
+}
+
+// FIXME: for assets, we should prompt the user a loading spinner
+export function usePaste() {
+  const { handlers } = React.useContext(LogseqContext)
+
+  return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(async (app, info) => {
+    // there is a special case for SHIFT+PASTE
+    // it will set the link to the current selected shape
+
+    if (info.shiftKey && app.selectedShapesArray.length === 1) {
+      // TODO: thinking about how to make this more generic with usePaste hook
+      // TODO: handle whiteboard shapes?
+      const items = await navigator.clipboard.read()
+      let newRef: string | undefined
+      if (items.length > 0) {
+        const blob = await items[0].getType('text/plain')
+        const rawText = (await blob.text()).trim()
+
+        if (rawText) {
+          if (/^\(\(.*\)\)$/.test(rawText) && rawText.length === NIL_UUID.length + 4) {
+            const blockRef = rawText.slice(2, -2)
+            if (validUUID(blockRef)) {
+              newRef = blockRef
+            }
+          } else if (/^\[\[.*\]\]$/.test(rawText)) {
+            newRef = rawText.slice(2, -2)
+          }
         }
-      })
-    },
-    []
-  )
+      }
+      if (newRef) {
+        app.selectedShapesArray[0].update({
+          refs: [newRef],
+        })
+        app.persist()
+        return
+      }
+      // fall through to creating shapes
+    }
+
+    handleCreatingShapes(app, info, handlers)
+  }, [])
 }

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

@@ -17,10 +17,14 @@ export interface LogseqContextValue {
     Breadcrumb: React.FC<{
       blockId: string
       levelLimit?: number
+      endSeparator?: boolean
     }>
-    PageNameLink: React.FC<{
+    PageName: React.FC<{
       pageName: string
     }>
+    BlockReference: React.FC<{
+      blockId: string
+    }>
     ReferencesCount: React.FC<{
       id: string
       className?: string

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

@@ -419,7 +419,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
       return null // not being correctly configured
     }
 
-    const { Breadcrumb, PageNameLink } = renderers
+    const { Breadcrumb, PageName } = renderers
 
     return (
       <HTMLContainer
@@ -474,7 +474,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
                     opacity={opacity}
                   >
                     {this.props.blockType === 'P' ? (
-                      <PageNameLink pageName={pageId} />
+                      <PageName pageName={pageId} />
                     ) : (
                       <Breadcrumb blockId={pageId} />
                     )}

+ 8 - 3
tldraw/demo/src/App.jsx

@@ -68,11 +68,15 @@ const Block = () => {
   )
 }
 
-const Breadcrumb = props => {
+const Breadcrumb = ({ endSeparator }) => {
+  return <div className="font-mono">Breadcrumb {endSeparator ? ' > ' : ''}</div>
+}
+
+const BlockReference = props => {
   return <div className="font-mono">{props.blockId}</div>
 }
 
-const PageNameLink = props => {
+const PageName = props => {
   const [value, setValue] = React.useState(JSON.stringify(props))
   return (
     <input
@@ -217,8 +221,9 @@ export default function App() {
           Page,
           Block,
           Breadcrumb,
-          PageNameLink,
+          PageName,
           ReferencesCount,
+          BlockReference,
         }}
         handlers={{
           search: searchHandler,

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

@@ -134,6 +134,13 @@ export interface TLAsset {
   src: string
 }
 
+export type TLPasteEventInfo = {
+  point: number[]
+  shiftKey: boolean
+  dataTransfer?: DataTransfer
+  fromDrop?: boolean
+}
+
 /* --------------------- Events --------------------- */
 
 export type TLSubscriptionEvent =
@@ -183,7 +190,7 @@ export type TLSubscriptionEvent =
     }
   | {
       event: 'paste'
-      info: { point: number[]; shiftKey: boolean; dataTransfer?: DataTransfer; fromDrop?: boolean }
+      info: TLPasteEventInfo
     }
   | {
       event: 'create-assets'

+ 0 - 1
tldraw/packages/react/src/components/ReferencesCountContainer copy/QuickLinksContainer.tsx

@@ -51,7 +51,6 @@ export const QuickLinksContainer = observer(function QuickLinksContainer<S exten
           }}
           {...events}
           onPointerDown={stop}
-          title="Shape Quick Links"
         >
           <QuickLinks
             className={'tl-backlinks-count ' + (rounded ? 'tl-backlinks-count-rounded' : '')}