Tldraw/packages/ui/src/lib/hooks/useCopyAs.ts

131 wiersze
3.4 KiB
TypeScript

import { App, getSvgAsImage, getSvgAsString, TLCopyType, TLShapeId, useApp } from '@tldraw/editor'
import { useCallback } from 'react'
import { useToasts } from './useToastsProvider'
import { useTranslation } from './useTranslation/useTranslation'
/** @public */
export function useCopyAs() {
const app = useApp()
const { addToast } = useToasts()
const msg = useTranslation()
return useCallback(
// it's important that this function itself isn't async - we need to
// create the relevant `ClipboardItem`s synchronously to make sure
// safari knows that the user _wants_ to copy:
// https://bugs.webkit.org/show_bug.cgi?id=222262
//
// this is fine for navigator.clipboard.write, but for fallbacks it's a
// little awkward.
function copyAs(ids: TLShapeId[] = app.selectedIds, format: TLCopyType = 'svg') {
if (ids.length === 0) {
ids = [...app.shapeIds]
}
if (ids.length === 0) {
return
}
switch (format) {
case 'svg': {
if (window.navigator.clipboard) {
if (window.navigator.clipboard.write) {
window.navigator.clipboard.write([
new ClipboardItem({
'text/plain': getExportedSvgBlob(app, ids),
}),
])
} else {
fallbackWriteTextAsync(async () =>
getSvgAsString(await getExportSvgElement(app, ids))
)
}
}
break
}
case 'jpeg':
case 'png': {
const mimeType = format === 'jpeg' ? 'image/jpeg' : 'image/png'
window.navigator.clipboard.write([
new ClipboardItem({
// Note: This needs to use the promise based approach for safari/ios to not bail on a permissions error.
[mimeType]: getExportedImageBlob(app, ids, format).then((blob) => {
if (blob) {
if (window.navigator.clipboard) {
return blob
}
throw new Error('Copy not supported')
} else {
addToast({
id: 'copy-fail',
icon: 'warning-triangle',
title: msg('toast.error.copy-fail.title'),
description: msg('toast.error.copy-fail.desc'),
})
throw new Error('Copy not possible')
}
}),
}),
])
break
}
case 'json': {
const data = app.getContent(ids)
if (window.navigator.clipboard) {
const jsonStr = JSON.stringify(data)
if (window.navigator.clipboard.write) {
window.navigator.clipboard.write([
new ClipboardItem({
'text/plain': new Blob([jsonStr], { type: 'text/plain' }),
}),
])
} else {
fallbackWriteTextAsync(async () => jsonStr)
}
}
break
}
default:
throw new Error(`Copy type ${format} not supported.`)
}
},
[app, addToast, msg]
)
}
async function getExportSvgElement(app: App, ids: TLShapeId[]) {
const svg = await app.getSvg(ids, {
scale: 1,
background: app.instanceState.exportBackground,
})
if (!svg) throw new Error('Could not construct SVG.')
return svg
}
async function getExportedSvgBlob(app: App, ids: TLShapeId[]) {
return new Blob([getSvgAsString(await getExportSvgElement(app, ids))], {
type: 'text/plain',
})
}
async function getExportedImageBlob(app: App, ids: TLShapeId[], format: 'png' | 'jpeg') {
return await getSvgAsImage(await getExportSvgElement(app, ids), {
type: format,
quality: 1,
scale: 2,
})
}
async function fallbackWriteTextAsync(getText: () => Promise<string>) {
navigator.clipboard.writeText(await getText())
}