1
0
Эх сурвалжийг харах

feat: add html shape to allow pasting iframes

Peng Xiao 3 жил өмнө
parent
commit
b397ee097f

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

@@ -18,6 +18,7 @@ import { LogseqContext } from '~lib/logseq-context'
 import { Shape, shapes } from '~lib/shapes'
 import {
   HighlighterTool,
+  HTMLTool,
   LineTool,
   LogseqPortalTool,
   NuEraseTool,
@@ -40,6 +41,7 @@ const tools: TLReactToolConstructor<Shape>[] = [
   PencilTool,
   TextTool,
   YouTubeTool,
+  HTMLTool,
   LogseqPortalTool,
 ]
 

+ 0 - 8
tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx

@@ -80,14 +80,6 @@ export const PrimaryTools = observer(function PrimaryTools() {
         >
           <TextIcon />
         </Button>
-        <Button
-          data-tool="youtube"
-          data-selected={selectedToolId === 'youtube'}
-          onClick={handleToolClick}
-          onDoubleClick={handleToolDoubleClick}
-        >
-          <VideoIcon />
-        </Button>
         <Button
           data-tool="logseq-portal"
           data-selected={selectedToolId === 'logseq-portal'}

+ 78 - 7
tldraw/apps/tldraw-logseq/src/hooks/usePaste.ts

@@ -6,12 +6,26 @@ import {
   TLBinding,
   TLShapeModel,
   uniqueId,
-  validUUID
+  validUUID,
 } from '@tldraw/core'
 import type { TLReactCallbacks } from '@tldraw/react'
 import * as React from 'react'
 import { NIL as NIL_UUID } from 'uuid'
-import { LogseqPortalShape, Shape, TextShape } from '~lib'
+import { HTMLShape, LogseqPortalShape, Shape, TextShape, YouTubeShape } from '~lib'
+
+const isValidURL = (url: string) => {
+  try {
+    new URL(url)
+    return true
+  } catch {
+    return false
+  }
+}
+
+const getYoutubeId = (url: string) => {
+  const match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#&?]*).*/)
+  return match && match[2].length === 11 ? match[2] : null
+}
 
 export function usePaste() {
   return React.useCallback<TLReactCallbacks<Shape>['onPaste']>(async (app, { point }) => {
@@ -48,6 +62,37 @@ export function usePaste() {
       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()
+
+        shapesToCreate.push({
+          ...HTMLShape.defaultProps,
+          html: rawText,
+          parentId: app.currentPageId,
+          point: [point[0], point[1]],
+        })
+        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
+    }
+
     async function handleLogseqShapes(item: ClipboardItem) {
       if (item.types.includes('text/plain')) {
         const blob = await item.getType('text/plain')
@@ -70,7 +115,6 @@ export function usePaste() {
             const clonedShapes = shapes.map(shape => {
               return {
                 ...shape,
-                id: uniqueId(),
                 parentId: app.currentPageId,
                 point: [
                   point[0] + shape.point![0] - commonBounds.minX,
@@ -115,7 +159,6 @@ export function usePaste() {
             if (validUUID(blockRef)) {
               shapesToCreate.push({
                 ...LogseqPortalShape.defaultProps,
-                id: uniqueId(),
                 parentId: app.currentPageId,
                 point: [point[0], point[1]],
                 size: [600, 400],
@@ -123,6 +166,26 @@ export function usePaste() {
                 blockType: 'B',
               })
             }
+          } else if (/^\[\[.*\]\]$/.test(rawText)) {
+            const pageName = rawText.slice(2, -2)
+            shapesToCreate.push({
+              ...LogseqPortalShape.defaultProps,
+              parentId: app.currentPageId,
+              point: [point[0], point[1]],
+              size: [600, 400],
+              pageId: pageName,
+              blockType: 'P',
+            })
+          } else if (isValidURL(rawText)) {
+            const youtubeId = getYoutubeId(rawText)
+            if (youtubeId) {
+              shapesToCreate.push({
+                ...YouTubeShape.defaultProps,
+                embedId: youtubeId,
+                parentId: app.currentPageId,
+                point: [point[0], point[1]],
+              })
+            }
           } else {
             // create text shape
             shapesToCreate.push({
@@ -138,10 +201,14 @@ export function usePaste() {
       return false
     }
 
-    // TODO: supporting other pasting formats
     for (const item of await navigator.clipboard.read()) {
       try {
         let handled = await handleImage(item)
+
+        if (!handled) {
+          handled = await handleHTML(item)
+        }
+
         if (!handled) {
           await handleLogseqShapes(item)
         }
@@ -152,7 +219,6 @@ export function usePaste() {
 
     const allShapesToAdd: TLShapeModel[] = [
       ...assetsToCreate.map((asset, i) => ({
-        id: uniqueId(),
         type: 'image',
         parentId: app.currentPageId,
         // TODO: Should be place near the last edited shape
@@ -162,7 +228,12 @@ export function usePaste() {
         opacity: 1,
       })),
       ...shapesToCreate,
-    ]
+    ].map(shape => {
+      return {
+        ...shape,
+        id: uniqueId(),
+      }
+    })
 
     app.wrapUpdate(() => {
       if (assetsToCreate.length > 0) {

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

@@ -93,8 +93,7 @@ export class PreviewManager {
             return (
               <g transform={transformArr.join(' ')} key={s.id}>
                 {s.getShapeSVGJsx({
-                  preview: true,
-                  assets: this.assets,
+                  assets: this.assets ?? [],
                 })}
               </g>
             )

+ 81 - 0
tldraw/apps/tldraw-logseq/src/lib/shapes/HTMLShape.tsx

@@ -0,0 +1,81 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as React from 'react'
+import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
+import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
+import { observer } from 'mobx-react-lite'
+import { CustomStyleProps, withClampedStyles } from './style-props'
+
+export interface HTMLShapeProps extends TLBoxShapeProps, CustomStyleProps {
+  type: 'html'
+  html: string
+}
+
+export class HTMLShape extends TLBoxShape<HTMLShapeProps> {
+  static id = 'html'
+
+  static defaultProps: HTMLShapeProps = {
+    id: 'html',
+    type: 'html',
+    parentId: 'page',
+    point: [0, 0],
+    size: [600, 320],
+    stroke: '#000000',
+    fill: '#ffffff',
+    strokeWidth: 2,
+    opacity: 1,
+    html: '',
+  }
+
+  aspectRatio = 480 / 853
+
+  canChangeAspectRatio = false
+
+  canFlip = false
+
+  canEdit = true
+
+  ReactComponent = observer(({ events, isErasing, isEditing }: TLComponentProps) => {
+    const {
+      props: { opacity, html },
+    } = this
+    return (
+      <HTMLContainer
+        style={{
+          overflow: 'hidden',
+          pointerEvents: 'all',
+          opacity: isErasing ? 0.2 : opacity,
+        }}
+        {...events}
+      >
+        <div
+          style={{
+            width: '100%',
+            height: '100%',
+            pointerEvents: isEditing ? 'all' : 'none',
+            userSelect: 'none',
+            position: 'relative',
+            margin: 0,
+          }}
+          dangerouslySetInnerHTML={{ __html: html }}
+        />
+      </HTMLContainer>
+    )
+  })
+
+  ReactIndicator = observer(() => {
+    const {
+      props: {
+        size: [w, h],
+      },
+    } = this
+    return <rect width={w} height={h} fill="transparent" />
+  })
+
+  validateProps = (props: Partial<HTMLShapeProps>) => {
+    if (props.size !== undefined) {
+      props.size[0] = Math.max(props.size[0], 1)
+      props.size[1] = Math.max(props.size[0] * this.aspectRatio, 1)
+    }
+    return withClampedStyles(props)
+  }
+}

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

@@ -65,8 +65,6 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
     const {
       props: { opacity, embedId },
     } = this
-    const app = useApp()
-    const isSelected = app.selectedIds.has(this.id)
     return (
       <HTMLContainer
         style={{
@@ -76,24 +74,10 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
         }}
         {...events}
       >
-        {embedId && (
-          <div
-            style={{
-              height: '32px',
-              width: '100%',
-              background: '#bbb',
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'center',
-            }}
-          >
-            {embedId}
-          </div>
-        )}
         <div
           style={{
             width: '100%',
-            height: embedId ? 'calc(100% - 32px)' : '100%',
+            height: '100%',
             pointerEvents: isEditing ? 'all' : 'none',
             userSelect: 'none',
             position: 'relative',
@@ -115,6 +99,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
                   height: '100%',
                   width: '100%',
                   position: 'absolute',
+                  margin: 0,
                 }}
                 width="853"
                 height="480"

+ 7 - 3
tldraw/apps/tldraw-logseq/src/lib/shapes/index.ts

@@ -1,15 +1,16 @@
+import type { TLReactShapeConstructor } from '@tldraw/react'
 import { BoxShape } from './BoxShape'
 import { DotShape } from './DotShape'
 import { EllipseShape } from './EllipseShape'
 import { HighlighterShape } from './HighlighterShape'
+import { HTMLShape } from './HTMLShape'
 import { ImageShape } from './ImageShape'
 import { LineShape } from './LineShape'
+import { LogseqPortalShape } from './LogseqPortalShape'
 import { PencilShape } from './PencilShape'
 import { PolygonShape } from './PolygonShape'
 import { TextShape } from './TextShape'
 import { YouTubeShape } from './YouTubeShape'
-import { LogseqPortalShape } from './LogseqPortalShape'
-import type { TLReactShapeConstructor } from '@tldraw/react'
 
 export type Shape =
   | BoxShape
@@ -23,19 +24,21 @@ export type Shape =
   | PolygonShape
   | TextShape
   | YouTubeShape
+  | HTMLShape
   | LogseqPortalShape
 
 export * from './BoxShape'
 export * from './DotShape'
 export * from './EllipseShape'
 export * from './HighlighterShape'
+export * from './HTMLShape'
 export * from './ImageShape'
 export * from './LineShape'
+export * from './LogseqPortalShape'
 export * from './PencilShape'
 export * from './PolygonShape'
 export * from './TextShape'
 export * from './YouTubeShape'
-export * from './LogseqPortalShape'
 
 export const shapes: TLReactShapeConstructor<Shape>[] = [
   BoxShape,
@@ -48,5 +51,6 @@ export const shapes: TLReactShapeConstructor<Shape>[] = [
   PolygonShape,
   TextShape,
   YouTubeShape,
+  HTMLShape,
   LogseqPortalShape,
 ]

+ 11 - 0
tldraw/apps/tldraw-logseq/src/lib/tools/HTMLTool.tsx

@@ -0,0 +1,11 @@
+import { TLBoxTool } from '@tldraw/core'
+import type { TLReactEventMap } from '@tldraw/react'
+import { HTMLShape, Shape } from '~lib/shapes'
+
+export class HTMLTool extends TLBoxTool<HTMLShape, Shape, TLReactEventMap> {
+  static id = 'youtube'
+  Shape = HTMLShape
+}
+
+export { }
+

+ 1 - 0
tldraw/apps/tldraw-logseq/src/lib/tools/index.ts

@@ -9,3 +9,4 @@ export * from './PolygonTool'
 export * from './TextTool'
 export * from './YouTubeTool'
 export * from './LogseqPortalTool'
+export * from './HTMLTool'