External content sources
Turn pasted text/html into a custom shape by overriding the text external content handler.
import { useCallback } from 'react'
import {
BaseBoxShapeUtil,
defaultHandleExternalTextContent,
Editor,
HTMLContainer,
Tldraw,
TLShape,
} from 'tldraw'
import 'tldraw/tldraw.css'
const DANGEROUS_HTML_TYPE = 'dangerous-html'
declare module 'tldraw' {
export interface TLGlobalShapePropsMap {
[DANGEROUS_HTML_TYPE]: { w: number; h: number; html: string }
}
}
// There's a guide at the bottom of this page!
// [1]
type DangerousHtmlShape = TLShape<typeof DANGEROUS_HTML_TYPE>
// [2]
class DangerousHtmlShapeUtil extends BaseBoxShapeUtil<DangerousHtmlShape> {
static override type = DANGEROUS_HTML_TYPE
override getDefaultProps() {
return {
w: 500,
h: 300,
html: '<div>hello</div>',
}
}
override component(shape: DangerousHtmlShape) {
return (
<HTMLContainer style={{ overflow: 'auto' }}>
<div dangerouslySetInnerHTML={{ __html: shape.props.html }}></div>
</HTMLContainer>
)
}
override getIndicatorPath(shape: DangerousHtmlShape) {
const path = new Path2D()
path.rect(0, 0, shape.props.w, shape.props.h)
return path
}
}
const shapeUtils = [DangerousHtmlShapeUtil]
export default function ExternalContentSourcesExample() {
const handleMount = useCallback((editor: Editor) => {
// [3]
editor.registerExternalContentHandler('text', async (content) => {
// [4]
const htmlSource = content.sources?.find((s) => s.type === 'text' && s.subtype === 'html')
if (htmlSource) {
const center = content.point ?? editor.getViewportPageBounds().center
editor.createShape({
type: DANGEROUS_HTML_TYPE,
x: center.x - 250,
y: center.y - 150,
props: {
html: htmlSource.data,
},
})
} else {
await defaultHandleExternalTextContent(editor, content)
}
})
}, [])
return (
<div className="tldraw__editor">
<Tldraw onMount={handleMount} shapeUtils={shapeUtils} />
</div>
)
}
/*
When content arrives from outside the editor (paste, drop, or `putExternalContent`), it's
classified as text, files, url, svg-text, embed, or tldraw content and routed to the handler
registered for that type. This example replaces the text handler so that pasted `text/html`
becomes a shape that renders the HTML directly.
[1]
The shape type. Its `html` prop holds the raw pasted markup.
[2]
A minimal box shape that renders its `html` with `dangerouslySetInnerHTML`. It's called
"dangerous" for a reason: never do this with HTML you don't trust. See the custom shape
examples for a fuller shape util.
[3]
`registerExternalContentHandler('text', ...)` replaces the built-in text handler for this
editor. The other content types (`files`, `url`, `svg-text`, `embed`, `tldraw`) can be
overridden the same way.
[4]
Pasted text arrives with a `sources` array, one entry per clipboard flavor. Rich text
editors and code editors usually put both `text/plain` and `text/html` on the clipboard.
When there's an HTML source we create our shape at the paste point (or the viewport center
for keyboard paste); otherwise we defer to `defaultHandleExternalTextContent`, which
creates a normal text shape.
*/
Content pasted or dropped into the editor is dispatched to a handler by type: text, files, url, svg-text, embed, or tldraw. This example calls editor.registerExternalContentHandler('text', ...) to replace the text handler. When the pasted text has an HTML source among its sources, it creates a dangerous-html shape that renders the markup with dangerouslySetInnerHTML; plain text falls back to defaultHandleExternalTextContent.
Try copying a few lines of code from VS Code, or some formatted text from a web page, and pasting them onto the canvas. Rendering arbitrary pasted HTML is unsafe outside of a demo, which is why the shape is named the way it is.
Is this page helpful?
Prev
Custom paste behaviorNext
Clipboard events