瀏覽代碼

Merge pull request #7002 from logseq/more-pasting-issues

fix(whiteboards): some pasting issues
Tienson Qin 3 年之前
父節點
當前提交
94e099ad5d

+ 23 - 10
src/main/frontend/handler/paste.cljs

@@ -64,19 +64,30 @@
   (try (js/JSON.parse text)
        (catch :default _ #js{})))
 
+(defn- get-whiteboard-tldr-from-text
+  [text]
+  (when-let [matched-text (util/safe-re-find #"<whiteboard-tldr>(.*)</whiteboard-tldr>" text)]
+    (try-parse-as-json (js/decodeURIComponent (second matched-text)))))
+
+(defn- get-whiteboard-shape-refs-text
+  [text]
+  (let [tldr (get-whiteboard-tldr-from-text text)]
+    (when (and tldr (object? tldr))
+      (->> (gobj/get tldr "shapes")
+           (mapv (fn [shape]
+                   (let [shape-id (gobj/get shape "id")]
+                     (block-ref/->block-ref shape-id))))
+           (string/join "\n")))))
+
 (defn- paste-copied-blocks-or-text
+  ;; todo: logseq/whiteboard-shapes is now text/html
   [text e html]
   (util/stop e)
   (let [copied-blocks (state/get-copied-blocks)
         input (state/get-input)
+        input-id (state/get-edit-input-id)
         text (string/replace text "\r\n" "\n") ;; Fix for Windows platform
-        whiteboard-data (try-parse-as-json text)
-        whiteboard-shape? (and whiteboard-data
-                               (object? whiteboard-data)
-                               (= "logseq/whiteboard-shapes" (.-type whiteboard-data)))
-        text (if whiteboard-shape?
-               (block-ref/->block-ref (gobj/getValueByKeys (try-parse-as-json text) "shapes" 0 "id"))
-               text)
+        shape-refs-text (when-not (string/blank? html) (get-whiteboard-shape-refs-text html))
         internal-paste? (and
                          (seq (:copy/blocks copied-blocks))
                          ;; not copied from the external clipboard
@@ -88,6 +99,9 @@
           (editor-handler/paste-blocks blocks {})))
       (let [{:keys [value]} (editor-handler/get-selection-and-format)]
         (cond
+          (not (string/blank? shape-refs-text))
+          (commands/simple-insert! input-id shape-refs-text nil)
+
           (and (or (gp-util/url? text)
                    (and value (gp-util/url? (string/trim value))))
                (not (string/blank? (util/get-selected-text))))
@@ -95,7 +109,7 @@
 
           (and (block-ref/block-ref? text)
                (editor-handler/wrapped-by? input block-ref/left-parens block-ref/right-parens))
-          (commands/simple-insert! (state/get-edit-input-id) (block-ref/get-block-ref-id text) nil)
+          (commands/simple-insert! input-id (block-ref/get-block-ref-id text) nil)
 
           :else
           ;; from external
@@ -108,7 +122,6 @@
                                              nil)))]
                             (if (string/blank? result) nil result))
                 text (or html-text text)
-                input-id (state/get-edit-input-id)
                 replace-text-f (fn []
                                  (commands/delete-selection! input-id)
                                  (commands/simple-insert! input-id text nil))]
@@ -175,7 +188,7 @@
         (let [clipboard-data (gobj/get e "clipboardData")
               html (when-not raw-paste? (.getData clipboard-data "text/html"))
               text (.getData clipboard-data "text")]
-          (if-not (string/blank? text)
+          (if-not (and (string/blank? text) (string/blank? html))
             (paste-text-or-blocks-aux input e text html)
             (when id
               (let [_handled

+ 1 - 0
tldraw/apps/tldraw-logseq/package.json

@@ -33,6 +33,7 @@
     "postcss": "^8.4.17",
     "react": "^17.0.0",
     "react-dom": "^17.0.0",
+    "react-virtuoso": "^3.1.0",
     "rimraf": "3.0.2",
     "shadow-cljs": "^2.19.5",
     "tsup": "^6.2.3",

+ 21 - 14
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -41,6 +41,13 @@ const safeParseJson = (json: string) => {
   }
 }
 
+const getWhiteboardsTldrFromText = (text: string) => {
+  const innerText = text.match(/<whiteboard-tldr>(.*)<\/whiteboard-tldr>/)?.[1]
+  if (innerText) {
+    return safeParseJson(decodeURIComponent(innerText))
+  }
+}
+
 interface VideoImageAsset extends TLAsset {
   size?: number[]
 }
@@ -139,11 +146,13 @@ export function usePaste() {
       }
 
       function createHTMLShape(text: string) {
-        return {
-          ...HTMLShape.defaultProps,
-          html: text,
-          point: [point[0], point[1]],
-        }
+        return [
+          {
+            ...HTMLShape.defaultProps,
+            html: text,
+            point: [point[0], point[1]],
+          },
+        ]
       }
 
       async function tryCreateShapesFromDataTransfer(dataTransfer: DataTransfer) {
@@ -208,7 +217,7 @@ export function usePaste() {
         }
         const rawText = await getDataFromType(item, 'text/html')
         if (rawText) {
-          return [createHTMLShape(rawText)]
+          return tryCreateShapeHelper(tryCreateClonedShapesFromJSON, createHTMLShape)(rawText)
         }
         return null
       }
@@ -246,7 +255,6 @@ export function usePaste() {
           return tryCreateShapeHelper(
             tryCreateShapeFromURL,
             tryCreateShapeFromIframeString,
-            tryCreateClonedShapesFromJSON,
             tryCreateLogseqPortalShapesFromString
           )(text)
         }
@@ -255,9 +263,9 @@ export function usePaste() {
       }
 
       function tryCreateClonedShapesFromJSON(rawText: string) {
-        const data = safeParseJson(rawText)
+        const data = getWhiteboardsTldrFromText(rawText)
         try {
-          if (data?.type === 'logseq/whiteboard-shapes') {
+          if (data) {
             const shapes = data.shapes as TLShapeModel[]
             assetsToClone = data.assets as TLAsset[]
             const commonBounds = BoundsUtils.getCommonBounds(
@@ -270,6 +278,7 @@ export function usePaste() {
                 maxY: (shape.point?.[1] ?? point[1]) + (shape.size?.[1] ?? 4),
               }))
             )
+            const bindings = data.bindings as Record<string, TLBinding>
             const shapesToCreate = shapes.map(shape => {
               return {
                 ...shape,
@@ -289,9 +298,7 @@ export function usePaste() {
                   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
+                const binding = bindings[h.bindingId]
                 if (binding) {
                   // if the copied binding from/to is in the source
                   const oldFromIdx = shapes.findIndex(s => s.id === binding.fromId)
@@ -322,7 +329,7 @@ export function usePaste() {
       }
 
       async function tryCreateShapeFromURL(rawText: string) {
-        if (isValidURL(rawText)) {
+        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+)?$/
@@ -443,7 +450,7 @@ export function usePaste() {
           app.createShapes(allShapesToAdd)
         }
 
-        if (app.selectedShapesArray.length === 1 && allShapesToAdd.length === 1) {
+        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)

+ 2 - 2
tldraw/apps/tldraw-logseq/src/lib/preview-manager.tsx

@@ -24,7 +24,7 @@ export class PreviewManager {
   }
 
   load(snapshot: TLDocumentModel) {
-    const page = snapshot.pages.find(p => snapshot.currentPageId === p.id)
+    const page = snapshot.pages[0]
     this.pageId = page?.id
     this.assets = snapshot.assets
     this.shapes = page?.shapes.map(s => {
@@ -47,7 +47,7 @@ export class PreviewManager {
     commonBounds = BoundsUtils.expandBounds(commonBounds, SVG_EXPORT_PADDING)
 
     // make sure commonBounds is of ratio 4/3 (should we have another ratio setting?)
-    commonBounds = BoundsUtils.ensureRatio(commonBounds, 4 / 3)
+    commonBounds = viewport ? BoundsUtils.ensureRatio(commonBounds, 4 / 3) : commonBounds
 
     const translatePoint = (p: [number, number]): [string, string] => {
       return [(p[0] - commonBounds.minX).toFixed(2), (p[1] - commonBounds.minY).toFixed(2)]

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

@@ -7,6 +7,7 @@ import {
   TLResizeInfo,
   validUUID,
 } from '@tldraw/core'
+import { Virtuoso } from 'react-virtuoso'
 import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
 import { useDebouncedValue } from '@tldraw/react'
 import Vec from '@tldraw/vec'
@@ -71,11 +72,7 @@ const LogseqTypeTag = ({
 
 const LogseqPortalShapeHeader = observer(
   ({ type, children }: { type: 'P' | 'B'; children: React.ReactNode }) => {
-    return (
-      <div className="tl-logseq-portal-header">
-        {children}
-      </div>
-    )
+    return <div className="tl-logseq-portal-header">{children}</div>
   }
 )
 
@@ -628,30 +625,35 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
           />
         </div>
         <div className="tl-quick-search-options" ref={optionsWrapperRef}>
-          {options.map(({ actionIcon, onChosen, element }, index) => {
-            return (
-              <div
-                key={index}
-                data-focused={index === focusedOptionIdx}
-                className="tl-quick-search-option"
-                tabIndex={0}
-                onMouseEnter={() => {
-                  setPrefixIcon(actionIcon)
-                  setFocusedOptionIdx(index)
-                }}
-                // we have to use mousedown && stop propagation EARLY, otherwise some
-                // default behavior of clicking the rendered elements will happen
-                onMouseDownCapture={e => {
-                  if (onChosen()) {
-                    e.stopPropagation()
-                    e.preventDefault()
-                  }
-                }}
-              >
-                {element}
-              </div>
-            )
-          })}
+          <Virtuoso
+            style={{ height: Math.min(Math.max(1, options.length), 12) * 36 }}
+            totalCount={options.length}
+            itemContent={index => {
+              const { actionIcon, onChosen, element } = options[index]
+              return (
+                <div
+                  key={index}
+                  data-focused={index === focusedOptionIdx}
+                  className="tl-quick-search-option"
+                  tabIndex={0}
+                  onMouseEnter={() => {
+                    setPrefixIcon(actionIcon)
+                    setFocusedOptionIdx(index)
+                  }}
+                  // we have to use mousedown && stop propagation EARLY, otherwise some
+                  // default behavior of clicking the rendered elements will happen
+                  onMouseDownCapture={e => {
+                    if (onChosen()) {
+                      e.stopPropagation()
+                      e.preventDefault()
+                    }
+                  }}
+                >
+                  {element}
+                </div>
+              )
+            }}
+          />
         </div>
       </div>
     )

+ 9 - 5
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -3,7 +3,7 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import type { TLBounds } from '@tldraw/intersect'
 import { Vec } from '@tldraw/vec'
-import { action, computed, makeObservable, observable, transaction } from 'mobx'
+import { action, computed, makeObservable, observable, toJS, transaction } from 'mobx'
 import { GRID_SIZE } from '../../constants'
 import type {
   TLAsset,
@@ -474,17 +474,21 @@ export class TLApp<
 
   copy = () => {
     if (this.selectedShapesArray.length > 0 && !this.editingShape) {
-      const tldrawString = JSON.stringify({
-        type: 'logseq/whiteboard-shapes',
+      const jsonString = JSON.stringify({
         shapes: this.selectedShapesArray.map(shape => shape.serialized),
-        // pasting into other whiteboard may require this if any shape uses asset
+        // pasting into other whiteboard may require this if any shape uses the assets
         assets: this.getCleanUpAssets().filter(asset => {
           return this.selectedShapesArray.some(shape => shape.props.assetId === asset.id)
         }),
+        // convey the bindings to maintain the new links after pasting
+        bindings: toJS(this.currentPage.bindings),
       })
+      const tldrawString = `<whiteboard-tldr>${encodeURIComponent(jsonString)}</whiteboard-tldr>`
+      // FIXME: use `writeClipboard` in frontend.utils
       navigator.clipboard.write([
         new ClipboardItem({
-          'text/plain': new Blob([tldrawString], { type: 'text/plain' }),
+          'text/html': new Blob([tldrawString], { type: 'text/html' }),
+          // ??? what plain text should be used here?
         }),
       ])
     }

+ 2 - 2
tldraw/packages/core/src/utils/BoundsUtils.ts

@@ -999,10 +999,10 @@ left past the initial left edge) then swap points on that axis.
     const commonBounds = BoundsUtils.getCommonBounds(shapes.map(({ bounds }) => bounds))
     const origin = [commonBounds.minX, commonBounds.minY]
     const shapesPosOriginal: Record<string, number[]> = Object.fromEntries(
-      shapes.map(s => [s.id, s.bounds.minX, s.bounds.minY])
+      shapes.map(s => [s.id, [s.bounds.minX, s.bounds.minY]])
     )
     const entries = shapes
-      .filter(s => s.type !== 'line')
+      .filter(s => !(s.props.handles?.start?.bindingId || s.props.handles?.end?.bindingId))
       .map(shape => {
         const bounds = shape.getBounds()
         return {

+ 7 - 1
tldraw/packages/react/src/hooks/useDebounced.ts

@@ -3,10 +3,16 @@ import { useState, useEffect } from 'react'
 export function useDebouncedValue<T>(value: T, ms = 0) {
   const [debouncedValue, setDebouncedValue] = useState(value)
   useEffect(() => {
+    let canceled = false
     const handler = setTimeout(() => {
-      setDebouncedValue(value)
+      requestIdleCallback(() => {
+        if (!canceled) {
+          setDebouncedValue(value)
+        }
+      })
     }, ms)
     return () => {
+      canceled = true
       clearTimeout(handler)
     }
   }, [value, ms])

+ 20 - 0
tldraw/yarn.lock

@@ -1077,6 +1077,18 @@
   dependencies:
     "@use-gesture/core" "10.2.20"
 
+"@virtuoso.dev/react-urx@^0.2.12":
+  version "0.2.13"
+  resolved "https://registry.yarnpkg.com/@virtuoso.dev/react-urx/-/react-urx-0.2.13.tgz#e2cfc42d259d2a002695e7517d34cb97b64ee9c4"
+  integrity sha512-MY0ugBDjFb5Xt8v2HY7MKcRGqw/3gTpMlLXId2EwQvYJoC8sP7nnXjAxcBtTB50KTZhO0SbzsFimaZ7pSdApwA==
+  dependencies:
+    "@virtuoso.dev/urx" "^0.2.13"
+
+"@virtuoso.dev/urx@^0.2.12", "@virtuoso.dev/urx@^0.2.13":
+  version "0.2.13"
+  resolved "https://registry.yarnpkg.com/@virtuoso.dev/urx/-/urx-0.2.13.tgz#a65e7e8d923cb03397ac876bfdd45c7f71c8edf1"
+  integrity sha512-iirJNv92A1ZWxoOHHDYW/1KPoi83939o83iUBQHIim0i3tMeSKEh+bxhJdTHQ86Mr4uXx9xGUTq69cp52ZP8Xw==
+
 "@vitejs/plugin-basic-ssl@^0.1.2":
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-0.1.2.tgz#7177f9adc5384f1377b9b91b17ce7cdb8f422abd"
@@ -3779,6 +3791,14 @@ react-style-singleton@^2.2.1:
     invariant "^2.2.4"
     tslib "^2.0.0"
 
+react-virtuoso@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-3.1.0.tgz#e358a39b9fd99895bcc72671a585091b756a1c92"
+  integrity sha512-Rur0d7xiZthRxy3f7Z217Q+U6k1sTsPZZU/kKT73GPB3ROtJCr4Y0Qehg/WxeKhochQPnSuT8VfcsAasdpX2ig==
+  dependencies:
+    "@virtuoso.dev/react-urx" "^0.2.12"
+    "@virtuoso.dev/urx" "^0.2.12"
+
 react@^17, react@^17.0.0:
   version "17.0.2"
   resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"