Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions app/ComponentShape/ComponentShape.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useEffect, useRef } from 'react'
import { BaseBoxShapeUtil, DefaultSpinner, HTMLContainer, TLBaseShape, Vec } from 'tldraw'

export type ComponentShape = TLBaseShape<
'component',
{
source: string
w: number
h: number
image: string
imageW: number
imageH: number
}
>

export class ComponentShapeUtil extends BaseBoxShapeUtil<ComponentShape> {
static override type = 'component' as const

getDefaultProps(): ComponentShape['props'] {
return {
source: '',
w: 100,
h: 100,
image: '',
imageW: 100,
imageH: 100,
}
}

// override onClick = (shape: ComponentShape) => {}

override canEdit = () => false
override isAspectRatioLocked = (_shape: ComponentShape) => false
override canResize = (_shape: ComponentShape) => true
override canBind = (_shape: ComponentShape) => false

override component(shape: ComponentShape) {
const divRef = useRef<HTMLDivElement>(null)
// Update the div with source
useEffect(() => {
if (!divRef.current) return
if (divRef.current.innerHTML === shape.props.source) return
divRef.current.innerHTML = shape.props.source
const divWidth = divRef.current.clientWidth
const divHeight = divRef.current.clientHeight
// this.editor.updateShape({
// type: 'component',
// id: shape.id,
// props: {
// w: divWidth,
// h: divHeight,
// },
// })
}, [shape.props.source, shape.id])

return (
<HTMLContainer
className="tl-embed-container"
id={shape.id}
style={{ border: '1px solid #0000001f' }}
>
<div
className="component-shape-div"
style={
{
// backgroundColor: 'var(--color-culled)',
// width: 'fit-content',
// height: 'fit-content',
// display: 'flex',
// flexDirection: 'column',
// height: '100%',
// justifyContent: 'center',
// alignItems: 'center',
}
}
onPointerDown={(e) => {
e.stopPropagation()
}}
ref={divRef}
></div>
{!shape.props.source && (
<div
style={{
width: '100%',
height: '100%',
backgroundColor: 'var(--color-culled)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
// boxShadow,
border: '1px solid var(--color-panel-contrast)',
borderRadius: 'var(--radius-2)',
}}
>
<DefaultSpinner />
</div>
)}
</HTMLContainer>
)
}

override toSvg(shape: ComponentShape) {
const imageUrl = shape.props.image
if (!imageUrl) return null

const widthDiff = shape.props.w - shape.props.imageW
const heightDiff = shape.props.h - shape.props.imageH
const x = widthDiff / 2
const y = heightDiff / 2
return (
<image href={imageUrl} width={shape.props.imageW} height={shape.props.imageH} x={x} y={y} />
)
}

indicator(shape: ComponentShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
}

// todo: export these from tldraw

const ROTATING_BOX_SHADOWS = [
{
offsetX: 0,
offsetY: 2,
blur: 4,
spread: -1,
color: '#0000003a',
},
{
offsetX: 0,
offsetY: 3,
blur: 12,
spread: -2,
color: '#0000001f',
},
]

export function getRotatedBoxShadow(rotation: number) {
const cssStrings = ROTATING_BOX_SHADOWS.map((shadow) => {
const { offsetX, offsetY, blur, spread, color } = shadow
const vec = new Vec(offsetX, offsetY)
const { x, y } = vec.rot(-rotation)
return `${x}px ${y}px ${blur}px ${spread}px ${color}`
})
return cssStrings.join(', ')
}
44 changes: 32 additions & 12 deletions app/PreviewShape/PreviewShape.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { ReactElement, useEffect } from 'react'
import {
BaseBoxShapeUtil,
DefaultSpinner,
Expand All @@ -12,9 +13,9 @@ import {
useToasts,
useValue,
} from 'tldraw'
import { useEffect } from 'react'
import { Dropdown } from '../components/Dropdown'
import { LINK_HOST, PROTOCOL } from '../lib/hosts'
import { getSandboxPermissions } from '../lib/iframe'
import { uploadLink } from '../lib/uploadLink'

export type PreviewShape = TLBaseShape<
Expand Down Expand Up @@ -47,7 +48,6 @@ export class PreviewShapeUtil extends BaseBoxShapeUtil<PreviewShape> {
override isAspectRatioLocked = (_shape: PreviewShape) => false
override canResize = (_shape: PreviewShape) => true
override canBind = (_shape: PreviewShape) => false
override canUnmount = () => false

override component(shape: PreviewShape) {
const isEditing = useIsEditing(shape.id)
Expand Down Expand Up @@ -124,6 +124,23 @@ export class PreviewShapeUtil extends BaseBoxShapeUtil<PreviewShape> {
border: '1px solid var(--color-panel-contrast)',
borderRadius: 'var(--radius-2)',
}}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
sandbox={getSandboxPermissions({
'allow-downloads-without-user-activation': false,
'allow-downloads': true,
'allow-modals': true,
'allow-orientation-lock': false,
'allow-pointer-lock': true,
'allow-popups': true,
'allow-popups-to-escape-sandbox': true,
'allow-presentation': true,
'allow-storage-access-by-user-activation': true,
'allow-top-navigation': true,
'allow-top-navigation-by-user-activation': true,
'allow-scripts': true,
'allow-same-origin': true,
'allow-forms': true,
})}
/>
<div
style={{
Expand Down Expand Up @@ -179,25 +196,28 @@ export class PreviewShapeUtil extends BaseBoxShapeUtil<PreviewShape> {
)
}

override toSvg(shape: PreviewShape, _ctx: SvgExportContext): SVGElement | Promise<SVGElement> {
override toSvg(shape: PreviewShape, _ctx: SvgExportContext): Promise<ReactElement> {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
// while screenshot is the same as the old one, keep waiting for a new one
return new Promise((resolve, _) => {
if (window === undefined) return resolve(g)
if (window === undefined) return resolve(<g></g>)
const windowListener = (event: MessageEvent) => {
if (event.data.screenshot && event.data?.shapeid === shape.id) {
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image')
image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', event.data.screenshot)
image.setAttribute('width', shape.props.w.toString())
image.setAttribute('height', shape.props.h.toString())
g.appendChild(image)
window.removeEventListener('message', windowListener)
clearTimeout(timeOut)
resolve(g)
resolve(
<g>
<image
href={event.data.screenshot}
width={shape.props.w}
height={shape.props.h}
></image>
</g>
)
}
}
const timeOut = setTimeout(() => {
resolve(g)
resolve(<g></g>)
window.removeEventListener('message', windowListener)
}, 2000)
window.addEventListener('message', windowListener)
Expand Down Expand Up @@ -238,7 +258,7 @@ const ROTATING_BOX_SHADOWS = [
},
]

function getRotatedBoxShadow(rotation: number) {
export function getRotatedBoxShadow(rotation: number) {
const cssStrings = ROTATING_BOX_SHADOWS.map((shadow) => {
const { offsetX, offsetY, blur, spread, color } = shadow
const vec = new Vec(offsetX, offsetY)
Expand Down
Loading