feat: add html shape to allow pasting iframes

This commit is contained in:
Peng Xiao
2022-07-26 10:26:13 +08:00
parent 66a08f8916
commit b397ee097f
9 changed files with 183 additions and 37 deletions

View File

@@ -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,
]

View File

@@ -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'}

View File

@@ -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) {

View File

@@ -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>
)

View File

@@ -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)
}
}

View File

@@ -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"

View File

@@ -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,
]

View File

@@ -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 { }

View File

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