mirror of
https://github.com/logseq/logseq.git
synced 2026-05-23 20:24:15 +00:00
feat: add html shape to allow pasting iframes
This commit is contained in:
@@ -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,
|
||||
]
|
||||
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
tldraw/apps/tldraw-logseq/src/lib/shapes/HTMLShape.tsx
Normal file
81
tldraw/apps/tldraw-logseq/src/lib/shapes/HTMLShape.tsx
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
tldraw/apps/tldraw-logseq/src/lib/tools/HTMLTool.tsx
Normal file
11
tldraw/apps/tldraw-logseq/src/lib/tools/HTMLTool.tsx
Normal 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 { }
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from './PolygonTool'
|
||||
export * from './TextTool'
|
||||
export * from './YouTubeTool'
|
||||
export * from './LogseqPortalTool'
|
||||
export * from './HTMLTool'
|
||||
|
||||
Reference in New Issue
Block a user