Added exporting of shapes and pages as images (#468)

* Added exporting of shapeses

* added video serialization

* Fix viewport sizes, add chrome-aws-lambda for puppeteer

* Update menu styling

* extract to callback

* Update Loading.tsx

* force update menu

* fix missing fonts

* Added SVG and JSON export

* Fix json exports

* Merge branch 'main' into pr/468, update menus

* Update TldrawApp.ts

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
pull/494/head
Faraz Shaikh 2022-01-10 20:36:28 +04:00 zatwierdzone przez GitHub
rodzic e62755ef10
commit 6103febaaf
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
24 zmienionych plików z 494 dodań i 226 usunięć

Wyświetl plik

@ -1,7 +1,8 @@
import React from 'react'
import * as gtag from 'utils/gtag'
import { Tldraw, TldrawApp, useFileSystem } from '@tldraw/tldraw'
import { Tldraw, TldrawApp, TldrawProps, useFileSystem } from '@tldraw/tldraw'
import { useAccountHandlers } from 'hooks/useAccountHandlers'
import { exportToImage } from 'utils/export'
declare const window: Window & { app: TldrawApp }
@ -11,7 +12,12 @@ interface EditorProps {
isSponsor?: boolean
}
export default function Editor({ id = 'home', isUser = false, isSponsor = false }: EditorProps) {
export default function Editor({
id = 'home',
isUser = false,
isSponsor = false,
...rest
}: EditorProps & Partial<TldrawProps>) {
const handleMount = React.useCallback((app: TldrawApp) => {
window.app = app
}, [])
@ -40,7 +46,9 @@ export default function Editor({ id = 'home', isUser = false, isSponsor = false
showSponsorLink={!isSponsor}
onSignIn={isSponsor ? undefined : onSignIn}
onSignOut={isUser ? onSignOut : undefined}
onExport={exportToImage}
{...fileSystemEvents}
{...rest}
/>
</div>
)

Wyświetl plik

@ -6,6 +6,7 @@ import { LiveblocksProvider, RoomProvider } from '@liveblocks/react'
import { useAccountHandlers } from 'hooks/useAccountHandlers'
import { styled } from 'styles'
import { useMultiplayerState } from 'hooks/useMultiplayerState'
import { exportToImage } from 'utils/export'
import { useMultiplayerAssets } from 'hooks/useMultiplayerAssets'
const client = createClient({
@ -58,6 +59,7 @@ function Editor({
showSponsorLink={!isSponsor}
onSignIn={isSponsor ? undefined : onSignIn}
onSignOut={isUser ? onSignOut : undefined}
onExport={exportToImage}
onAssetCreate={onAssetCreate}
onAssetDelete={onAssetDelete}
{...fileSystemEvents}

Wyświetl plik

@ -0,0 +1,73 @@
import { NextApiRequest, NextApiResponse } from 'next'
import puppeteer from 'puppeteer'
import Cors from 'cors'
import { TDExport, TDExportTypes, TldrawApp } from '@tldraw/tldraw'
const cors = Cors({
methods: ['POST'],
})
function runMiddleware(req, res, fn) {
return new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) return reject(result)
return resolve(result)
})
})
}
const FRONTEND_URL =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000/?exportMode'
: 'https://www.tldraw.com/?exportMode'
declare global {
interface Window {
app: TldrawApp
}
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
await runMiddleware(req, res, cors)
const { body } = req
const {
size: [width, height],
type,
} = body
if (type === TDExportTypes.PDF) res.status(500).send('Not implemented yet.')
let browser: puppeteer.Browser = null
try {
browser = await puppeteer.launch({
slowMo: 50,
ignoreHTTPSErrors: true,
headless: true,
})
const page = await browser.newPage()
await page.setUserAgent(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
)
await page.setViewport({ width: Math.floor(width), height: Math.floor(height) })
await page.goto(FRONTEND_URL, { timeout: 15 * 1000, waitUntil: 'networkidle0' })
await page.evaluateHandle('document.fonts.ready')
await page.evaluate(async (body: TDExport) => {
let app = window.app
if (!app) app = await new Promise((resolve) => setTimeout(() => resolve(window.app), 250))
await app.ready
const { assets, shapes } = body
app.patchAssets(assets)
app.createShapes(...shapes)
app.selectAll()
app.zoomToSelection()
app.selectNone()
}, body)
const imageBuffer = await page.screenshot({
type,
})
res.status(200).send(imageBuffer)
} catch (err) {
console.error(err.message)
res.status(500).send(err)
} finally {
await browser.close()
}
}

Wyświetl plik

@ -2,6 +2,8 @@ import dynamic from 'next/dynamic'
import type { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/react'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useMemo } from 'react'
const Editor = dynamic(() => import('components/Editor'), { ssr: false })
@ -11,12 +13,15 @@ interface PageProps {
}
export default function Home({ isUser, isSponsor }: PageProps): JSX.Element {
const { query } = useRouter()
const isExportMode = useMemo(() => 'exportMode' in query, [query])
return (
<>
<Head>
<title>tldraw</title>
</Head>
<Editor id="home" isUser={isUser} isSponsor={isSponsor} />
<Editor id="home" isUser={isUser} isSponsor={isSponsor} showUI={!isExportMode} />
</>
)
}
@ -27,7 +32,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
return {
props: {
isUser: session?.user ? true : false,
isSponsor: session?.isSponsor,
isSponsor: session?.isSponsor || false,
},
}
}

Wyświetl plik

@ -0,0 +1,29 @@
import { TDExport, TDExportTypes } from '@tldraw/tldraw'
export const EXPORT_ENDPOINT =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000/api/export'
: 'https://www.tldraw.com/api/export'
export async function exportToImage(info: TDExport) {
if (info.serialized) {
const link = document.createElement('a')
link.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(info.serialized)
link.download = info.name + '.' + info.type
link.click()
return
}
const response = await fetch(EXPORT_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(info),
})
const blob = await response.blob()
const blobUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = blobUrl
link.download = info.name + '.' + info.type
link.click()
}

Wyświetl plik

@ -1,4 +1,4 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700');
html,
* {

Wyświetl plik

@ -0,0 +1,33 @@
import * as React from 'react'
import { TDExport, Tldraw } from '@tldraw/tldraw'
export default function Export(): JSX.Element {
const handleExport = React.useCallback(async (info: TDExport) => {
if (info.serialized) {
const link = document.createElement('a')
link.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(info.serialized)
link.download = info.name + '.' + info.type
link.click()
return
}
const response = await fetch('some_serverless_endpoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(info),
})
const blob = await response.blob()
const blobUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = blobUrl
link.download = info.name + '.' + info.type
link.click()
}, [])
return (
<div className="tldraw">
<Tldraw onExport={handleExport} />
</div>
)
}

Wyświetl plik

@ -53,6 +53,7 @@
"@tldraw/vec": "^1.4.3",
"idb-keyval": "^6.0.3",
"perfect-freehand": "^1.0.16",
"puppeteer": "^13.0.1",
"react-hotkeys-hook": "^3.4.0",
"tslib": "^2.3.1",
"zustand": "^3.6.5"

Wyświetl plik

@ -1,7 +1,7 @@
import * as React from 'react'
import { Renderer } from '@tldraw/core'
import { styled, dark } from '~styles'
import { TDDocument, TDShape, TDBinding, TDStatus, TDUser, TDAsset } from '~types'
import { TDDocument, TDStatus } from '~types'
import { TldrawApp, TDCallbacks } from '~state'
import { TldrawContext, useStylesheet, useKeyboardShortcuts, useTldrawApp } from '~hooks'
import { shapeUtils } from '~state/shapes'
@ -86,85 +86,6 @@ export interface TldrawProps extends TDCallbacks {
* bucket based solution will cause massive base64 string to be written to the liveblocks room.
*/
disableAssets?: boolean
/**
* (optional) A callback to run when the component mounts.
*/
onMount?: (state: TldrawApp) => void
/**
* (optional) A callback to run when the user creates a new project through the menu or through a keyboard shortcut.
*/
onNewProject?: (state: TldrawApp, e?: KeyboardEvent) => void
/**
* (optional) A callback to run when the user saves a project through the menu or through a keyboard shortcut.
*/
onSaveProject?: (state: TldrawApp, e?: KeyboardEvent) => void
/**
* (optional) A callback to run when the user saves a project as a new project through the menu or through a keyboard shortcut.
*/
onSaveProjectAs?: (state: TldrawApp, e?: KeyboardEvent) => void
/**
* (optional) A callback to run when the user opens new project through the menu or through a keyboard shortcut.
*/
onOpenProject?: (state: TldrawApp, e?: KeyboardEvent) => void
/**
* (optional) A callback to run when the user signs in via the menu.
*/
onSignIn?: (state: TldrawApp) => void
/**
* (optional) A callback to run when the user signs out via the menu.
*/
onSignOut?: (state: TldrawApp) => void
/**
* (optional) A callback to run when the user creates a new project.
*/
onChangePresence?: (state: TldrawApp, user: TDUser) => void
/**
* (optional) A callback to run when the component's state changes.
*/
onChange?: (state: TldrawApp, reason?: string) => void
/**
* (optional) A callback to run when the state is patched.
*/
onPatch?: (state: TldrawApp, reason?: string) => void
/**
* (optional) A callback to run when the state is changed with a command.
*/
onCommand?: (state: TldrawApp, reason?: string) => void
/**
* (optional) A callback to run when the state is persisted.
*/
onPersist?: (state: TldrawApp) => void
/**
* (optional) A callback to run when the user undos.
*/
onUndo?: (state: TldrawApp) => void
/**
* (optional) A callback to run when the user redos.
*/
onRedo?: (state: TldrawApp) => void
/**
* (optional) A callback to run when an asset will be deleted.
*/
onAssetDelete?: (assetId: string) => void
/**
* (optional) A callback to run when an asset will be created. Should return the value for the image/video's `src` property.
*/
onAssetCreate?: (file: File, id: string) => Promise<string | false>
onChangePage?: (
app: TldrawApp,
shapes: Record<string, TDShape | undefined>,
bindings: Record<string, TDBinding | undefined>,
assets: Record<string, TDAsset | undefined>
) => void
}
export function Tldraw({
@ -199,6 +120,7 @@ export function Tldraw({
onChangePage,
onAssetCreate,
onAssetDelete,
onExport,
}: TldrawProps) {
const [sId, setSId] = React.useState(id)
@ -250,6 +172,7 @@ export function Tldraw({
onChangePage,
onAssetDelete,
onAssetCreate,
onExport,
})
setSId(id)
setApp(newApp)
@ -303,6 +226,7 @@ export function Tldraw({
onChangePage,
onAssetDelete,
onAssetCreate,
onExport,
}
}, [
onMount,
@ -323,6 +247,7 @@ export function Tldraw({
onChangePage,
onAssetDelete,
onAssetCreate,
onExport,
])
// Use the `key` to ensure that new selector hooks are made when the id changes

Wyświetl plik

@ -2,7 +2,7 @@ import * as React from 'react'
import { styled } from '~styles'
import * as RadixContextMenu from '@radix-ui/react-context-menu'
import { useTldrawApp } from '~hooks'
import { TDSnapshot, AlignType, DistributeType, StretchType } from '~types'
import { TDSnapshot, AlignType, DistributeType, StretchType, TDExportTypes } from '~types'
import {
AlignBottomIcon,
AlignCenterHorizontallyIcon,
@ -130,6 +130,26 @@ const InnerMenu = React.memo(function InnerMenu({ onBlur }: InnerContextMenuProp
app.redo()
}, [app])
const handleExportPNG = React.useCallback(async () => {
await app.exportSelectedShapesAs(TDExportTypes.PNG)
}, [app])
const handleExportJPG = React.useCallback(async () => {
await app.exportSelectedShapesAs(TDExportTypes.JPG)
}, [app])
const handleExportWEBP = React.useCallback(async () => {
await app.exportSelectedShapesAs(TDExportTypes.WEBP)
}, [app])
const handleExportSVG = React.useCallback(async () => {
await app.exportSelectedShapesAs(TDExportTypes.SVG)
}, [app])
const handleExportJSON = React.useCallback(async () => {
await app.exportSelectedShapesAs(TDExportTypes.JSON)
}, [app])
const hasSelection = numberOfSelectedIds > 0
const hasTwoOrMore = numberOfSelectedIds > 1
const hasThreeOrMore = numberOfSelectedIds > 2
@ -188,6 +208,31 @@ const InnerMenu = React.memo(function InnerMenu({ onBlur }: InnerContextMenuProp
{hasTwoOrMore && (
<AlignDistributeSubMenu hasTwoOrMore={hasTwoOrMore} hasThreeOrMore={hasThreeOrMore} />
)}
{app.callbacks.onExport ? (
<>
<Divider />
<ContextMenuSubMenu label="Export" size="small">
<CMRowButton onClick={handleExportPNG}>PNG</CMRowButton>
<CMRowButton onClick={handleExportJPG}>JPG</CMRowButton>
<CMRowButton onClick={handleExportWEBP}>WEBP</CMRowButton>
<CMRowButton onClick={handleExportSVG}>SVG</CMRowButton>
<CMRowButton onClick={handleExportJSON}>JSON</CMRowButton>
<Divider />
<CMRowButton onClick={handleCopySvg} kbd="#⇧C">
Copy as SVG
</CMRowButton>
{isDebugMode && <CMRowButton onClick={handleCopyJson}>Copy as JSON</CMRowButton>}
</ContextMenuSubMenu>
</>
) : (
<>
<Divider />
<CMRowButton onClick={handleCopySvg} kbd="#⇧C">
Copy as SVG
</CMRowButton>
{isDebugMode && <CMRowButton onClick={handleCopyJson}>Copy as JSON</CMRowButton>}
</>
)}
<Divider />
<CMRowButton onClick={handleCut} kbd="#X">
Cut
@ -195,13 +240,10 @@ const InnerMenu = React.memo(function InnerMenu({ onBlur }: InnerContextMenuProp
<CMRowButton onClick={handleCopy} kbd="#C">
Copy
</CMRowButton>
<CMRowButton onClick={handleCopySvg} kbd="#⇧C">
Copy as SVG
</CMRowButton>
{isDebugMode && <CMRowButton onClick={handleCopyJson}>Copy as JSON</CMRowButton>}
<CMRowButton onClick={handlePaste} kbd="#V">
Paste
</CMRowButton>
<Divider />
<CMRowButton onClick={handleDelete} kbd="⌫">
Delete
@ -376,15 +418,20 @@ function MoveToPageMenu(): JSX.Element | null {
export interface ContextMenuSubMenuProps {
label: string
size?: 'small'
children: React.ReactNode
}
export function ContextMenuSubMenu({ children, label }: ContextMenuSubMenuProps): JSX.Element {
export function ContextMenuSubMenu({
children,
label,
size,
}: ContextMenuSubMenuProps): JSX.Element {
return (
<RadixContextMenu.Root dir="ltr">
<CMTriggerButton isSubmenu>{label}</CMTriggerButton>
<RadixContextMenu.Content dir="ltr" sideOffset={2} alignOffset={-2} asChild>
<MenuContent>
<MenuContent size={size}>
{children}
<CMArrow offset={13} />
</MenuContent>

Wyświetl plik

@ -20,7 +20,7 @@ const StyledLoadingPanelContainer = styled('div', {
transform: `translate(-50%, 0)`,
borderBottomLeftRadius: '12px',
borderBottomRightRadius: '12px',
padding: '8px',
padding: '8px 16px',
fontFamily: 'var(--fonts-ui)',
fontSize: 'var(--fontSizes-1)',
boxShadow: 'var(--shadows-panel)',

Wyświetl plik

@ -5,11 +5,17 @@ import { MenuContent } from '~components/Primitives/MenuContent'
export interface DMSubMenuProps {
label: string
size?: 'small'
disabled?: boolean
children: React.ReactNode
}
export function DMSubMenu({ children, disabled = false, label }: DMSubMenuProps): JSX.Element {
export function DMSubMenu({
children,
size,
disabled = false,
label,
}: DMSubMenuProps): JSX.Element {
return (
<Root dir="ltr">
<TriggerItem dir="ltr" asChild>
@ -18,7 +24,7 @@ export function DMSubMenu({ children, disabled = false, label }: DMSubMenuProps)
</RowButton>
</TriggerItem>
<Content dir="ltr" asChild sideOffset={2} alignOffset={-2}>
<MenuContent>
<MenuContent size={size}>
{children}
<Arrow offset={13} />
</MenuContent>

Wyświetl plik

@ -14,4 +14,11 @@ export const MenuContent = styled('div', {
padding: '$2 $2',
borderRadius: '$3',
font: '$ui',
variants: {
size: {
small: {
minWidth: 72,
},
},
},
})

Wyświetl plik

@ -15,7 +15,7 @@ import { useFileSystemHandlers } from '~hooks'
import { HeartIcon } from '~components/Primitives/icons/HeartIcon'
import { preventEvent } from '~components/preventEvent'
import { DiscordIcon } from '~components/Primitives/icons'
import type { TDSnapshot } from '~types'
import { TDExportTypes, TDSnapshot } from '~types'
import { Divider } from '~components/Primitives/Divider'
interface MenuProps {
@ -33,11 +33,41 @@ const disableAssetsSelector = (s: TDSnapshot) => {
export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: MenuProps) {
const app = useTldrawApp()
const numberOfSelectedIds = app.useStore(numberOfSelectedIdsSelector)
const disableAssets = app.useStore(disableAssetsSelector)
const [_, setForce] = React.useState(0)
React.useEffect(() => setForce(1), [])
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
const handleExportPNG = React.useCallback(async () => {
await app.exportAllShapesAs(TDExportTypes.PNG)
}, [app])
const handleExportJPG = React.useCallback(async () => {
await app.exportAllShapesAs(TDExportTypes.JPG)
}, [app])
const handleExportWEBP = React.useCallback(async () => {
await app.exportAllShapesAs(TDExportTypes.WEBP)
}, [app])
const handleExportPDF = React.useCallback(async () => {
await app.exportAllShapesAs(TDExportTypes.PDF)
}, [app])
const handleExportSVG = React.useCallback(async () => {
await app.exportAllShapesAs(TDExportTypes.SVG)
}, [app])
const handleExportJSON = React.useCallback(async () => {
await app.exportAllShapesAs(TDExportTypes.JSON)
}, [app])
const handleSignIn = React.useCallback(() => {
app.callbacks.onSignIn?.(app)
}, [app])
@ -82,7 +112,8 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
app.callbacks.onNewProject ||
app.callbacks.onOpenProject ||
app.callbacks.onSaveProject ||
app.callbacks.onSaveProjectAs
app.callbacks.onSaveProjectAs ||
app.callbacks.onExport
const showSignInOutMenu = app.callbacks.onSignIn || app.callbacks.onSignOut || showSponsorLink
@ -116,6 +147,18 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
Save As...
</DMItem>
)}
{app.callbacks.onExport && (
<>
<Divider />
<DMSubMenu label="Export" size="small">
<DMItem onClick={handleExportPNG}>PNG</DMItem>
<DMItem onClick={handleExportJPG}>JPG</DMItem>
<DMItem onClick={handleExportWEBP}>WEBP</DMItem>
<DMItem onClick={handleExportSVG}>SVG</DMItem>
<DMItem onClick={handleExportJSON}>JSON</DMItem>
</DMSubMenu>
</>
)}
{!disableAssets && (
<>
<Divider />

Wyświetl plik

@ -13,7 +13,6 @@ export const VERY_SLOW_SPEED = 2.5
export const GHOSTED_OPACITY = 0.3
export const DEAD_ZONE = 3
export const LABEL_POINT = [0.5, 0.5]
import type { Easing } from '~types'
export const PI2 = Math.PI * 2
@ -84,7 +83,7 @@ export const USER_COLORS = [
'#FF802B',
]
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
export const isSafari =
typeof Window === 'undefined' ? false : /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
export const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
export const VIDEO_EXTENSIONS = isSafari ? [] : ['.mp4', '.webm']

Wyświetl plik

@ -10,13 +10,11 @@ const CSS = `
export function useStylesheet() {
React.useLayoutEffect(() => {
if (styles.get(UID)) return
const style = document.createElement('style')
style.innerHTML = CSS
style.setAttribute('id', UID)
document.head.appendChild(style)
styles.set(UID, style)
return () => {
if (style && document.head.contains(style)) {
document.head.removeChild(style)

Wyświetl plik

@ -5,6 +5,5 @@ export const TldrawContext = React.createContext<TldrawApp>({} as TldrawApp)
export function useTldrawApp() {
const context = React.useContext(TldrawContext)
return context
}

Wyświetl plik

@ -37,6 +37,10 @@ import {
TDToolType,
TDAssetType,
TDAsset,
TDExportTypes,
TDAssets,
TDExport,
ImageShape,
} from '~types'
import {
migrate,
@ -53,7 +57,6 @@ import { shapeUtils } from '~state/shapes'
import { defaultStyle } from '~state/shapes/shared/shape-styles'
import * as Commands from './commands'
import { SessionArgsOfType, getSession, TldrawSession } from './sessions'
import type { BaseTool } from './tools/BaseTool'
import {
USER_COLORS,
FIT_TO_SCREEN_PADDING,
@ -62,6 +65,7 @@ import {
VIDEO_EXTENSIONS,
SVG_EXPORT_PADDING,
} from '~constants'
import type { BaseTool } from './tools/BaseTool'
import { SelectTool } from './tools/SelectTool'
import { EraseTool } from './tools/EraseTool'
import { TextTool } from './tools/TextTool'
@ -154,6 +158,10 @@ export interface TDCallbacks {
* (optional) A callback to run when an asset will be created. Should return the value for the image/video's `src` property.
*/
onAssetCreate?: (file: File, id: string) => Promise<string | false>
/**
* (optional) A callback to run when the user exports their page or selection.
*/
onExport?: (info: TDExport) => Promise<void>
}
export class TldrawApp extends StateManager<TDSnapshot> {
@ -438,6 +446,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
}
// Cleanup assets
if (!('assets' in next.document)) next.document.assets = {}
Object.keys(next.document.assets).forEach((id) => {
if (!next.document.assets[id]) {
delete next.document.assets[id]
@ -1689,6 +1699,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
const copyingAssets = copyingShapes
.map((shape) => {
if (!shape.assetId) return
return this.document.assets[shape.assetId]
})
.filter(Boolean) as TDAsset[]
@ -1878,9 +1889,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
const bounds = util.getBounds(shape)
const elm = util.getSvgElement(shape)
if (!elm) return
// If the element is an image, set the asset src as the xlinkhref
if (shape.type === TDShapeType.Image) {
elm.setAttribute('xlink:href', this.document.assets[shape.assetId].src)
} else if (shape.type === TDShapeType.Video) {
elm.setAttribute('xlink:href', this.serializeVideo(shape.id))
}
// Put the element in the correct position relative to the common bounds
elm.setAttribute(
@ -2049,29 +2063,22 @@ export class TldrawApp extends StateManager<TDSnapshot> {
* Zoom to fit the page's shapes.
*/
zoomToFit = (): this => {
const shapes = this.shapes
const {
shapes,
pageState: { camera },
} = this
if (shapes.length === 0) return this
const { rendererBounds } = this
const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getBounds))
let zoom = TLDR.getCameraZoom(
Math.min(
(rendererBounds.width - FIT_TO_SCREEN_PADDING) / commonBounds.width,
(rendererBounds.height - FIT_TO_SCREEN_PADDING) / commonBounds.height
)
)
zoom =
this.pageState.camera.zoom === zoom || this.pageState.camera.zoom < 1
? Math.min(1, zoom)
: zoom
zoom = camera.zoom === zoom || camera.zoom < 1 ? Math.min(1, zoom) : zoom
const mx = (rendererBounds.width - commonBounds.width * zoom) / 2 / zoom
const my = (rendererBounds.height - commonBounds.height * zoom) / 2 / zoom
return this.setCamera(
Vec.toFixed(Vec.sub([mx, my], [commonBounds.minX, commonBounds.minY])),
zoom,
@ -3388,6 +3395,100 @@ export class TldrawApp extends StateManager<TDSnapshot> {
return this.selectedIds.includes(id)
}
/* ----------------- Export ----------------- */
/**
* Get a snapshot of a video at current frame as base64 encoded image
* @param id ID of video shape
* @returns base64 encoded frame
* @throws Error if video shape with given ID does not exist
*/
serializeVideo(id: string): string {
const video = document.getElementById(id + '_video') as HTMLVideoElement
if (video) {
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const canvasContext = canvas.getContext('2d')!
canvasContext.drawImage(video, 0, 0)
return canvas.toDataURL('image/png')
} else throw new Error('Video with id ' + id + ' not found')
}
patchAssets(assets: TDAssets) {
this.document.assets = {
...this.document.assets,
...assets,
}
}
async exportAllShapesAs(type: TDExportTypes) {
const initialSelectedIds = [...this.selectedIds]
this.selectAll()
const { width, height } = Utils.expandBounds(TLDR.getSelectedBounds(this.state), 64)
const allIds = [...this.selectedIds]
this.setSelectedIds(initialSelectedIds)
await this.exportShapesAs(allIds, [width, height], type)
}
async exportSelectedShapesAs(type: TDExportTypes) {
const { width, height } = Utils.expandBounds(TLDR.getSelectedBounds(this.state), 64)
await this.exportShapesAs(this.selectedIds, [width, height], type)
}
async exportShapesAs(shapeIds: string[], size: number[], type: TDExportTypes) {
this.setIsLoading(true)
const assets: TDAssets = {}
let shapes = shapeIds.map((id) => ({ ...this.getShape(id) }))
// Patch asset table. Replace videos with serialized snapshots
shapes.forEach((s, i) => {
if (s.assetId) {
assets[s.assetId] = { ...this.document.assets[s.assetId] }
if (s.type === TDShapeType.Video) {
assets[s.assetId].src = this.serializeVideo(s.id)
assets[s.assetId].type = TDAssetType.Image
}
}
})
// Cast exported video shapes to image shapes to properly display snapshots
shapes = shapes.map((s) => {
if (s.type === TDShapeType.Video) {
const shape = s as TDShape
shape.type = TDShapeType.Image
return shape as ImageShape
} else return s
})
let serializedExport
if (type == TDExportTypes.SVG) {
serializedExport = this.copySvg(shapeIds)
} else if (type == TDExportTypes.JSON) {
serializedExport = this.copyJson(shapeIds)
}
const exportInfo: TDExport = {
name: this.page.name ?? 'export',
shapes: shapes,
assets: assets,
type,
size: type === 'png' ? Vec.mul(size, 2) : size,
serialized: serializedExport,
}
if (this.callbacks.onExport) {
try {
this.setIsLoading(true)
await this.callbacks.onExport?.(exportInfo)
} catch (error) {
console.error(error)
} finally {
this.setIsLoading(false)
}
}
}
get room() {
return this.state.room
}

Wyświetl plik

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import type { TDDocument, TDFile } from '~types'
import { fileSave, fileOpen, FileSystemHandle } from './browser-fs-access'
import type { FileSystemHandle } from './browser-fs-access'
import { get as getFromIdb, set as setToIdb } from 'idb-keyval'
import { IMAGE_EXTENSIONS, VIDEO_EXTENSIONS } from '~constants'
@ -46,6 +47,8 @@ export async function saveToFileSystem(document: TDDocument, fileHandle: FileSys
}
// Save to file system
// @ts-ignore
const fileSave = await import('./browser-fs-access').default.fileSave
const newFileHandle = await fileSave(
blob,
{
@ -67,6 +70,8 @@ export async function openFromFileSystem(): Promise<null | {
document: TDDocument
}> {
// Get the blob
// @ts-ignore
const fileOpen = await import('./browser-fs-access').fileOpen
const blob = await fileOpen({
description: 'Tldraw File',
extensions: [`.tldr`],
@ -100,6 +105,8 @@ export async function openFromFileSystem(): Promise<null | {
}
export async function openAssetFromFileSystem() {
// @ts-ignore
const fileOpen = await import('./browser-fs-access').fileOpen
return fileOpen({
description: 'Image or Video',
extensions: [...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS],

Wyświetl plik

@ -1,7 +1,5 @@
import { Vec } from '@tldraw/vec'
import {
SessionType,
ShapesWithProp,
TldrawCommand,
TldrawPatch,
TDStatus,

Wyświetl plik

@ -129,6 +129,7 @@ export class ImageUtil extends TDShapeUtil<T, E> {
const elm = document.createElementNS('http://www.w3.org/2000/svg', 'image')
elm.setAttribute('width', `${bounds.width}`)
elm.setAttribute('height', `${bounds.height}`)
elm.setAttribute('xmlns:xlink', `http://www.w3.org/1999/xlink`)
return elm
}
}

Wyświetl plik

@ -172,6 +172,15 @@ export class VideoUtil extends TDShapeUtil<T, E> {
return next.size !== prev.size || next.style !== prev.style || next.isPlaying !== prev.isPlaying
}
getSvgElement = (shape: VideoShape) => {
const bounds = this.getBounds(shape)
const elm = document.createElementNS('http://www.w3.org/2000/svg', 'image')
elm.setAttribute('width', `${bounds.width}`)
elm.setAttribute('height', `${bounds.height}`)
elm.setAttribute('xmlns:xlink', `http://www.w3.org/1999/xlink`)
return elm
}
transform = transformRectangle
transformSingle = transformSingleRectangle

Wyświetl plik

@ -20,6 +20,7 @@ import type {
TLShapeBlurHandler,
TLShapeCloneHandler,
TLAsset,
TLBounds,
} from '@tldraw/core'
/* -------------------------------------------------- */
@ -293,6 +294,7 @@ export enum Decoration {
export interface TDBaseShape extends TLShape {
style: ShapeStyles
type: TDShapeType
label?: string
handles?: Record<string, TldrawHandle>
}
@ -484,6 +486,28 @@ export type TDAsset = TDImageAsset | TDVideoAsset
export type TDAssets = Record<string, TDAsset>
/* -------------------------------------------------- */
/* Export */
/* -------------------------------------------------- */
export enum TDExportTypes {
PNG = 'png',
JPG = 'jpeg',
WEBP = 'webp',
PDF = 'pdf',
SVG = 'svg',
JSON = 'json',
}
export interface TDExport {
name: string
shapes: TDShape[]
assets: TDAssets
type: TDExportTypes
size: number[]
serialized?: string
}
/* -------------------------------------------------- */
/* Type Helpers */
/* -------------------------------------------------- */

167
yarn.lock
Wyświetl plik

@ -4493,21 +4493,6 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
aws-sdk@^2.1053.0:
version "2.1053.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1053.0.tgz#cd8263bde89177351e7ef49d9e6a89d4584c852c"
integrity sha512-bsVudymGczfn7kOsY9tiMFZUCNFOQi7iG3d1HiBFrnEDCKtVTyKuFrXy4iKUPCcjfOaqNnb1S3ZxN/A70MOTkg==
dependencies:
buffer "4.9.2"
events "1.1.1"
ieee754 "1.1.13"
jmespath "0.15.0"
querystring "0.2.0"
sax "1.2.1"
url "0.10.3"
uuid "3.3.2"
xml2js "0.4.19"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -4937,15 +4922,6 @@ buffer-xor@^1.0.3:
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
buffer@4.9.2:
version "4.9.2"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
isarray "^1.0.0"
buffer@5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
@ -6426,6 +6402,11 @@ detective-typescript-70@^7.0.0:
node-source-walk "^4.2.0"
typescript "^3.9.7"
devtools-protocol@0.0.937139:
version "0.0.937139"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.937139.tgz#bdee3751fdfdb81cb701fd3afa94b1065dafafcf"
integrity sha512-daj+rzR3QSxsPRy5vjjthn58axO8c11j58uY0lG5vvlJk/EiOdCWOptGdkXDjtuRHr78emKq0udHCXM4trhoDQ==
dezalgo@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
@ -7313,11 +7294,6 @@ eventemitter3@^4.0.0:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
events@3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
@ -7450,17 +7426,7 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-zip@^1.0.3:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
extract-zip@^2.0.1:
extract-zip@2.0.1, extract-zip@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
@ -7471,6 +7437,16 @@ extract-zip@^2.0.1:
optionalDependencies:
"@types/yauzl" "^2.9.1"
extract-zip@^1.0.3:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@ -8571,6 +8547,14 @@ https-browserify@1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
https-proxy-agent@^2.2.3:
version "2.2.4"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
@ -8579,14 +8563,6 @@ https-proxy-agent@^2.2.3:
agent-base "^4.3.0"
debug "^3.1.0"
https-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
human-signals@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
@ -8648,11 +8624,6 @@ idb@^6.1.4:
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.5.tgz#dbc53e7adf1ac7c59f9b2bf56e00b4ea4fce8c7b"
integrity sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw==
ieee754@1.1.13:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
ieee754@^1.1.13, ieee754@^1.1.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -9280,7 +9251,7 @@ isarray@0.0.1:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
isarray@1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
@ -9793,11 +9764,6 @@ jest@^27.3.1:
import-local "^3.0.2"
jest-cli "^27.4.7"
jmespath@0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
joi@^17.4.2:
version "17.5.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.5.0.tgz#7e66d0004b5045d971cf416a55fb61d33ac6e011"
@ -12089,6 +12055,13 @@ pirates@^4.0.4:
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.4.tgz#07df81e61028e402735cdd49db701e4885b4e6e6"
integrity sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw==
pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
dependencies:
find-up "^4.0.0"
pkg-dir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
@ -12096,13 +12069,6 @@ pkg-dir@^3.0.0:
dependencies:
find-up "^3.0.0"
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
dependencies:
find-up "^4.0.0"
platform@1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
@ -12300,7 +12266,7 @@ process@0.11.10, process@^0.11.10:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
progress@^2.0.0, progress@^2.0.3:
progress@2.0.3, progress@^2.0.0, progress@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@ -12392,7 +12358,7 @@ protoduck@^5.0.1:
dependencies:
genfun "^5.0.0"
proxy-from-env@^1.1.0:
proxy-from-env@1.1.0, proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
@ -12439,11 +12405,6 @@ pumpify@^1.3.3:
inherits "^2.0.3"
pump "^2.0.0"
punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@ -12456,6 +12417,24 @@ pupa@^2.1.1:
dependencies:
escape-goat "^2.0.0"
puppeteer@^13.0.1:
version "13.0.1"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-13.0.1.tgz#9cd9bb8ec090bade183ca186bf342396bdffa135"
integrity sha512-wqGIx59LzYqWhYcJQphMT+ux0sgatEUbjKG0lbjJxNVqVIT3ZC5m4Bvmq2gHE3qhb63EwS+rNkql08bm4BvO0A==
dependencies:
debug "4.3.2"
devtools-protocol "0.0.937139"
extract-zip "2.0.1"
https-proxy-agent "5.0.0"
node-fetch "2.6.5"
pkg-dir "4.2.0"
progress "2.0.3"
proxy-from-env "1.1.0"
rimraf "3.0.2"
tar-fs "2.1.1"
unbzip2-stream "1.4.3"
ws "8.2.3"
q@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@ -12488,11 +12467,6 @@ querystring-es3@0.2.1:
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
querystring@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@ -13256,11 +13230,6 @@ sass-lookup@^3.0.0:
dependencies:
commander "^2.16.0"
sax@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
sax@>=0.6.0, sax@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@ -14159,7 +14128,7 @@ tapable@^2.2.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
tar-fs@^2.0.0:
tar-fs@2.1.1, tar-fs@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
@ -14739,7 +14708,7 @@ unbox-primitive@^1.0.1:
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
unbzip2-stream@^1.0.9:
unbzip2-stream@1.4.3, unbzip2-stream@^1.0.9:
version "1.4.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
@ -14895,14 +14864,6 @@ url-parse-lax@^3.0.0:
dependencies:
prepend-http "^2.0.0"
url@0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=
dependencies:
punycode "1.3.2"
querystring "0.2.0"
use-callback-ref@^1.2.3:
version "1.2.5"
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
@ -14962,11 +14923,6 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^3.0.1, uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
@ -15533,6 +15489,11 @@ write-pkg@^3.1.0:
sort-keys "^2.0.0"
write-json-file "^2.2.0"
ws@8.2.3:
version "8.2.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
ws@>=7.4.6:
version "8.4.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.0.tgz#f05e982a0a88c604080e8581576e2a063802bed6"
@ -15553,14 +15514,6 @@ xml-name-validator@^3.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
xml2js@0.4.19:
version "0.4.19"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
dependencies:
sax ">=0.6.0"
xmlbuilder "~9.0.1"
xml2js@^0.4.23:
version "0.4.23"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
@ -15574,7 +15527,7 @@ xmlbuilder@>=11.0.1:
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"
integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==
xmlbuilder@^9.0.7, xmlbuilder@~9.0.1:
xmlbuilder@^9.0.7:
version "9.0.7"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=