Add Image and Video shapes (#460)
* Added image and video shapes * Fixed bugs; Added optional onImageUpload callback * Added id field to onImageUpload * Added onImageDelete callback for cleanup * Added firebase storage to multiplayer for media * Added firebase storage to multiplayer for media * Silence unnecessary TS errors * Fixed bugs; Added tests * Added tests * Disable images for multiplayer example * switch to assets in document, rather than on shapes, fix resize, fix sizes * bump version, add migration for assets table * Rename onImageUpload * Add isPlaying state to video (not complete) * Revert "Add isPlaying state to video (not complete)" This reverts commit 3dc2ba703f4194eb7c47524d384dc8392daa18be. * Adds controls when editing video, sync current time when cloning * Remove unused tools * avoid duplication in assets * Remove unused image styles from style menu * Fix placement of clone buttons * Fix flag to hide image assets in multiplayer * move getSizeFromDataUrl to filesystem * Update VideoUtil.tsx * Re-center video after it loads * Add copy and paste support for assets * Fix bug in state manager, remove unused assets on load, fix indicators * Add multiplayer with images example * Update MultiplayerEditor.tsx * Add images to copy SVG * tighten up some code around svg export * Update TldrawApp.spec.ts * Update useBoundsHandleEvents.tsx * Reset image size by double clicking bounds * fix reset size Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
2f84abcc1e
commit
1c65c031b2
53 changed files with 2044 additions and 260 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -16,3 +16,4 @@ apps/www/public/worker-*
|
||||||
apps/www/public/sw.js
|
apps/www/public/sw.js
|
||||||
apps/www/public/sw.js.map
|
apps/www/public/sw.js.map
|
||||||
.env
|
.env
|
||||||
|
firebase.config.*
|
|
@ -51,6 +51,7 @@ function Editor({
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<Tldraw
|
<Tldraw
|
||||||
autofocus
|
autofocus
|
||||||
|
disableAssets
|
||||||
showPages={false}
|
showPages={false}
|
||||||
showSponsorLink={!isSponsor}
|
showSponsorLink={!isSponsor}
|
||||||
onSignIn={isSponsor ? undefined : onSignIn}
|
onSignIn={isSponsor ? undefined : onSignIn}
|
||||||
|
|
|
@ -24,10 +24,10 @@
|
||||||
"@types/react-router-dom": "^5.1.8",
|
"@types/react-router-dom": "^5.1.8",
|
||||||
"concurrently": "6.0.1",
|
"concurrently": "6.0.1",
|
||||||
"create-serve": "1.0.1",
|
"create-serve": "1.0.1",
|
||||||
"dotenv": "^10.0.0",
|
|
||||||
"esbuild": "^0.13.8",
|
"esbuild": "^0.13.8",
|
||||||
"esbuild-envfile-plugin": "^1.0.1",
|
"esbuild-envfile-plugin": "^1.0.1",
|
||||||
"esbuild-serve": "^1.0.1",
|
"esbuild-serve": "^1.0.1",
|
||||||
|
"firebase": "^9.6.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router": "^5.2.1",
|
"react-router": "^5.2.1",
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default function Develop(): JSX.Element {
|
||||||
onSignIn={handleSignIn}
|
onSignIn={handleSignIn}
|
||||||
onSignOut={handleSignOut}
|
onSignOut={handleSignOut}
|
||||||
onPersist={handlePersist}
|
onPersist={handlePersist}
|
||||||
showSponsorLink={true}
|
showSponsorLink={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './multiplayer'
|
|
@ -0,0 +1,60 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import * as React from 'react'
|
||||||
|
import { TDShape, Tldraw } from '@tldraw/tldraw'
|
||||||
|
import { createClient } from '@liveblocks/client'
|
||||||
|
import { LiveblocksProvider, RoomProvider } from '@liveblocks/react'
|
||||||
|
import { useMultiplayerState } from './useMultiplayerState'
|
||||||
|
// import { initializeApp } from 'firebase/app'
|
||||||
|
// import firebaseConfig from '../firebase.config'
|
||||||
|
// import { useMemo } from 'react'
|
||||||
|
// import { getStorage, ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage'
|
||||||
|
|
||||||
|
const client = createClient({
|
||||||
|
publicApiKey: process.env.LIVEBLOCKS_PUBLIC_API_KEY || '',
|
||||||
|
throttle: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
const roomId = 'mp-test-8'
|
||||||
|
|
||||||
|
export function Multiplayer() {
|
||||||
|
return (
|
||||||
|
<LiveblocksProvider client={client}>
|
||||||
|
<RoomProvider id={roomId}>
|
||||||
|
<Editor roomId={roomId} />
|
||||||
|
</RoomProvider>
|
||||||
|
</LiveblocksProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Editor({ roomId }: { roomId: string }) {
|
||||||
|
const { error, ...events } = useMultiplayerState(roomId)
|
||||||
|
// const app = useMemo(() => initializeApp(firebaseConfig), [firebaseConfig])
|
||||||
|
// const storage = useMemo(() => getStorage(app, firebaseConfig.storageBucket), [])
|
||||||
|
|
||||||
|
if (error) return <div>Error: {error.message}</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tldraw">
|
||||||
|
<Tldraw
|
||||||
|
showPages={false}
|
||||||
|
{...events}
|
||||||
|
/**
|
||||||
|
* Warning: Keeping images enabled for multiplayer applications
|
||||||
|
* without provifing a storage bucket based solution will cause
|
||||||
|
* massive base64 string to be written to the liveblocks room.
|
||||||
|
*/
|
||||||
|
disableAssets={true}
|
||||||
|
// onImageCreate={async (file: File, id: string) => {
|
||||||
|
// const imageRef = ref(storage, id)
|
||||||
|
// const snapshot = await uploadBytes(imageRef, file)
|
||||||
|
// const url = await getDownloadURL(snapshot.ref)
|
||||||
|
// return url
|
||||||
|
// }}
|
||||||
|
// onImageDelete={async (id: string) => {
|
||||||
|
// const imageRef = ref(storage, id)
|
||||||
|
// await deleteObject(imageRef)
|
||||||
|
// }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,211 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import * as React from 'react'
|
||||||
|
import type { TldrawApp, TDUser, TDShape, TDBinding, TDDocument } from '@tldraw/tldraw'
|
||||||
|
import { useRedo, useUndo, useRoom, useUpdateMyPresence } from '@liveblocks/react'
|
||||||
|
import { LiveMap, LiveObject } from '@liveblocks/client'
|
||||||
|
|
||||||
|
declare const window: Window & { app: TldrawApp }
|
||||||
|
|
||||||
|
export function useMultiplayerState(roomId: string) {
|
||||||
|
const [app, setApp] = React.useState<TldrawApp>()
|
||||||
|
const [error, setError] = React.useState<Error>()
|
||||||
|
const [loading, setLoading] = React.useState(true)
|
||||||
|
|
||||||
|
const room = useRoom()
|
||||||
|
const onUndo = useUndo()
|
||||||
|
const onRedo = useRedo()
|
||||||
|
const updateMyPresence = useUpdateMyPresence()
|
||||||
|
|
||||||
|
const rLiveShapes = React.useRef<LiveMap<string, TDShape>>()
|
||||||
|
const rLiveBindings = React.useRef<LiveMap<string, TDBinding>>()
|
||||||
|
|
||||||
|
// Callbacks --------------
|
||||||
|
|
||||||
|
// Put the state into the window, for debugging.
|
||||||
|
const onMount = React.useCallback(
|
||||||
|
(app: TldrawApp) => {
|
||||||
|
app.loadRoom(roomId)
|
||||||
|
app.pause() // Turn off the app's own undo / redo stack
|
||||||
|
window.app = app
|
||||||
|
setApp(app)
|
||||||
|
},
|
||||||
|
[roomId]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update the live shapes when the app's shapes change.
|
||||||
|
const onChangePage = React.useCallback(
|
||||||
|
(
|
||||||
|
app: TldrawApp,
|
||||||
|
shapes: Record<string, TDShape | undefined>,
|
||||||
|
bindings: Record<string, TDBinding | undefined>
|
||||||
|
) => {
|
||||||
|
room.batch(() => {
|
||||||
|
const lShapes = rLiveShapes.current
|
||||||
|
const lBindings = rLiveBindings.current
|
||||||
|
|
||||||
|
if (!(lShapes && lBindings)) return
|
||||||
|
|
||||||
|
Object.entries(shapes).forEach(([id, shape]) => {
|
||||||
|
if (!shape) {
|
||||||
|
lShapes.delete(id)
|
||||||
|
} else {
|
||||||
|
lShapes.set(shape.id, shape)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.entries(bindings).forEach(([id, binding]) => {
|
||||||
|
if (!binding) {
|
||||||
|
lBindings.delete(id)
|
||||||
|
} else {
|
||||||
|
lBindings.set(binding.id, binding)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[room]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle presence updates when the user's pointer / selection changes
|
||||||
|
const onChangePresence = React.useCallback(
|
||||||
|
(app: TldrawApp, user: TDUser) => {
|
||||||
|
updateMyPresence({ id: app.room?.userId, user })
|
||||||
|
},
|
||||||
|
[updateMyPresence]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Document Changes --------
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const unsubs: (() => void)[] = []
|
||||||
|
if (!(app && room)) return
|
||||||
|
// Handle errors
|
||||||
|
unsubs.push(room.subscribe('error', (error) => setError(error)))
|
||||||
|
|
||||||
|
// Handle changes to other users' presence
|
||||||
|
unsubs.push(
|
||||||
|
room.subscribe('others', (others) => {
|
||||||
|
app.updateUsers(
|
||||||
|
others
|
||||||
|
.toArray()
|
||||||
|
.filter((other) => other.presence)
|
||||||
|
.map((other) => other.presence!.user)
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle events from the room
|
||||||
|
unsubs.push(
|
||||||
|
room.subscribe(
|
||||||
|
'event',
|
||||||
|
(e: { connectionId: number; event: { name: string; userId: string } }) => {
|
||||||
|
switch (e.event.name) {
|
||||||
|
case 'exit': {
|
||||||
|
app?.removeUser(e.event.userId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send the exit event when the tab closes
|
||||||
|
function handleExit() {
|
||||||
|
if (!(room && app?.room)) return
|
||||||
|
room?.broadcastEvent({ name: 'exit', userId: app.room.userId })
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', handleExit)
|
||||||
|
unsubs.push(() => window.removeEventListener('beforeunload', handleExit))
|
||||||
|
|
||||||
|
let stillAlive = true
|
||||||
|
|
||||||
|
// Setup the document's storage and subscriptions
|
||||||
|
async function setupDocument() {
|
||||||
|
const storage = await room.getStorage<any>()
|
||||||
|
|
||||||
|
// Initialize (get or create) shapes and bindings maps
|
||||||
|
|
||||||
|
let lShapes: LiveMap<string, TDShape> = storage.root.get('shapes')
|
||||||
|
if (!lShapes) {
|
||||||
|
storage.root.set('shapes', new LiveMap<string, TDShape>())
|
||||||
|
lShapes = storage.root.get('shapes')
|
||||||
|
}
|
||||||
|
rLiveShapes.current = lShapes
|
||||||
|
|
||||||
|
let lBindings: LiveMap<string, TDBinding> = storage.root.get('bindings')
|
||||||
|
if (!lBindings) {
|
||||||
|
storage.root.set('bindings', new LiveMap<string, TDBinding>())
|
||||||
|
lBindings = storage.root.get('bindings')
|
||||||
|
}
|
||||||
|
rLiveBindings.current = lBindings
|
||||||
|
|
||||||
|
// Migrate previous versions
|
||||||
|
const version = storage.root.get('version')
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
// The doc object will only be present if the document was created
|
||||||
|
// prior to the current multiplayer implementation. At this time, the
|
||||||
|
// document was a single LiveObject named 'doc'. If we find a doc,
|
||||||
|
// then we need to move the shapes and bindings over to the new structures
|
||||||
|
// and then mark the doc as migrated.
|
||||||
|
const doc = storage.root.get('doc') as LiveObject<{
|
||||||
|
uuid: string
|
||||||
|
document: TDDocument
|
||||||
|
migrated?: boolean
|
||||||
|
}>
|
||||||
|
|
||||||
|
// No doc? No problem. This was likely a newer document
|
||||||
|
if (doc) {
|
||||||
|
const {
|
||||||
|
document: {
|
||||||
|
pages: {
|
||||||
|
page: { shapes, bindings },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = doc.toObject()
|
||||||
|
|
||||||
|
Object.values(shapes).forEach((shape) => lShapes.set(shape.id, shape))
|
||||||
|
Object.values(bindings).forEach((binding) => lBindings.set(binding.id, binding))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the version number for future migrations
|
||||||
|
storage.root.set('version', 2)
|
||||||
|
|
||||||
|
// Subscribe to changes
|
||||||
|
const handleChanges = () => {
|
||||||
|
app?.replacePageContent(
|
||||||
|
Object.fromEntries(lShapes.entries()),
|
||||||
|
Object.fromEntries(lBindings.entries())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stillAlive) {
|
||||||
|
unsubs.push(room.subscribe(lShapes, handleChanges))
|
||||||
|
|
||||||
|
// Update the document with initial content
|
||||||
|
handleChanges()
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDocument()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stillAlive = false
|
||||||
|
unsubs.forEach((unsub) => unsub())
|
||||||
|
}
|
||||||
|
}, [app])
|
||||||
|
|
||||||
|
return {
|
||||||
|
onUndo,
|
||||||
|
onRedo,
|
||||||
|
onMount,
|
||||||
|
onChangePage,
|
||||||
|
onChangePresence,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,12 +24,11 @@ export function Multiplayer() {
|
||||||
|
|
||||||
function Editor({ roomId }: { roomId: string }) {
|
function Editor({ roomId }: { roomId: string }) {
|
||||||
const { error, ...events } = useMultiplayerState(roomId)
|
const { error, ...events } = useMultiplayerState(roomId)
|
||||||
|
|
||||||
if (error) return <div>Error: {error.message}</div>
|
if (error) return <div>Error: {error.message}</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw">
|
<div className="tldraw">
|
||||||
<Tldraw showPages={false} {...events} />
|
<Tldraw showPages={false} {...events} disableAssets={true} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import * as React from 'react'
|
||||||
import type { TldrawApp, TDUser, TDShape, TDBinding, TDDocument } from '@tldraw/tldraw'
|
import type { TldrawApp, TDUser, TDShape, TDBinding, TDDocument } from '@tldraw/tldraw'
|
||||||
import { useRedo, useUndo, useRoom, useUpdateMyPresence } from '@liveblocks/react'
|
import { useRedo, useUndo, useRoom, useUpdateMyPresence } from '@liveblocks/react'
|
||||||
import { LiveMap, LiveObject } from '@liveblocks/client'
|
import { LiveMap, LiveObject } from '@liveblocks/client'
|
||||||
import { Utils } from '@tldraw/core'
|
|
||||||
|
|
||||||
declare const window: Window & { app: TldrawApp }
|
declare const window: Window & { app: TldrawApp }
|
||||||
|
|
||||||
|
@ -188,7 +187,6 @@ export function useMultiplayerState(roomId: string) {
|
||||||
|
|
||||||
// Update the document with initial content
|
// Update the document with initial content
|
||||||
handleChanges()
|
handleChanges()
|
||||||
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const CenterHandle = observer<CenterHandleProps>(function CenterHandle({
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<rect
|
<rect
|
||||||
className={isLocked ? 'tl-bounds-center tl-dashed' : 'tl-bounds-center'}
|
className={['tl-bounds-center', isLocked ? 'tl-dashed' : ''].join(' ')}
|
||||||
x={-1}
|
x={-1}
|
||||||
y={-1}
|
y={-1}
|
||||||
width={bounds.width + 2}
|
width={bounds.width + 2}
|
||||||
|
|
|
@ -27,26 +27,27 @@ export const CloneButton = observer<CloneButtonProps>(function CloneButton({
|
||||||
targetSize,
|
targetSize,
|
||||||
size,
|
size,
|
||||||
}: CloneButtonProps) {
|
}: CloneButtonProps) {
|
||||||
|
const s = targetSize * 2
|
||||||
const x = {
|
const x = {
|
||||||
left: -44,
|
left: -s,
|
||||||
topLeft: -44,
|
topLeft: -s,
|
||||||
bottomLeft: -44,
|
bottomLeft: -s,
|
||||||
right: bounds.width + 44,
|
right: bounds.width,
|
||||||
topRight: bounds.width + 44,
|
topRight: bounds.width,
|
||||||
bottomRight: bounds.width + 44,
|
bottomRight: bounds.width,
|
||||||
top: bounds.width / 2,
|
top: bounds.width / 2 - s / 2,
|
||||||
bottom: bounds.width / 2,
|
bottom: bounds.width / 2 - s / 2,
|
||||||
}[side]
|
}[side]
|
||||||
|
|
||||||
const y = {
|
const y = {
|
||||||
left: bounds.height / 2,
|
left: bounds.height / 2 - s / 2,
|
||||||
right: bounds.height / 2,
|
right: bounds.height / 2 - s / 2,
|
||||||
top: -44,
|
top: -s * 2,
|
||||||
topLeft: -44,
|
topLeft: -s,
|
||||||
topRight: -44,
|
topRight: -s,
|
||||||
bottom: bounds.height + 44,
|
bottom: bounds.height,
|
||||||
bottomLeft: bounds.height + 44,
|
bottomLeft: bounds.height,
|
||||||
bottomRight: bounds.height + 44,
|
bottomRight: bounds.height,
|
||||||
}[side]
|
}[side]
|
||||||
|
|
||||||
const { callbacks, inputs } = useTLContext()
|
const { callbacks, inputs } = useTLContext()
|
||||||
|
@ -62,17 +63,11 @@ export const CloneButton = observer<CloneButtonProps>(function CloneButton({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g className="tl-clone-target" transform={`translate(${x}, ${y})`} aria-label="clone button">
|
<g className="tl-clone-target" transform={`translate(${x}, ${y})`} aria-label="clone button">
|
||||||
<rect
|
<rect className="tl-transparent" width={targetSize * 2} height={targetSize * 2} />
|
||||||
className="tl-transparent"
|
|
||||||
width={targetSize * 4}
|
|
||||||
height={targetSize * 4}
|
|
||||||
x={-targetSize * 2}
|
|
||||||
y={-targetSize * 2}
|
|
||||||
/>
|
|
||||||
<g
|
<g
|
||||||
className="tl-clone-button-target"
|
className="tl-clone-button-target"
|
||||||
onPointerDown={handleClick}
|
onPointerDown={handleClick}
|
||||||
transform={`rotate(${ROTATIONS[side]})`}
|
transform={`translate(${targetSize}, ${targetSize}) rotate(${ROTATIONS[side]})`}
|
||||||
>
|
>
|
||||||
<circle className="tl-transparent " r={targetSize} />
|
<circle className="tl-transparent " r={targetSize} />
|
||||||
<path
|
<path
|
||||||
|
|
|
@ -28,17 +28,18 @@ describe('CloneButton', () => {
|
||||||
|
|
||||||
const cloneBtn = screen.getByLabelText('clone button')
|
const cloneBtn = screen.getByLabelText('clone button')
|
||||||
|
|
||||||
expect(cloneBtn).toHaveAttribute('transform', 'translate(50, -44)')
|
expect(cloneBtn).toHaveAttribute('transform', 'translate(30, -80)')
|
||||||
|
|
||||||
// transparent rect
|
// transparent rect
|
||||||
const rect = cloneBtn.querySelector('rect')
|
const rect = cloneBtn.querySelector('rect')
|
||||||
|
|
||||||
expect(rect).toHaveAttribute('height', '80')
|
expect(rect).toHaveAttribute('height', '40')
|
||||||
expect(rect).toHaveAttribute('width', '80')
|
expect(rect).toHaveAttribute('width', '40')
|
||||||
expect(rect).toHaveAttribute('x', '-40')
|
|
||||||
expect(rect).toHaveAttribute('y', '-40')
|
|
||||||
|
|
||||||
expect(cloneBtn.querySelector('g')).toHaveAttribute('transform', 'rotate(270)')
|
expect(cloneBtn.querySelector('g')).toHaveAttribute(
|
||||||
|
'transform',
|
||||||
|
'translate(20, 20) rotate(270)'
|
||||||
|
)
|
||||||
expect(cloneBtn.querySelector('circle')).toHaveAttribute('r', '20')
|
expect(cloneBtn.querySelector('circle')).toHaveAttribute('r', '20')
|
||||||
expect(cloneBtn.querySelector('path')).toHaveAttribute('d', 'M -5,-5 L 5,0 -5,5 Z')
|
expect(cloneBtn.querySelector('path')).toHaveAttribute('d', 'M -5,-5 L 5,0 -5,5 Z')
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,6 +19,7 @@ describe('page', () => {
|
||||||
onBoundsChange={() => {
|
onBoundsChange={() => {
|
||||||
// noop
|
// noop
|
||||||
}}
|
}}
|
||||||
|
assets={{}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,7 +9,16 @@ import {
|
||||||
useCameraCss,
|
useCameraCss,
|
||||||
useKeyEvents,
|
useKeyEvents,
|
||||||
} from '~hooks'
|
} from '~hooks'
|
||||||
import type { TLBinding, TLBounds, TLPage, TLPageState, TLShape, TLSnapLine, TLUsers } from '~types'
|
import type {
|
||||||
|
TLAssets,
|
||||||
|
TLBinding,
|
||||||
|
TLBounds,
|
||||||
|
TLPage,
|
||||||
|
TLPageState,
|
||||||
|
TLShape,
|
||||||
|
TLSnapLine,
|
||||||
|
TLUsers,
|
||||||
|
} from '~types'
|
||||||
import { Brush } from '~components/Brush'
|
import { Brush } from '~components/Brush'
|
||||||
import { Page } from '~components/Page'
|
import { Page } from '~components/Page'
|
||||||
import { Users } from '~components/Users'
|
import { Users } from '~components/Users'
|
||||||
|
@ -20,13 +29,10 @@ import { SnapLines } from '~components/SnapLines/SnapLines'
|
||||||
import { Grid } from '~components/Grid'
|
import { Grid } from '~components/Grid'
|
||||||
import { Overlay } from '~components/Overlay'
|
import { Overlay } from '~components/Overlay'
|
||||||
|
|
||||||
function resetError() {
|
|
||||||
void null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
page: TLPage<T, TLBinding>
|
page: TLPage<T, TLBinding>
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
|
assets: TLAssets
|
||||||
snapLines?: TLSnapLine[]
|
snapLines?: TLSnapLine[]
|
||||||
grid?: number
|
grid?: number
|
||||||
users?: TLUsers<T>
|
users?: TLUsers<T>
|
||||||
|
@ -52,6 +58,7 @@ export const Canvas = observer(function _Canvas<
|
||||||
id,
|
id,
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
|
assets,
|
||||||
snapLines,
|
snapLines,
|
||||||
grid,
|
grid,
|
||||||
users,
|
users,
|
||||||
|
@ -96,6 +103,7 @@ export const Canvas = observer(function _Canvas<
|
||||||
<Page
|
<Page
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
|
assets={assets}
|
||||||
hideBounds={hideBounds}
|
hideBounds={hideBounds}
|
||||||
hideIndicators={hideIndicators}
|
hideIndicators={hideIndicators}
|
||||||
hideHandles={hideHandles}
|
hideHandles={hideHandles}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const HTMLContainer = React.forwardRef<HTMLDivElement, HTMLContainerProps
|
||||||
<Observer>
|
<Observer>
|
||||||
{() => (
|
{() => (
|
||||||
<div ref={ref} className={`tl-positioned-div ${className}`} {...rest}>
|
<div ref={ref} className={`tl-positioned-div ${className}`} {...rest}>
|
||||||
{children}
|
<div className="tl-inner-div">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Observer>
|
</Observer>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { TLBinding, TLPage, TLPageState, TLShape } from '~types'
|
import type { TLAssets, TLBinding, TLPage, TLPageState, TLShape } from '~types'
|
||||||
import { useSelection, useShapeTree, useTLContext } from '~hooks'
|
import { useSelection, useShapeTree, useTLContext } from '~hooks'
|
||||||
import { Bounds } from '~components/Bounds'
|
import { Bounds } from '~components/Bounds'
|
||||||
import { BoundsBg } from '~components/Bounds/BoundsBg'
|
import { BoundsBg } from '~components/Bounds/BoundsBg'
|
||||||
|
@ -13,6 +13,7 @@ import type { TLShapeUtil } from '~TLShapeUtil'
|
||||||
interface PageProps<T extends TLShape, M extends Record<string, unknown>> {
|
interface PageProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
page: TLPage<T, TLBinding>
|
page: TLPage<T, TLBinding>
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
|
assets: TLAssets
|
||||||
hideBounds: boolean
|
hideBounds: boolean
|
||||||
hideHandles: boolean
|
hideHandles: boolean
|
||||||
hideIndicators: boolean
|
hideIndicators: boolean
|
||||||
|
@ -29,6 +30,7 @@ interface PageProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||||
export const Page = observer(function _Page<T extends TLShape, M extends Record<string, unknown>>({
|
export const Page = observer(function _Page<T extends TLShape, M extends Record<string, unknown>>({
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
|
assets,
|
||||||
hideBounds,
|
hideBounds,
|
||||||
hideHandles,
|
hideHandles,
|
||||||
hideIndicators,
|
hideIndicators,
|
||||||
|
@ -40,30 +42,29 @@ export const Page = observer(function _Page<T extends TLShape, M extends Record<
|
||||||
}: PageProps<T, M>): JSX.Element {
|
}: PageProps<T, M>): JSX.Element {
|
||||||
const { bounds: rendererBounds, shapeUtils } = useTLContext()
|
const { bounds: rendererBounds, shapeUtils } = useTLContext()
|
||||||
|
|
||||||
const shapeTree = useShapeTree(page, pageState, meta)
|
const shapeTree = useShapeTree(page, pageState, assets, meta)
|
||||||
|
|
||||||
const { bounds, isLinked, isLocked, rotation } = useSelection(page, pageState, shapeUtils)
|
const { bounds, isLinked, isLocked, rotation } = useSelection(page, pageState, shapeUtils)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedIds,
|
selectedIds,
|
||||||
hoveredId,
|
hoveredId,
|
||||||
|
editingId,
|
||||||
camera: { zoom },
|
camera: { zoom },
|
||||||
} = pageState
|
} = pageState
|
||||||
|
|
||||||
let _hideCloneHandles = true
|
let _hideCloneHandles = true
|
||||||
|
let _isEditing = false
|
||||||
|
|
||||||
// Does the selected shape have handles?
|
// Does the selected shape have handles?
|
||||||
let shapeWithHandles: TLShape | undefined = undefined
|
let shapeWithHandles: TLShape | undefined = undefined
|
||||||
|
|
||||||
const selectedShapes = selectedIds.map((id) => page.shapes[id])
|
const selectedShapes = selectedIds.map((id) => page.shapes[id])
|
||||||
|
|
||||||
if (selectedShapes.length === 1) {
|
if (selectedShapes.length === 1) {
|
||||||
const shape = selectedShapes[0]
|
const shape = selectedShapes[0]
|
||||||
|
_isEditing = editingId === shape.id
|
||||||
const utils = shapeUtils[shape.type] as TLShapeUtil<any, any>
|
const utils = shapeUtils[shape.type] as TLShapeUtil<any, any>
|
||||||
|
|
||||||
_hideCloneHandles = hideCloneHandles || !utils.showCloneHandles
|
_hideCloneHandles = hideCloneHandles || !utils.showCloneHandles
|
||||||
|
|
||||||
if (shape.handles !== undefined) {
|
if (shape.handles !== undefined) {
|
||||||
shapeWithHandles = shape
|
shapeWithHandles = shape
|
||||||
}
|
}
|
||||||
|
@ -82,9 +83,10 @@ export const Page = observer(function _Page<T extends TLShape, M extends Record<
|
||||||
shape={shape}
|
shape={shape}
|
||||||
meta={meta as any}
|
meta={meta as any}
|
||||||
isSelected
|
isSelected
|
||||||
|
isEditing={_isEditing}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!hideIndicators && hoveredId && (
|
{!hideIndicators && hoveredId && hoveredId !== editingId && (
|
||||||
<ShapeIndicator
|
<ShapeIndicator
|
||||||
key={'hovered_' + hoveredId}
|
key={'hovered_' + hoveredId}
|
||||||
shape={page.shapes[hoveredId]}
|
shape={page.shapes[hoveredId]}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
TLBinding,
|
TLBinding,
|
||||||
TLSnapLine,
|
TLSnapLine,
|
||||||
TLUsers,
|
TLUsers,
|
||||||
|
TLAssets,
|
||||||
} from '../../types'
|
} from '../../types'
|
||||||
import { Canvas } from '../Canvas'
|
import { Canvas } from '../Canvas'
|
||||||
import { Inputs } from '../../inputs'
|
import { Inputs } from '../../inputs'
|
||||||
|
@ -30,6 +31,10 @@ export interface RendererProps<T extends TLShape, M = any> extends Partial<TLCal
|
||||||
* The current page state.
|
* The current page state.
|
||||||
*/
|
*/
|
||||||
pageState: TLPageState
|
pageState: TLPageState
|
||||||
|
/**
|
||||||
|
* A map of assets to be used in the renderer.
|
||||||
|
*/
|
||||||
|
assets: TLAssets
|
||||||
/**
|
/**
|
||||||
* (optional) A unique id to be applied to the renderer element, used to scope styles.
|
* (optional) A unique id to be applied to the renderer element, used to scope styles.
|
||||||
*/
|
*/
|
||||||
|
@ -121,6 +126,7 @@ export const Renderer = observer(function _Renderer<
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
page,
|
page,
|
||||||
pageState,
|
pageState,
|
||||||
|
assets,
|
||||||
users,
|
users,
|
||||||
userId,
|
userId,
|
||||||
theme,
|
theme,
|
||||||
|
@ -177,6 +183,7 @@ export const Renderer = observer(function _Renderer<
|
||||||
id={id}
|
id={id}
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
|
assets={assets}
|
||||||
snapLines={snapLines}
|
snapLines={snapLines}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
users={users}
|
users={users}
|
||||||
|
|
|
@ -6,7 +6,13 @@ import { boxShape } from '~TLShapeUtil/TLShapeUtil.spec'
|
||||||
describe('shape indicator', () => {
|
describe('shape indicator', () => {
|
||||||
test('mounts component without crashing', () => {
|
test('mounts component without crashing', () => {
|
||||||
renderWithSvg(
|
renderWithSvg(
|
||||||
<ShapeIndicator shape={boxShape} isSelected={true} isHovered={false} meta={undefined} />
|
<ShapeIndicator
|
||||||
|
shape={boxShape}
|
||||||
|
isSelected={true}
|
||||||
|
isHovered={false}
|
||||||
|
isEditing={false}
|
||||||
|
meta={undefined}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,12 +8,14 @@ interface IndicatorProps<T extends TLShape, M = unknown> {
|
||||||
meta: M extends unknown ? M : undefined
|
meta: M extends unknown ? M : undefined
|
||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
isHovered?: boolean
|
isHovered?: boolean
|
||||||
|
isEditing?: boolean
|
||||||
user?: TLUser<T>
|
user?: TLUser<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShapeIndicator = observer(function ShapeIndicator<T extends TLShape, M>({
|
export const ShapeIndicator = observer(function ShapeIndicator<T extends TLShape, M>({
|
||||||
isHovered = false,
|
isHovered = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
|
isEditing = false,
|
||||||
shape,
|
shape,
|
||||||
user,
|
user,
|
||||||
meta,
|
meta,
|
||||||
|
@ -26,9 +28,12 @@ export const ShapeIndicator = observer(function ShapeIndicator<T extends TLShape
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={rPositioned}
|
ref={rPositioned}
|
||||||
className={
|
className={[
|
||||||
'tl-indicator tl-absolute ' + (user ? '' : isSelected ? 'tl-selected' : 'tl-hovered')
|
'tl-indicator',
|
||||||
}
|
'tl-absolute',
|
||||||
|
isSelected && !user ? 'tl-selected' : 'tl-hovered',
|
||||||
|
isEditing ? 'tl-editing' : '',
|
||||||
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<svg width="100%" height="100%">
|
<svg width="100%" height="100%">
|
||||||
<g className="tl-centered-g" stroke={user?.color}>
|
<g className="tl-centered-g" stroke={user?.color}>
|
||||||
|
|
|
@ -11,10 +11,10 @@ export function useBoundsHandleEvents(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (e.button !== 0) return
|
if (e.button !== 0) return
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
e.stopPropagation()
|
|
||||||
e.currentTarget?.setPointerCapture(e.pointerId)
|
|
||||||
const info = inputs.pointerDown(e, id)
|
const info = inputs.pointerDown(e, id)
|
||||||
|
if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) {
|
||||||
|
callbacks.onDoubleClickBoundsHandle?.(info, e)
|
||||||
|
}
|
||||||
callbacks.onPointBoundsHandle?.(info, e)
|
callbacks.onPointBoundsHandle?.(info, e)
|
||||||
callbacks.onPointerDown?.(info, e)
|
callbacks.onPointerDown?.(info, e)
|
||||||
},
|
},
|
||||||
|
@ -25,18 +25,7 @@ export function useBoundsHandleEvents(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (e.button !== 0) return
|
if (e.button !== 0) return
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
e.stopPropagation()
|
|
||||||
const isDoubleClick = inputs.isDoubleClick()
|
|
||||||
const info = inputs.pointerUp(e, id)
|
const info = inputs.pointerUp(e, id)
|
||||||
|
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
||||||
e.currentTarget?.releasePointerCapture(e.pointerId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDoubleClick && !(info.altKey || info.metaKey)) {
|
|
||||||
callbacks.onDoubleClickBoundsHandle?.(info, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
callbacks.onReleaseBoundsHandle?.(info, e)
|
callbacks.onReleaseBoundsHandle?.(info, e)
|
||||||
callbacks.onPointerUp?.(info, e)
|
callbacks.onPointerUp?.(info, e)
|
||||||
},
|
},
|
||||||
|
@ -47,7 +36,6 @@ export function useBoundsHandleEvents(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (!inputs.pointerIsValid(e)) return
|
if (!inputs.pointerIsValid(e)) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||||
callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e)
|
callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,5 +61,7 @@ export function useCanvasEvents() {
|
||||||
onPointerDown,
|
onPointerDown,
|
||||||
onPointerMove,
|
onPointerMove,
|
||||||
onPointerUp,
|
onPointerUp,
|
||||||
|
onDrop: callbacks.onDrop,
|
||||||
|
onDragOver: callbacks.onDragOver,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import type { IShapeTreeNode, TLPage, TLPageState, TLShape, TLBinding, TLBounds } from '~types'
|
import type {
|
||||||
|
IShapeTreeNode,
|
||||||
|
TLPage,
|
||||||
|
TLPageState,
|
||||||
|
TLShape,
|
||||||
|
TLBinding,
|
||||||
|
TLBounds,
|
||||||
|
TLAssets,
|
||||||
|
} from '~types'
|
||||||
import { Utils } from '~utils'
|
import { Utils } from '~utils'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { useTLContext } from '~hooks'
|
import { useTLContext } from '~hooks'
|
||||||
|
@ -13,6 +21,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
||||||
pageState: TLPageState & {
|
pageState: TLPageState & {
|
||||||
bindingTargetId?: string | null
|
bindingTargetId?: string | null
|
||||||
},
|
},
|
||||||
|
assets: TLAssets,
|
||||||
isChildOfGhost = false,
|
isChildOfGhost = false,
|
||||||
isChildOfSelected = false,
|
isChildOfSelected = false,
|
||||||
meta?: M
|
meta?: M
|
||||||
|
@ -20,6 +29,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
||||||
// Create a node for this shape
|
// Create a node for this shape
|
||||||
const node: IShapeTreeNode<T, M> = {
|
const node: IShapeTreeNode<T, M> = {
|
||||||
shape,
|
shape,
|
||||||
|
asset: shape.assetId ? assets[shape.assetId] : undefined,
|
||||||
meta: meta as any,
|
meta: meta as any,
|
||||||
isChildOfSelected,
|
isChildOfSelected,
|
||||||
isGhost: shape.isGhost || isChildOfGhost,
|
isGhost: shape.isGhost || isChildOfGhost,
|
||||||
|
@ -54,6 +64,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
||||||
node.children!,
|
node.children!,
|
||||||
shapes,
|
shapes,
|
||||||
pageState,
|
pageState,
|
||||||
|
assets,
|
||||||
node.isGhost,
|
node.isGhost,
|
||||||
node.isSelected || node.isChildOfSelected,
|
node.isSelected || node.isChildOfSelected,
|
||||||
meta
|
meta
|
||||||
|
@ -69,6 +80,7 @@ function shapeIsInViewport(bounds: TLBounds, viewport: TLBounds) {
|
||||||
export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
||||||
page: TLPage<T, TLBinding>,
|
page: TLPage<T, TLBinding>,
|
||||||
pageState: TLPageState,
|
pageState: TLPageState,
|
||||||
|
assets: TLAssets,
|
||||||
meta?: M
|
meta?: M
|
||||||
) {
|
) {
|
||||||
const { callbacks, shapeUtils, bounds } = useTLContext()
|
const { callbacks, shapeUtils, bounds } = useTLContext()
|
||||||
|
@ -154,6 +166,7 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
||||||
tree,
|
tree,
|
||||||
page.shapes,
|
page.shapes,
|
||||||
{ ...pageState, bindingTargetId },
|
{ ...pageState, bindingTargetId },
|
||||||
|
assets,
|
||||||
shape.isGhost,
|
shape.isGhost,
|
||||||
false,
|
false,
|
||||||
meta
|
meta
|
||||||
|
|
|
@ -219,6 +219,12 @@ const tlcss = css`
|
||||||
contain: layout style size;
|
contain: layout style size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-inner-div {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.tl-stroke-hitarea {
|
.tl-stroke-hitarea {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
fill: none;
|
fill: none;
|
||||||
|
@ -309,12 +315,16 @@ const tlcss = css`
|
||||||
border-width: calc(1px * var(--tl-scale));
|
border-width: calc(1px * var(--tl-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-hovered {
|
||||||
|
stroke: var(--tl-selectStroke);
|
||||||
|
}
|
||||||
|
|
||||||
.tl-selected {
|
.tl-selected {
|
||||||
stroke: var(--tl-selectStroke);
|
stroke: var(--tl-selectStroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-hovered {
|
.tl-editing {
|
||||||
stroke: var(--tl-selectStroke);
|
stroke-width: calc(2.5px * min(5, var(--tl-scale)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-clone-target {
|
.tl-clone-target {
|
||||||
|
|
|
@ -4,6 +4,32 @@
|
||||||
|
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
|
export type TLAssets = Record<string, TLAsset>
|
||||||
|
|
||||||
|
export enum TLAssetType {
|
||||||
|
Image = 'image',
|
||||||
|
Video = 'video',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TLBaseAsset {
|
||||||
|
id: string
|
||||||
|
type: TLAssetType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TLImageAsset extends TLBaseAsset {
|
||||||
|
type: TLAssetType.Image
|
||||||
|
src: string
|
||||||
|
size: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TLVideoAsset extends TLBaseAsset {
|
||||||
|
type: TLAssetType.Video
|
||||||
|
src: string
|
||||||
|
size: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TLAsset = TLImageAsset | TLVideoAsset
|
||||||
|
|
||||||
export type Patch<T> = Partial<{ [P in keyof T]: T | Partial<T> | Patch<T[P]> }>
|
export type Patch<T> = Partial<{ [P in keyof T]: T | Partial<T> | Patch<T[P]> }>
|
||||||
|
|
||||||
export type TLForwardedRef<T> =
|
export type TLForwardedRef<T> =
|
||||||
|
@ -57,6 +83,7 @@ export interface TLShape {
|
||||||
childIndex: number
|
childIndex: number
|
||||||
name: string
|
name: string
|
||||||
point: number[]
|
point: number[]
|
||||||
|
assetId?: string
|
||||||
rotation?: number
|
rotation?: number
|
||||||
children?: string[]
|
children?: string[]
|
||||||
handles?: Record<string, TLHandle>
|
handles?: Record<string, TLHandle>
|
||||||
|
@ -69,6 +96,7 @@ export interface TLShape {
|
||||||
|
|
||||||
export interface TLComponentProps<T extends TLShape, E = any, M = any> {
|
export interface TLComponentProps<T extends TLShape, E = any, M = any> {
|
||||||
shape: T
|
shape: T
|
||||||
|
asset?: TLAsset
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
isBinding: boolean
|
isBinding: boolean
|
||||||
isHovered: boolean
|
isHovered: boolean
|
||||||
|
@ -117,6 +145,8 @@ export type TLWheelEventHandler = (
|
||||||
e: React.WheelEvent<Element> | WheelEvent
|
e: React.WheelEvent<Element> | WheelEvent
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
|
export type TLDropEventHandler = (e: React.DragEvent<Element>) => void
|
||||||
|
|
||||||
export type TLPinchEventHandler = (
|
export type TLPinchEventHandler = (
|
||||||
info: TLPointerInfo<string>,
|
info: TLPointerInfo<string>,
|
||||||
e:
|
e:
|
||||||
|
@ -176,6 +206,8 @@ export interface TLCallbacks<T extends TLShape> {
|
||||||
onRightPointCanvas: TLCanvasEventHandler
|
onRightPointCanvas: TLCanvasEventHandler
|
||||||
onDragCanvas: TLCanvasEventHandler
|
onDragCanvas: TLCanvasEventHandler
|
||||||
onReleaseCanvas: TLCanvasEventHandler
|
onReleaseCanvas: TLCanvasEventHandler
|
||||||
|
onDragOver: TLDropEventHandler
|
||||||
|
onDrop: TLDropEventHandler
|
||||||
|
|
||||||
// Shape
|
// Shape
|
||||||
onPointShape: TLPointerEventHandler
|
onPointShape: TLPointerEventHandler
|
||||||
|
@ -314,6 +346,7 @@ export type Snap =
|
||||||
|
|
||||||
export interface IShapeTreeNode<T extends TLShape, M = any> {
|
export interface IShapeTreeNode<T extends TLShape, M = any> {
|
||||||
shape: T
|
shape: T
|
||||||
|
asset?: TLAsset
|
||||||
children?: IShapeTreeNode<TLShape, M>[]
|
children?: IShapeTreeNode<TLShape, M>[]
|
||||||
isGhost: boolean
|
isGhost: boolean
|
||||||
isChildOfSelected: boolean
|
isChildOfSelected: boolean
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { ContextMenu } from '~components/ContextMenu'
|
||||||
import { FocusButton } from '~components/FocusButton'
|
import { FocusButton } from '~components/FocusButton'
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
import { GRID_SIZE } from '~constants'
|
import { GRID_SIZE } from '~constants'
|
||||||
|
import { Loading } from '~components/Loading'
|
||||||
|
|
||||||
export interface TldrawProps extends TDCallbacks {
|
export interface TldrawProps extends TDCallbacks {
|
||||||
/**
|
/**
|
||||||
|
@ -78,6 +79,14 @@ export interface TldrawProps extends TDCallbacks {
|
||||||
*/
|
*/
|
||||||
darkMode?: boolean
|
darkMode?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional) If provided, image/video componnets will be disabled.
|
||||||
|
*
|
||||||
|
* Warning: Keeping this enabled for multiplayer applications without provifing a storage
|
||||||
|
* 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.
|
* (optional) A callback to run when the component mounts.
|
||||||
*/
|
*/
|
||||||
|
@ -142,6 +151,16 @@ export interface TldrawProps extends TDCallbacks {
|
||||||
*/
|
*/
|
||||||
onRedo?: (state: TldrawApp) => void
|
onRedo?: (state: TldrawApp) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional) A callback to run when the user creates an image or video asset. Returns the desired "src" attribute eg: base64 (default) or remote URL
|
||||||
|
*/
|
||||||
|
onImageCreate?: (file: File, id: string) => Promise<string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional) A callback to run when the user deletes an image or video.
|
||||||
|
*/
|
||||||
|
onImageDelete?: (id: string) => void
|
||||||
|
|
||||||
onChangePage?: (
|
onChangePage?: (
|
||||||
app: TldrawApp,
|
app: TldrawApp,
|
||||||
shapes: Record<string, TDShape | undefined>,
|
shapes: Record<string, TDShape | undefined>,
|
||||||
|
@ -153,7 +172,6 @@ export function Tldraw({
|
||||||
id,
|
id,
|
||||||
document,
|
document,
|
||||||
currentPageId,
|
currentPageId,
|
||||||
darkMode = false,
|
|
||||||
autofocus = true,
|
autofocus = true,
|
||||||
showMenu = true,
|
showMenu = true,
|
||||||
showPages = true,
|
showPages = true,
|
||||||
|
@ -163,6 +181,7 @@ export function Tldraw({
|
||||||
showUI = true,
|
showUI = true,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
showSponsorLink = false,
|
showSponsorLink = false,
|
||||||
|
disableAssets = false,
|
||||||
onMount,
|
onMount,
|
||||||
onChange,
|
onChange,
|
||||||
onChangePresence,
|
onChangePresence,
|
||||||
|
@ -170,6 +189,7 @@ export function Tldraw({
|
||||||
onSaveProject,
|
onSaveProject,
|
||||||
onSaveProjectAs,
|
onSaveProjectAs,
|
||||||
onOpenProject,
|
onOpenProject,
|
||||||
|
onOpenMedia,
|
||||||
onSignOut,
|
onSignOut,
|
||||||
onSignIn,
|
onSignIn,
|
||||||
onUndo,
|
onUndo,
|
||||||
|
@ -178,30 +198,35 @@ export function Tldraw({
|
||||||
onPatch,
|
onPatch,
|
||||||
onCommand,
|
onCommand,
|
||||||
onChangePage,
|
onChangePage,
|
||||||
|
onImageCreate,
|
||||||
|
onImageDelete,
|
||||||
}: TldrawProps) {
|
}: TldrawProps) {
|
||||||
const [sId, setSId] = React.useState(id)
|
const [sId, setSId] = React.useState(id)
|
||||||
|
|
||||||
// Create a new app when the component mounts.
|
// Create a new app when the component mounts.
|
||||||
const [app, setApp] = React.useState(
|
const [app, setApp] = React.useState(() => {
|
||||||
() =>
|
const app = new TldrawApp(id, {
|
||||||
new TldrawApp(id, {
|
onMount,
|
||||||
onMount,
|
onChange,
|
||||||
onChange,
|
onChangePresence,
|
||||||
onChangePresence,
|
onNewProject,
|
||||||
onNewProject,
|
onSaveProject,
|
||||||
onSaveProject,
|
onSaveProjectAs,
|
||||||
onSaveProjectAs,
|
onOpenProject,
|
||||||
onOpenProject,
|
onOpenMedia,
|
||||||
onSignOut,
|
onSignOut,
|
||||||
onSignIn,
|
onSignIn,
|
||||||
onUndo,
|
onUndo,
|
||||||
onRedo,
|
onRedo,
|
||||||
onPersist,
|
onPersist,
|
||||||
onPatch,
|
onPatch,
|
||||||
onCommand,
|
onCommand,
|
||||||
onChangePage,
|
onChangePage,
|
||||||
})
|
onImageDelete,
|
||||||
)
|
onImageCreate,
|
||||||
|
})
|
||||||
|
return app
|
||||||
|
})
|
||||||
|
|
||||||
// Create a new app if the `id` prop changes.
|
// Create a new app if the `id` prop changes.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -215,6 +240,7 @@ export function Tldraw({
|
||||||
onSaveProject,
|
onSaveProject,
|
||||||
onSaveProjectAs,
|
onSaveProjectAs,
|
||||||
onOpenProject,
|
onOpenProject,
|
||||||
|
onOpenMedia,
|
||||||
onSignOut,
|
onSignOut,
|
||||||
onSignIn,
|
onSignIn,
|
||||||
onUndo,
|
onUndo,
|
||||||
|
@ -223,10 +249,10 @@ export function Tldraw({
|
||||||
onPatch,
|
onPatch,
|
||||||
onCommand,
|
onCommand,
|
||||||
onChangePage,
|
onChangePage,
|
||||||
|
onImageDelete,
|
||||||
|
onImageCreate,
|
||||||
})
|
})
|
||||||
|
|
||||||
setSId(id)
|
setSId(id)
|
||||||
|
|
||||||
setApp(newApp)
|
setApp(newApp)
|
||||||
}, [sId, id])
|
}, [sId, id])
|
||||||
|
|
||||||
|
@ -234,7 +260,6 @@ export function Tldraw({
|
||||||
// are the same, or else load a new document if the ids are different.
|
// are the same, or else load a new document if the ids are different.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!document) return
|
if (!document) return
|
||||||
|
|
||||||
if (document.id === app.document.id) {
|
if (document.id === app.document.id) {
|
||||||
app.updateDocument(document)
|
app.updateDocument(document)
|
||||||
} else {
|
} else {
|
||||||
|
@ -242,24 +267,22 @@ export function Tldraw({
|
||||||
}
|
}
|
||||||
}, [document, app])
|
}, [document, app])
|
||||||
|
|
||||||
// Change the page when the `currentPageId` prop changes
|
// Disable assets when the `disableAssets` prop changes.
|
||||||
|
React.useEffect(() => {
|
||||||
|
app.setDisableAssets(disableAssets)
|
||||||
|
}, [app, disableAssets])
|
||||||
|
|
||||||
|
// Change the page when the `currentPageId` prop changes.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentPageId) return
|
if (!currentPageId) return
|
||||||
app.changePage(currentPageId)
|
app.changePage(currentPageId)
|
||||||
}, [currentPageId, app])
|
}, [currentPageId, app])
|
||||||
|
|
||||||
// Toggle the app's readOnly mode when the `readOnly` prop changes
|
// Toggle the app's readOnly mode when the `readOnly` prop changes.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
app.readOnly = readOnly
|
app.readOnly = readOnly
|
||||||
}, [app, readOnly])
|
}, [app, readOnly])
|
||||||
|
|
||||||
// Toggle the app's readOnly mode when the `readOnly` prop changes
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (darkMode && !app.settings.isDarkMode) {
|
|
||||||
// app.toggleDarkMode()
|
|
||||||
}
|
|
||||||
}, [app, darkMode])
|
|
||||||
|
|
||||||
// Update the app's callbacks when any callback changes.
|
// Update the app's callbacks when any callback changes.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
app.callbacks = {
|
app.callbacks = {
|
||||||
|
@ -270,6 +293,7 @@ export function Tldraw({
|
||||||
onSaveProject,
|
onSaveProject,
|
||||||
onSaveProjectAs,
|
onSaveProjectAs,
|
||||||
onOpenProject,
|
onOpenProject,
|
||||||
|
onOpenMedia,
|
||||||
onSignOut,
|
onSignOut,
|
||||||
onSignIn,
|
onSignIn,
|
||||||
onUndo,
|
onUndo,
|
||||||
|
@ -278,6 +302,8 @@ export function Tldraw({
|
||||||
onPatch,
|
onPatch,
|
||||||
onCommand,
|
onCommand,
|
||||||
onChangePage,
|
onChangePage,
|
||||||
|
onImageDelete,
|
||||||
|
onImageCreate,
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
onMount,
|
onMount,
|
||||||
|
@ -287,6 +313,7 @@ export function Tldraw({
|
||||||
onSaveProject,
|
onSaveProject,
|
||||||
onSaveProjectAs,
|
onSaveProjectAs,
|
||||||
onOpenProject,
|
onOpenProject,
|
||||||
|
onOpenMedia,
|
||||||
onSignOut,
|
onSignOut,
|
||||||
onSignIn,
|
onSignIn,
|
||||||
onUndo,
|
onUndo,
|
||||||
|
@ -295,6 +322,8 @@ export function Tldraw({
|
||||||
onPatch,
|
onPatch,
|
||||||
onCommand,
|
onCommand,
|
||||||
onChangePage,
|
onChangePage,
|
||||||
|
onImageDelete,
|
||||||
|
onImageCreate,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Use the `key` to ensure that new selector hooks are made when the id changes
|
// Use the `key` to ensure that new selector hooks are made when the id changes
|
||||||
|
@ -354,6 +383,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
|
|
||||||
const page = document.pages[appState.currentPageId]
|
const page = document.pages[appState.currentPageId]
|
||||||
const pageState = document.pageStates[page.id]
|
const pageState = document.pageStates[page.id]
|
||||||
|
const assets = document.assets
|
||||||
const { selectedIds } = pageState
|
const { selectedIds } = pageState
|
||||||
|
|
||||||
const isHideBoundsShape =
|
const isHideBoundsShape =
|
||||||
|
@ -366,22 +396,6 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
page.shapes[selectedIds[0]] &&
|
page.shapes[selectedIds[0]] &&
|
||||||
TLDR.getShapeUtil(page.shapes[selectedIds[0]].type).hideResizeHandles
|
TLDR.getShapeUtil(page.shapes[selectedIds[0]].type).hideResizeHandles
|
||||||
|
|
||||||
const isInSession = app.session !== undefined
|
|
||||||
|
|
||||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
|
||||||
const hideBounds =
|
|
||||||
(isInSession && app.session?.constructor.name !== 'BrushSession') ||
|
|
||||||
!isSelecting ||
|
|
||||||
isHideBoundsShape ||
|
|
||||||
!!pageState.editingId
|
|
||||||
|
|
||||||
// Hide bounds when not using the select tool, or when in session
|
|
||||||
const hideHandles = isInSession || !isSelecting
|
|
||||||
|
|
||||||
// Hide indicators when not using the select tool, or when in session
|
|
||||||
const hideIndicators =
|
|
||||||
(isInSession && state.appState.status !== TDStatus.Brushing) || !isSelecting
|
|
||||||
|
|
||||||
// Custom rendering meta, with dark mode for shapes
|
// Custom rendering meta, with dark mode for shapes
|
||||||
const meta = React.useMemo(() => {
|
const meta = React.useMemo(() => {
|
||||||
return { isDarkMode: settings.isDarkMode }
|
return { isDarkMode: settings.isDarkMode }
|
||||||
|
@ -414,8 +428,28 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
elm.dispatchEvent(new Event('pointerup', { bubbles: true }))
|
elm.dispatchEvent(new Event('pointerup', { bubbles: true }))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const isInSession = app.session !== undefined
|
||||||
|
|
||||||
|
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||||
|
const hideBounds =
|
||||||
|
(isInSession && app.session?.constructor.name !== 'BrushSession') ||
|
||||||
|
!isSelecting ||
|
||||||
|
isHideBoundsShape ||
|
||||||
|
!!pageState.editingId
|
||||||
|
|
||||||
|
// Hide bounds when not using the select tool, or when in session
|
||||||
|
const hideHandles = isInSession || !isSelecting
|
||||||
|
|
||||||
|
// Hide indicators when not using the select tool, or when in session
|
||||||
|
const hideIndicators =
|
||||||
|
(isInSession && state.appState.status !== TDStatus.Brushing) || !isSelecting
|
||||||
|
|
||||||
|
const hideCloneHandles =
|
||||||
|
isInSession || !isSelecting || !settings.showCloneHandles || pageState.camera.zoom < 0.2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledLayout ref={rWrapper} tabIndex={-0} className={settings.isDarkMode ? dark : ''}>
|
<StyledLayout ref={rWrapper} tabIndex={-0} className={settings.isDarkMode ? dark : ''}>
|
||||||
|
<Loading />
|
||||||
<OneOff focusableRef={rWrapper} autofocus={autofocus} />
|
<OneOff focusableRef={rWrapper} autofocus={autofocus} />
|
||||||
<ContextMenu onBlur={handleMenuBlur}>
|
<ContextMenu onBlur={handleMenuBlur}>
|
||||||
<Renderer
|
<Renderer
|
||||||
|
@ -424,6 +458,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
shapeUtils={shapeUtils}
|
shapeUtils={shapeUtils}
|
||||||
page={page}
|
page={page}
|
||||||
pageState={pageState}
|
pageState={pageState}
|
||||||
|
assets={assets}
|
||||||
snapLines={appState.snapLines}
|
snapLines={appState.snapLines}
|
||||||
grid={GRID_SIZE}
|
grid={GRID_SIZE}
|
||||||
users={room?.users}
|
users={room?.users}
|
||||||
|
@ -435,7 +470,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
hideResizeHandles={isHideResizeHandlesShape}
|
hideResizeHandles={isHideResizeHandlesShape}
|
||||||
hideIndicators={hideIndicators}
|
hideIndicators={hideIndicators}
|
||||||
hideBindingHandles={!settings.showBindingHandles}
|
hideBindingHandles={!settings.showBindingHandles}
|
||||||
hideCloneHandles={!settings.showCloneHandles}
|
hideCloneHandles={hideCloneHandles}
|
||||||
hideRotateHandles={!settings.showRotateHandles}
|
hideRotateHandles={!settings.showRotateHandles}
|
||||||
hideGrid={!settings.showGrid}
|
hideGrid={!settings.showGrid}
|
||||||
onPinchStart={app.onPinchStart}
|
onPinchStart={app.onPinchStart}
|
||||||
|
@ -487,6 +522,8 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||||
onBoundsChange={app.updateBounds}
|
onBoundsChange={app.updateBounds}
|
||||||
onKeyDown={app.onKeyDown}
|
onKeyDown={app.onKeyDown}
|
||||||
onKeyUp={app.onKeyUp}
|
onKeyUp={app.onKeyUp}
|
||||||
|
onDragOver={app.onDragOver}
|
||||||
|
onDrop={app.onDrop}
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{showUI && (
|
{showUI && (
|
||||||
|
|
43
packages/tldraw/src/components/Loading/Loading.tsx
Normal file
43
packages/tldraw/src/components/Loading/Loading.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { Panel } from '~components/Primitives/Panel'
|
||||||
|
import { useTldrawApp } from '~hooks'
|
||||||
|
import { styled } from '~styles'
|
||||||
|
import type { TDSnapshot } from '~types'
|
||||||
|
|
||||||
|
const loadingSelector = (s: TDSnapshot) => s.appState.isLoading
|
||||||
|
|
||||||
|
export function Loading() {
|
||||||
|
const app = useTldrawApp()
|
||||||
|
const isLoading = app.useStore(loadingSelector)
|
||||||
|
|
||||||
|
return <StyledLoadingPanelContainer hidden={!isLoading}>Loading...</StyledLoadingPanelContainer>
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledLoadingPanelContainer = styled('div', {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: '50%',
|
||||||
|
transform: `translate(-50%, 0)`,
|
||||||
|
borderBottomLeftRadius: '12px',
|
||||||
|
borderBottomRightRadius: '12px',
|
||||||
|
padding: '8px',
|
||||||
|
fontFamily: 'var(--fonts-ui)',
|
||||||
|
fontSize: 'var(--fontSizes-1)',
|
||||||
|
boxShadow: 'var(--shadows-panel)',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
zIndex: 200,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
'& > div > *': {
|
||||||
|
pointerEvents: 'all',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
transform: {
|
||||||
|
hidden: {
|
||||||
|
transform: `translate(-50%, 100%)`,
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
transform: `translate(-50%, 0%)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
1
packages/tldraw/src/components/Loading/index.ts
Normal file
1
packages/tldraw/src/components/Loading/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { Loading } from './Loading'
|
|
@ -16,6 +16,7 @@ import { HeartIcon } from '~components/Primitives/icons/HeartIcon'
|
||||||
import { preventEvent } from '~components/preventEvent'
|
import { preventEvent } from '~components/preventEvent'
|
||||||
import { DiscordIcon } from '~components/Primitives/icons'
|
import { DiscordIcon } from '~components/Primitives/icons'
|
||||||
import type { TDSnapshot } from '~types'
|
import type { TDSnapshot } from '~types'
|
||||||
|
import { Divider } from '~components/Primitives/Divider'
|
||||||
|
|
||||||
interface MenuProps {
|
interface MenuProps {
|
||||||
showSponsorLink: boolean
|
showSponsorLink: boolean
|
||||||
|
@ -26,9 +27,14 @@ const numberOfSelectedIdsSelector = (s: TDSnapshot) => {
|
||||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length
|
return s.document.pageStates[s.appState.currentPageId].selectedIds.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disableAssetsSelector = (s: TDSnapshot) => {
|
||||||
|
return s.appState.disableAssets
|
||||||
|
}
|
||||||
|
|
||||||
export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: MenuProps) {
|
export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: MenuProps) {
|
||||||
const app = useTldrawApp()
|
const app = useTldrawApp()
|
||||||
const numberOfSelectedIds = app.useStore(numberOfSelectedIdsSelector)
|
const numberOfSelectedIds = app.useStore(numberOfSelectedIdsSelector)
|
||||||
|
const disableAssets = app.useStore(disableAssetsSelector)
|
||||||
|
|
||||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
||||||
|
|
||||||
|
@ -64,10 +70,14 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
|
||||||
app.selectAll()
|
app.selectAll()
|
||||||
}, [app])
|
}, [app])
|
||||||
|
|
||||||
const handleselectNone = React.useCallback(() => {
|
const handleSelectNone = React.useCallback(() => {
|
||||||
app.selectNone()
|
app.selectNone()
|
||||||
}, [app])
|
}, [app])
|
||||||
|
|
||||||
|
const handleUploadMedia = React.useCallback(() => {
|
||||||
|
app.openAsset()
|
||||||
|
}, [app])
|
||||||
|
|
||||||
const showFileMenu =
|
const showFileMenu =
|
||||||
app.callbacks.onNewProject ||
|
app.callbacks.onNewProject ||
|
||||||
app.callbacks.onOpenProject ||
|
app.callbacks.onOpenProject ||
|
||||||
|
@ -106,6 +116,14 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
|
||||||
Save As...
|
Save As...
|
||||||
</DMItem>
|
</DMItem>
|
||||||
)}
|
)}
|
||||||
|
{!disableAssets && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<DMItem onClick={handleUploadMedia} kbd="#U">
|
||||||
|
Upload Media
|
||||||
|
</DMItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DMSubMenu>
|
</DMSubMenu>
|
||||||
)}
|
)}
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
|
@ -148,7 +166,7 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
|
||||||
<DMItem onSelect={preventEvent} onClick={handleSelectAll} kbd="#A">
|
<DMItem onSelect={preventEvent} onClick={handleSelectAll} kbd="#A">
|
||||||
Select All
|
Select All
|
||||||
</DMItem>
|
</DMItem>
|
||||||
<DMItem onSelect={preventEvent} onClick={handleselectNone}>
|
<DMItem onSelect={preventEvent} onClick={handleSelectNone}>
|
||||||
Select None
|
Select None
|
||||||
</DMItem>
|
</DMItem>
|
||||||
</DMSubMenu>
|
</DMSubMenu>
|
||||||
|
|
|
@ -33,7 +33,6 @@ import {
|
||||||
TextAlignLeftIcon,
|
TextAlignLeftIcon,
|
||||||
TextAlignRightIcon,
|
TextAlignRightIcon,
|
||||||
} from '@radix-ui/react-icons'
|
} from '@radix-ui/react-icons'
|
||||||
import { RowButton } from '~components/Primitives/RowButton'
|
|
||||||
|
|
||||||
const currentStyleSelector = (s: TDSnapshot) => s.appState.currentStyle
|
const currentStyleSelector = (s: TDSnapshot) => s.appState.currentStyle
|
||||||
const selectedIdsSelector = (s: TDSnapshot) =>
|
const selectedIdsSelector = (s: TDSnapshot) =>
|
||||||
|
@ -290,27 +289,6 @@ const ColorGrid = styled('div', {
|
||||||
gap: 0,
|
gap: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// const StyledRowInner = styled('div', {
|
|
||||||
// height: '100%',
|
|
||||||
// width: '100%',
|
|
||||||
// backgroundColor: '$panel',
|
|
||||||
// borderRadius: '$2',
|
|
||||||
// display: 'flex',
|
|
||||||
// gap: '$1',
|
|
||||||
// flexDirection: 'row',
|
|
||||||
// alignItems: 'center',
|
|
||||||
// padding: '0 $3',
|
|
||||||
// justifyContent: 'space-between',
|
|
||||||
// border: '1px solid transparent',
|
|
||||||
|
|
||||||
// '& svg': {
|
|
||||||
// position: 'relative',
|
|
||||||
// stroke: '$overlay',
|
|
||||||
// strokeWidth: 1,
|
|
||||||
// zIndex: 1,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
|
|
||||||
export const StyledRow = styled('div', {
|
export const StyledRow = styled('div', {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
export const GRID_SIZE = 8
|
export const GRID_SIZE = 8
|
||||||
|
export const SVG_EXPORT_PADDING = 16
|
||||||
export const BINDING_DISTANCE = 16
|
export const BINDING_DISTANCE = 16
|
||||||
export const CLONING_DISTANCE = 32
|
export const CLONING_DISTANCE = 32
|
||||||
export const FIT_TO_SCREEN_PADDING = 128
|
export const FIT_TO_SCREEN_PADDING = 128
|
||||||
|
@ -79,3 +80,8 @@ export const USER_COLORS = [
|
||||||
'#55B467',
|
'#55B467',
|
||||||
'#FF802B',
|
'#FF802B',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|
||||||
|
|
||||||
|
export const IMAGE_EXTENSIONS = ['.png', '.svg', '.jpg', '.jpeg', '.gif']
|
||||||
|
export const VIDEO_EXTENSIONS = isSafari ? [] : ['.mp4', '.webm']
|
||||||
|
|
|
@ -42,10 +42,15 @@ export function useFileSystem() {
|
||||||
[promptSaveBeforeChange]
|
[promptSaveBeforeChange]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onOpenMedia = React.useCallback(async (app: TldrawApp) => {
|
||||||
|
app.openAsset?.()
|
||||||
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onNewProject,
|
onNewProject,
|
||||||
onSaveProject,
|
onSaveProject,
|
||||||
onSaveProjectAs,
|
onSaveProjectAs,
|
||||||
onOpenProject,
|
onOpenProject,
|
||||||
|
onOpenMedia,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,10 +36,19 @@ export function useFileSystemHandlers() {
|
||||||
[app]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onOpenMedia = React.useCallback(
|
||||||
|
async (e?: React.MouseEvent | React.KeyboardEvent | KeyboardEvent) => {
|
||||||
|
if (e && app.callbacks.onOpenMedia) e.preventDefault()
|
||||||
|
app.callbacks.onOpenMedia?.(app)
|
||||||
|
},
|
||||||
|
[app]
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onNewProject,
|
onNewProject,
|
||||||
onSaveProject,
|
onSaveProject,
|
||||||
onSaveProjectAs,
|
onSaveProjectAs,
|
||||||
onOpenProject,
|
onOpenProject,
|
||||||
|
onOpenMedia,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
const canHandleEvent = React.useCallback(
|
const canHandleEvent = React.useCallback(
|
||||||
(ignoreMenus = false) => {
|
(ignoreMenus = false) => {
|
||||||
const elm = ref.current
|
const elm = ref.current
|
||||||
if (ignoreMenus && app.isMenuOpen()) return true
|
if (ignoreMenus && app.isMenuOpen) return true
|
||||||
return elm && (document.activeElement === elm || elm.contains(document.activeElement))
|
return elm && (document.activeElement === elm || elm.contains(document.activeElement))
|
||||||
},
|
},
|
||||||
[ref]
|
[ref]
|
||||||
|
@ -155,7 +155,8 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
|
|
||||||
// File System
|
// File System
|
||||||
|
|
||||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs, onOpenMedia } =
|
||||||
|
useFileSystemHandlers()
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+n,⌘+n',
|
'ctrl+n,⌘+n',
|
||||||
|
@ -198,6 +199,15 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
undefined,
|
undefined,
|
||||||
[app]
|
[app]
|
||||||
)
|
)
|
||||||
|
useHotkeys(
|
||||||
|
'ctrl+u,⌘+u',
|
||||||
|
(e) => {
|
||||||
|
if (!canHandleEvent()) return
|
||||||
|
onOpenMedia(e)
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
[app]
|
||||||
|
)
|
||||||
|
|
||||||
// Undo Redo
|
// Undo Redo
|
||||||
|
|
||||||
|
|
|
@ -110,8 +110,6 @@ export class StateManager<T extends Record<string, any>> {
|
||||||
this._status = 'ready'
|
this._status = 'ready'
|
||||||
resolve(message)
|
resolve(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(message)
|
|
||||||
}).then((message) => {
|
}).then((message) => {
|
||||||
if (this.onReady) this.onReady(message)
|
if (this.onReady) this.onReady(message)
|
||||||
return message
|
return message
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type { SelectTool } from './tools/SelectTool'
|
||||||
describe('TldrawTestApp', () => {
|
describe('TldrawTestApp', () => {
|
||||||
describe('When copying and pasting...', () => {
|
describe('When copying and pasting...', () => {
|
||||||
it('copies a shape', () => {
|
it('copies a shape', () => {
|
||||||
const app = new TldrawTestApp().loadDocument(mockDocument).selectNone().copy(['rect1'])
|
new TldrawTestApp().loadDocument(mockDocument).selectNone().copy(['rect1'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('pastes a shape', () => {
|
it('pastes a shape', () => {
|
||||||
|
@ -43,6 +43,8 @@ describe('TldrawTestApp', () => {
|
||||||
expect(Object.keys(app.page.shapes).length).toBe(1)
|
expect(Object.keys(app.page.shapes).length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.todo('Copies and pastes a shape with an asset')
|
||||||
|
|
||||||
it('Copies grouped shapes.', () => {
|
it('Copies grouped shapes.', () => {
|
||||||
const app = new TldrawTestApp()
|
const app = new TldrawTestApp()
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
|
@ -581,10 +583,10 @@ describe('TldrawTestApp', () => {
|
||||||
|
|
||||||
it('Respects child index', () => {
|
it('Respects child index', () => {
|
||||||
const result = new TldrawTestApp()
|
const result = new TldrawTestApp()
|
||||||
.loadDocument(mockDocument)
|
.loadDocument(mockDocument)
|
||||||
.moveToBack(['rect2'])
|
.moveToBack(['rect2'])
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.copySvg()
|
.copySvg()
|
||||||
|
|
||||||
expect(result).toMatchSnapshot('copied svg with reordered elements')
|
expect(result).toMatchSnapshot('copied svg with reordered elements')
|
||||||
})
|
})
|
||||||
|
@ -710,3 +712,13 @@ describe('TldrawTestApp', () => {
|
||||||
.expectSelectedIdsToBe(['box1'])
|
.expectSelectedIdsToBe(['box1'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When adding an image', () => {
|
||||||
|
it.todo('Adds the image to the assets table')
|
||||||
|
it.todo('Does not add the image if that image already exists as an asset')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When adding a video', () => {
|
||||||
|
it.todo('Adds the video to the assets table')
|
||||||
|
it.todo('Does not add the video if that video already exists as an asset')
|
||||||
|
})
|
||||||
|
|
|
@ -14,6 +14,9 @@ import {
|
||||||
TLWheelEventHandler,
|
TLWheelEventHandler,
|
||||||
Utils,
|
Utils,
|
||||||
TLBounds,
|
TLBounds,
|
||||||
|
TLDropEventHandler,
|
||||||
|
TLAssetType,
|
||||||
|
TLAsset,
|
||||||
} from '@tldraw/core'
|
} from '@tldraw/core'
|
||||||
import {
|
import {
|
||||||
FlipType,
|
FlipType,
|
||||||
|
@ -41,6 +44,9 @@ import {
|
||||||
loadFileHandle,
|
loadFileHandle,
|
||||||
openFromFileSystem,
|
openFromFileSystem,
|
||||||
saveToFileSystem,
|
saveToFileSystem,
|
||||||
|
openAssetFromFileSystem,
|
||||||
|
fileToBase64,
|
||||||
|
getSizeFromDataurl,
|
||||||
} from './data'
|
} from './data'
|
||||||
import { TLDR } from './TLDR'
|
import { TLDR } from './TLDR'
|
||||||
import { shapeUtils } from '~state/shapes'
|
import { shapeUtils } from '~state/shapes'
|
||||||
|
@ -48,7 +54,14 @@ import { defaultStyle } from '~state/shapes/shared/shape-styles'
|
||||||
import * as Commands from './commands'
|
import * as Commands from './commands'
|
||||||
import { SessionArgsOfType, getSession, TldrawSession } from './sessions'
|
import { SessionArgsOfType, getSession, TldrawSession } from './sessions'
|
||||||
import type { BaseTool } from './tools/BaseTool'
|
import type { BaseTool } from './tools/BaseTool'
|
||||||
import { USER_COLORS, FIT_TO_SCREEN_PADDING, GRID_SIZE } from '~constants'
|
import {
|
||||||
|
USER_COLORS,
|
||||||
|
FIT_TO_SCREEN_PADDING,
|
||||||
|
GRID_SIZE,
|
||||||
|
IMAGE_EXTENSIONS,
|
||||||
|
VIDEO_EXTENSIONS,
|
||||||
|
SVG_EXPORT_PADDING,
|
||||||
|
} from '~constants'
|
||||||
import { SelectTool } from './tools/SelectTool'
|
import { SelectTool } from './tools/SelectTool'
|
||||||
import { EraseTool } from './tools/EraseTool'
|
import { EraseTool } from './tools/EraseTool'
|
||||||
import { TextTool } from './tools/TextTool'
|
import { TextTool } from './tools/TextTool'
|
||||||
|
@ -88,6 +101,10 @@ export interface TDCallbacks {
|
||||||
* (optional) A callback to run when the user opens new project through the menu or through a keyboard shortcut.
|
* (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
|
onOpenProject?: (state: TldrawApp, e?: KeyboardEvent) => void
|
||||||
|
/**
|
||||||
|
* (optional) A callback to run when the opens a file to upload.
|
||||||
|
*/
|
||||||
|
onOpenMedia?: (state: TldrawApp) => void
|
||||||
/**
|
/**
|
||||||
* (optional) A callback to run when the user signs in via the menu.
|
* (optional) A callback to run when the user signs in via the menu.
|
||||||
*/
|
*/
|
||||||
|
@ -128,6 +145,9 @@ export interface TDCallbacks {
|
||||||
* (optional) A callback to run when the user creates a new project.
|
* (optional) A callback to run when the user creates a new project.
|
||||||
*/
|
*/
|
||||||
onChangePresence?: (state: TldrawApp, user: TDUser) => void
|
onChangePresence?: (state: TldrawApp, user: TDUser) => void
|
||||||
|
|
||||||
|
onImageDelete?: (id: string) => void
|
||||||
|
onImageCreate?: (file: File, id: string) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TldrawApp extends StateManager<TDSnapshot> {
|
export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
|
@ -194,6 +214,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
clipboard?: {
|
clipboard?: {
|
||||||
shapes: TDShape[]
|
shapes: TDShape[]
|
||||||
bindings: TDBinding[]
|
bindings: TDBinding[]
|
||||||
|
assets: TLAsset[]
|
||||||
}
|
}
|
||||||
|
|
||||||
rotationInfo = {
|
rotationInfo = {
|
||||||
|
@ -262,6 +283,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => {
|
protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => {
|
||||||
const next = { ...state }
|
const next = { ...state }
|
||||||
|
|
||||||
|
const assetIdsInUse = new Set<string>([])
|
||||||
|
|
||||||
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
|
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
|
||||||
if (next.document !== prev.document) {
|
if (next.document !== prev.document) {
|
||||||
Object.entries(next.document.pages).forEach(([pageId, page]) => {
|
Object.entries(next.document.pages).forEach(([pageId, page]) => {
|
||||||
|
@ -290,6 +313,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
parentId = prevPage?.shapes[id]?.parentId
|
parentId = prevPage?.shapes[id]?.parentId
|
||||||
delete page.shapes[id]
|
delete page.shapes[id]
|
||||||
} else {
|
} else {
|
||||||
|
if (shape.assetId) assetIdsInUse.add(shape.assetId)
|
||||||
parentId = shape.parentId
|
parentId = shape.parentId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,6 +431,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup assets
|
||||||
|
|
||||||
const currentPageId = next.appState.currentPageId
|
const currentPageId = next.appState.currentPageId
|
||||||
|
|
||||||
const currentPageState = next.document.pageStates[currentPageId]
|
const currentPageState = next.document.pageStates[currentPageId]
|
||||||
|
@ -973,7 +999,32 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
isMenuOpen = (): boolean => this.appState.isMenuOpen
|
/**
|
||||||
|
* Toggles the state if something is loading
|
||||||
|
*/
|
||||||
|
setIsLoading = (isLoading: boolean): this => {
|
||||||
|
this.patchState({ appState: { isLoading } }, 'ui:toggled_is_loading')
|
||||||
|
this.persist()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisableAssets = (disableAssets: boolean): this => {
|
||||||
|
this.patchState({ appState: { disableAssets } }, 'ui:toggled_disable_images')
|
||||||
|
this.persist()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
get isMenuOpen(): boolean {
|
||||||
|
return this.appState.isMenuOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLoading(): boolean {
|
||||||
|
return this.appState.isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
get disableAssets(): boolean {
|
||||||
|
return this.appState.disableAssets
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle grids.
|
* Toggle grids.
|
||||||
|
@ -1051,6 +1102,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
.clearSelectHistory()
|
.clearSelectHistory()
|
||||||
.loadDocument(migrate(TldrawApp.defaultDocument, TldrawApp.version))
|
.loadDocument(migrate(TldrawApp.defaultDocument, TldrawApp.version))
|
||||||
.persist()
|
.persist()
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1258,6 +1310,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
appState: {
|
appState: {
|
||||||
...TldrawApp.defaultState.appState,
|
...TldrawApp.defaultState.appState,
|
||||||
currentPageId: Object.keys(document.pages)[0],
|
currentPageId: Object.keys(document.pages)[0],
|
||||||
|
disableAssets: this.disableAssets,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'loaded_document'
|
'loaded_document'
|
||||||
|
@ -1282,7 +1335,10 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
saveProject = async () => {
|
saveProject = async () => {
|
||||||
if (this.readOnly) return
|
if (this.readOnly) return
|
||||||
try {
|
try {
|
||||||
const fileHandle = await saveToFileSystem(this.document, this.fileSystemHandle)
|
const fileHandle = await saveToFileSystem(
|
||||||
|
migrate(this.document, TldrawApp.version),
|
||||||
|
this.fileSystemHandle
|
||||||
|
)
|
||||||
this.fileSystemHandle = fileHandle
|
this.fileSystemHandle = fileHandle
|
||||||
this.persist()
|
this.persist()
|
||||||
this.isDirty = false
|
this.isDirty = false
|
||||||
|
@ -1334,6 +1390,23 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload media from file
|
||||||
|
*/
|
||||||
|
openAsset = async () => {
|
||||||
|
if (!this.isLocal) return
|
||||||
|
if (!this.disableAssets)
|
||||||
|
try {
|
||||||
|
const file = await openAssetFromFileSystem()
|
||||||
|
if (!file) return
|
||||||
|
this.addMediaFromFile(file)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
this.persist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign out of the current account.
|
* Sign out of the current account.
|
||||||
* Should move to the www layer.
|
* Should move to the www layer.
|
||||||
|
@ -1560,28 +1633,29 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
const copyingShapeIds = ids.flatMap((id) =>
|
const copyingShapeIds = ids.flatMap((id) =>
|
||||||
TLDR.getDocumentBranch(this.state, id, this.currentPageId)
|
TLDR.getDocumentBranch(this.state, id, this.currentPageId)
|
||||||
)
|
)
|
||||||
|
|
||||||
const copyingShapes = copyingShapeIds.map((id) =>
|
const copyingShapes = copyingShapeIds.map((id) =>
|
||||||
Utils.deepClone(this.getShape(id, this.currentPageId))
|
Utils.deepClone(this.getShape(id, this.currentPageId))
|
||||||
)
|
)
|
||||||
|
|
||||||
if (copyingShapes.length === 0) return this
|
if (copyingShapes.length === 0) return this
|
||||||
|
|
||||||
const copyingBindings: TDBinding[] = Object.values(this.page.bindings).filter(
|
const copyingBindings: TDBinding[] = Object.values(this.page.bindings).filter(
|
||||||
(binding) =>
|
(binding) =>
|
||||||
copyingShapeIds.includes(binding.fromId) && copyingShapeIds.includes(binding.toId)
|
copyingShapeIds.includes(binding.fromId) && copyingShapeIds.includes(binding.toId)
|
||||||
)
|
)
|
||||||
|
const copyingAssets = copyingShapes
|
||||||
|
.map((shape) => {
|
||||||
|
if (!shape.assetId) return
|
||||||
|
return this.document.assets[shape.assetId]
|
||||||
|
})
|
||||||
|
.filter(Boolean) as TLAsset[]
|
||||||
this.clipboard = {
|
this.clipboard = {
|
||||||
shapes: copyingShapes,
|
shapes: copyingShapes,
|
||||||
bindings: copyingBindings,
|
bindings: copyingBindings,
|
||||||
|
assets: copyingAssets,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = JSON.stringify({
|
const text = JSON.stringify({
|
||||||
type: 'tldr/clipboard',
|
type: 'tldr/clipboard',
|
||||||
shapes: copyingShapes,
|
...this.clipboard,
|
||||||
bindings: copyingBindings,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
navigator.clipboard.writeText(text).then(
|
navigator.clipboard.writeText(text).then(
|
||||||
|
@ -1595,10 +1669,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Browser does not support copying to clipboard
|
// Browser does not support copying to clipboard
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pasteInfo.offset = [0, 0]
|
this.pasteInfo.offset = [0, 0]
|
||||||
this.pasteInfo.center = [0, 0]
|
this.pasteInfo.center = [0, 0]
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1618,35 +1690,35 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
*/
|
*/
|
||||||
paste = (point?: number[]) => {
|
paste = (point?: number[]) => {
|
||||||
if (this.readOnly) return
|
if (this.readOnly) return
|
||||||
const pasteInCurrentPage = (shapes: TDShape[], bindings: TDBinding[]) => {
|
const pasteInCurrentPage = (shapes: TDShape[], bindings: TDBinding[], assets: TLAsset[]) => {
|
||||||
const idsMap: Record<string, string> = {}
|
const idsMap: Record<string, string> = {}
|
||||||
|
const newAssets = assets.filter((asset) => this.document.assets[asset.id] === undefined)
|
||||||
|
if (newAssets.length) {
|
||||||
|
this.patchState({
|
||||||
|
document: {
|
||||||
|
assets: Object.fromEntries(newAssets.map((asset) => [asset.id, asset])),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
shapes.forEach((shape) => (idsMap[shape.id] = Utils.uniqueId()))
|
shapes.forEach((shape) => (idsMap[shape.id] = Utils.uniqueId()))
|
||||||
|
|
||||||
bindings.forEach((binding) => (idsMap[binding.id] = Utils.uniqueId()))
|
bindings.forEach((binding) => (idsMap[binding.id] = Utils.uniqueId()))
|
||||||
|
|
||||||
let startIndex = TLDR.getTopChildIndex(this.state, this.currentPageId)
|
let startIndex = TLDR.getTopChildIndex(this.state, this.currentPageId)
|
||||||
|
|
||||||
const shapesToPaste = shapes
|
const shapesToPaste = shapes
|
||||||
.sort((a, b) => a.childIndex - b.childIndex)
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
.map((shape) => {
|
.map((shape) => {
|
||||||
const parentShapeId = idsMap[shape.parentId]
|
const parentShapeId = idsMap[shape.parentId]
|
||||||
|
|
||||||
const copy = {
|
const copy = {
|
||||||
...shape,
|
...shape,
|
||||||
id: idsMap[shape.id],
|
id: idsMap[shape.id],
|
||||||
parentId: parentShapeId || this.currentPageId,
|
parentId: parentShapeId || this.currentPageId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shape.children) {
|
if (shape.children) {
|
||||||
copy.children = shape.children.map((id) => idsMap[id])
|
copy.children = shape.children.map((id) => idsMap[id])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parentShapeId) {
|
if (!parentShapeId) {
|
||||||
copy.childIndex = startIndex
|
copy.childIndex = startIndex
|
||||||
startIndex++
|
startIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (copy.handles) {
|
if (copy.handles) {
|
||||||
Object.values(copy.handles).forEach((handle) => {
|
Object.values(copy.handles).forEach((handle) => {
|
||||||
if (handle.bindingId) {
|
if (handle.bindingId) {
|
||||||
|
@ -1654,21 +1726,16 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return copy
|
return copy
|
||||||
})
|
})
|
||||||
|
|
||||||
const bindingsToPaste = bindings.map((binding) => ({
|
const bindingsToPaste = bindings.map((binding) => ({
|
||||||
...binding,
|
...binding,
|
||||||
id: idsMap[binding.id],
|
id: idsMap[binding.id],
|
||||||
toId: idsMap[binding.toId],
|
toId: idsMap[binding.toId],
|
||||||
fromId: idsMap[binding.fromId],
|
fromId: idsMap[binding.fromId],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
|
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
|
||||||
|
|
||||||
let center = Vec.toFixed(this.getPagePoint(point || this.centerPoint))
|
let center = Vec.toFixed(this.getPagePoint(point || this.centerPoint))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Vec.dist(center, this.pasteInfo.center) < 2 ||
|
Vec.dist(center, this.pasteInfo.center) < 2 ||
|
||||||
Vec.dist(center, Vec.toFixed(Utils.getBoundsCenter(commonBounds))) < 2
|
Vec.dist(center, Vec.toFixed(Utils.getBoundsCenter(commonBounds))) < 2
|
||||||
|
@ -1679,14 +1746,11 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
this.pasteInfo.center = center
|
this.pasteInfo.center = center
|
||||||
this.pasteInfo.offset = [0, 0]
|
this.pasteInfo.offset = [0, 0]
|
||||||
}
|
}
|
||||||
|
|
||||||
const centeredBounds = Utils.centerBounds(commonBounds, center)
|
const centeredBounds = Utils.centerBounds(commonBounds, center)
|
||||||
|
|
||||||
const delta = Vec.sub(
|
const delta = Vec.sub(
|
||||||
Utils.getBoundsCenter(centeredBounds),
|
Utils.getBoundsCenter(centeredBounds),
|
||||||
Utils.getBoundsCenter(commonBounds)
|
Utils.getBoundsCenter(commonBounds)
|
||||||
)
|
)
|
||||||
|
|
||||||
this.create(
|
this.create(
|
||||||
shapesToPaste.map((shape) =>
|
shapesToPaste.map((shape) =>
|
||||||
TLDR.getShapeUtil(shape.type).create({
|
TLDR.getShapeUtil(shape.type).create({
|
||||||
|
@ -1705,19 +1769,19 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
|
|
||||||
navigator.clipboard.readText().then((result) => {
|
navigator.clipboard.readText().then((result) => {
|
||||||
try {
|
try {
|
||||||
const data: { type: string; shapes: TDShape[]; bindings: TDBinding[] } =
|
const data: {
|
||||||
JSON.parse(result)
|
type: string
|
||||||
|
shapes: TDShape[]
|
||||||
|
bindings: TDBinding[]
|
||||||
|
assets: TLAsset[]
|
||||||
|
} = JSON.parse(result)
|
||||||
if (data.type !== 'tldr/clipboard') {
|
if (data.type !== 'tldr/clipboard') {
|
||||||
throw Error('The pasted string was not from the Tldraw clipboard.')
|
throw Error('The pasted string was not from the Tldraw clipboard.')
|
||||||
}
|
}
|
||||||
|
pasteInCurrentPage(data.shapes, data.bindings, data.assets)
|
||||||
pasteInCurrentPage(data.shapes, data.bindings)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
TLDR.warn(e)
|
TLDR.warn(e)
|
||||||
|
|
||||||
const shapeId = Utils.uniqueId()
|
const shapeId = Utils.uniqueId()
|
||||||
|
|
||||||
this.createShapes({
|
this.createShapes({
|
||||||
id: shapeId,
|
id: shapeId,
|
||||||
type: TDShapeType.Text,
|
type: TDShapeType.Text,
|
||||||
|
@ -1726,7 +1790,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
point: this.getPagePoint(this.centerPoint, this.currentPageId),
|
point: this.getPagePoint(this.centerPoint, this.currentPageId),
|
||||||
style: { ...this.appState.currentStyle },
|
style: { ...this.appState.currentStyle },
|
||||||
})
|
})
|
||||||
|
|
||||||
this.select(shapeId)
|
this.select(shapeId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1734,7 +1797,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
// Navigator does not support clipboard. Note that this fallback will
|
// Navigator does not support clipboard. Note that this fallback will
|
||||||
// not support pasting from one document to another.
|
// not support pasting from one document to another.
|
||||||
if (this.clipboard) {
|
if (this.clipboard) {
|
||||||
pasteInCurrentPage(this.clipboard.shapes, this.clipboard.bindings)
|
pasteInCurrentPage(this.clipboard.shapes, this.clipboard.bindings, this.clipboard.assets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1750,87 +1813,84 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
|
copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
|
||||||
if (ids.length === 0) ids = Object.keys(this.page.shapes)
|
if (ids.length === 0) ids = Object.keys(this.page.shapes)
|
||||||
if (ids.length === 0) return
|
if (ids.length === 0) return
|
||||||
|
|
||||||
const shapes = ids.map((id) => this.getShape(id, pageId))
|
|
||||||
shapes.sort((a, b) => a.childIndex - b.childIndex)
|
|
||||||
|
|
||||||
const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
|
|
||||||
const padding = 16
|
|
||||||
|
|
||||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||||
|
// Embed our custom fonts
|
||||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
|
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
|
||||||
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
||||||
|
|
||||||
style.textContent = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');`
|
style.textContent = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');`
|
||||||
defs.appendChild(style)
|
defs.appendChild(style)
|
||||||
svg.appendChild(defs)
|
svg.appendChild(defs)
|
||||||
|
// Get the shapes in order
|
||||||
function getSvgElementForShape(shape: TDShape) {
|
const shapes = ids
|
||||||
|
.map((id) => this.getShape(id, pageId))
|
||||||
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
// Find their common bounding box. S hapes will be positioned relative to this box
|
||||||
|
const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
|
||||||
|
// A quick routine to get an SVG element for each shape
|
||||||
|
const getSvgElementForShape = (shape: TDShape) => {
|
||||||
const util = TLDR.getShapeUtil(shape)
|
const util = TLDR.getShapeUtil(shape)
|
||||||
const element = util.getSvgElement(shape)
|
|
||||||
const bounds = util.getBounds(shape)
|
const bounds = util.getBounds(shape)
|
||||||
|
const elm = util.getSvgElement(shape)
|
||||||
if (!element) return
|
if (!elm) return
|
||||||
|
// If the element is an image, set the asset src as the xlinkhref
|
||||||
element.setAttribute(
|
if (shape.type === TDShapeType.Image) {
|
||||||
|
elm.setAttribute('xlink:href', this.document.assets[shape.assetId].src)
|
||||||
|
}
|
||||||
|
// Put the element in the correct position relative to the common bounds
|
||||||
|
elm.setAttribute(
|
||||||
'transform',
|
'transform',
|
||||||
`translate(${padding + shape.point[0] - commonBounds.minX}, ${
|
`translate(${SVG_EXPORT_PADDING + shape.point[0] - commonBounds.minX}, ${
|
||||||
padding + shape.point[1] - commonBounds.minY
|
SVG_EXPORT_PADDING + shape.point[1] - commonBounds.minY
|
||||||
}) rotate(${((shape.rotation || 0) * 180) / Math.PI}, ${bounds.width / 2}, ${
|
}) rotate(${((shape.rotation || 0) * 180) / Math.PI}, ${bounds.width / 2}, ${
|
||||||
bounds.height / 2
|
bounds.height / 2
|
||||||
})`
|
})`
|
||||||
)
|
)
|
||||||
|
return elm
|
||||||
return element
|
|
||||||
}
|
}
|
||||||
|
// Assemble the final SVG by iterating through each shape and its children
|
||||||
shapes.forEach((shape) => {
|
shapes.forEach((shape) => {
|
||||||
|
// The shape is a group! Just add the children.
|
||||||
if (shape.children?.length) {
|
if (shape.children?.length) {
|
||||||
// Create a group <g> element for shape
|
// Create a group <g> elm for shape
|
||||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
||||||
|
// Get the shape's children as elms and add them to the group
|
||||||
// Get the shape's children as elements
|
shape.children.forEach((childId) => {
|
||||||
shape.children
|
const shape = this.getShape(childId, pageId)
|
||||||
.map((childId) => this.getShape(childId, pageId))
|
const elm = getSvgElementForShape(shape)
|
||||||
.map(getSvgElementForShape)
|
if (elm) g.appendChild(elm)
|
||||||
.filter(Boolean)
|
})
|
||||||
.forEach((element) => g.appendChild(element!))
|
// Add the group elm to the SVG
|
||||||
|
|
||||||
// Add the group element to the SVG
|
|
||||||
svg.appendChild(g)
|
svg.appendChild(g)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Just add the shape's element to the
|
||||||
const element = getSvgElementForShape(shape)
|
const elm = getSvgElementForShape(shape)
|
||||||
|
if (elm) svg.appendChild(elm)
|
||||||
if (element) {
|
|
||||||
svg.appendChild(element)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
// Resize the elm to the bounding box
|
||||||
// Resize the element to the bounding box
|
|
||||||
svg.setAttribute(
|
svg.setAttribute(
|
||||||
'viewBox',
|
'viewBox',
|
||||||
[0, 0, commonBounds.width + padding * 2, commonBounds.height + padding * 2].join(' ')
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
commonBounds.width + SVG_EXPORT_PADDING * 2,
|
||||||
|
commonBounds.height + SVG_EXPORT_PADDING * 2,
|
||||||
|
].join(' ')
|
||||||
)
|
)
|
||||||
|
|
||||||
svg.setAttribute('width', String(commonBounds.width))
|
svg.setAttribute('width', String(commonBounds.width))
|
||||||
svg.setAttribute('height', String(commonBounds.height))
|
svg.setAttribute('height', String(commonBounds.height))
|
||||||
svg.setAttribute('fill', 'transparent')
|
svg.setAttribute('fill', 'transparent')
|
||||||
|
// Clean up the SVG by removing any hidden elements
|
||||||
svg
|
svg
|
||||||
.querySelectorAll('.tl-fill-hitarea, .tl-stroke-hitarea, .tl-binding-indicator')
|
.querySelectorAll('.tl-fill-hitarea, .tl-stroke-hitarea, .tl-binding-indicator')
|
||||||
.forEach((element) => element.remove())
|
.forEach((elm) => elm.remove())
|
||||||
|
// Serialize the SVG to a string
|
||||||
const s = new XMLSerializer()
|
const svgString = new XMLSerializer()
|
||||||
|
|
||||||
const svgString = s
|
|
||||||
.serializeToString(svg)
|
.serializeToString(svg)
|
||||||
.replaceAll(' ', '')
|
.replaceAll(' ', '')
|
||||||
.replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1')
|
.replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1')
|
||||||
|
// Copy the string to the clipboard
|
||||||
TLDR.copyStringToClipboard(svgString)
|
TLDR.copyStringToClipboard(svgString)
|
||||||
|
|
||||||
return svgString
|
return svgString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1843,7 +1903,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
copyJson = (ids = this.selectedIds, pageId = this.currentPageId) => {
|
copyJson = (ids = this.selectedIds, pageId = this.currentPageId) => {
|
||||||
if (ids.length === 0) ids = Object.keys(this.page.shapes)
|
if (ids.length === 0) ids = Object.keys(this.page.shapes)
|
||||||
if (ids.length === 0) return
|
if (ids.length === 0) return
|
||||||
|
|
||||||
const shapes = ids.map((id) => this.getShape(id, pageId))
|
const shapes = ids.map((id) => this.getShape(id, pageId))
|
||||||
const json = JSON.stringify(shapes, null, 2)
|
const json = JSON.stringify(shapes, null, 2)
|
||||||
TLDR.copyStringToClipboard(json)
|
TLDR.copyStringToClipboard(json)
|
||||||
|
@ -1872,7 +1931,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
},
|
},
|
||||||
reason
|
reason
|
||||||
)
|
)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2405,6 +2463,44 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createImageOrVideoShapeAtPoint(
|
||||||
|
id: string,
|
||||||
|
type: TDShapeType.Image | TDShapeType.Video,
|
||||||
|
point: number[],
|
||||||
|
size: number[],
|
||||||
|
assetId: string
|
||||||
|
): this {
|
||||||
|
const {
|
||||||
|
shapes,
|
||||||
|
appState: { currentPageId, currentStyle },
|
||||||
|
} = this
|
||||||
|
|
||||||
|
const childIndex =
|
||||||
|
shapes.length === 0
|
||||||
|
? 1
|
||||||
|
: shapes
|
||||||
|
.filter((shape) => shape.parentId === currentPageId)
|
||||||
|
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||||
|
|
||||||
|
const Shape = shapeUtils[type]
|
||||||
|
|
||||||
|
const newShape = Shape.create({
|
||||||
|
id,
|
||||||
|
parentId: currentPageId,
|
||||||
|
childIndex,
|
||||||
|
point,
|
||||||
|
size,
|
||||||
|
style: { ...currentStyle },
|
||||||
|
assetId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const bounds = Shape.getBounds(newShape as never)
|
||||||
|
newShape.point = Vec.sub(newShape.point, [bounds.width / 2, bounds.height / 2])
|
||||||
|
this.createShapes(newShape)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create one or more shapes.
|
* Create one or more shapes.
|
||||||
* @param shapes An array of shapes.
|
* @param shapes An array of shapes.
|
||||||
|
@ -2431,6 +2527,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
* @command
|
* @command
|
||||||
*/
|
*/
|
||||||
delete = (ids = this.selectedIds): this => {
|
delete = (ids = this.selectedIds): this => {
|
||||||
|
if (this.callbacks.onImageDelete) {
|
||||||
|
ids.forEach((id) => {
|
||||||
|
const node = this.getShape(id)
|
||||||
|
if (node.type === TDShapeType.Image || node.type === TDShapeType.Video)
|
||||||
|
this.callbacks.onImageDelete!(id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (ids.length === 0) return this
|
if (ids.length === 0) return this
|
||||||
return this.setState(Commands.deleteShapes(this, ids))
|
return this.setState(Commands.deleteShapes(this, ids))
|
||||||
}
|
}
|
||||||
|
@ -2701,6 +2805,52 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addMediaFromFile = async (file: File, point = this.centerPoint) => {
|
||||||
|
this.setIsLoading(true)
|
||||||
|
const id = Utils.uniqueId()
|
||||||
|
try {
|
||||||
|
let dataurl: string | ArrayBuffer | null
|
||||||
|
if (this.callbacks.onImageCreate) dataurl = await this.callbacks.onImageCreate(file, id)
|
||||||
|
else dataurl = await fileToBase64(file)
|
||||||
|
if (typeof dataurl === 'string') {
|
||||||
|
const extension = file.name.match(/\.[0-9a-z]+$/i)
|
||||||
|
if (!extension) throw Error('No extension')
|
||||||
|
const isImage = IMAGE_EXTENSIONS.includes(extension[0].toLowerCase())
|
||||||
|
const isVideo = VIDEO_EXTENSIONS.includes(extension[0].toLowerCase())
|
||||||
|
if (!(isImage || isVideo)) throw Error('Wrong extension')
|
||||||
|
let assetId = Utils.uniqueId()
|
||||||
|
const pagePoint = this.getPagePoint(point)
|
||||||
|
const shapeType = isImage ? TDShapeType.Image : TDShapeType.Video
|
||||||
|
const assetType = isImage ? TLAssetType.Image : TLAssetType.Video
|
||||||
|
const size = isImage ? await getSizeFromDataurl(dataurl) : [401.42, 401.42] // special
|
||||||
|
const match = Object.values(this.document.assets).find(
|
||||||
|
(asset) => asset.type === assetType && asset.src === dataurl
|
||||||
|
)
|
||||||
|
if (!match) {
|
||||||
|
this.patchState({
|
||||||
|
document: {
|
||||||
|
assets: {
|
||||||
|
[assetId]: {
|
||||||
|
id: assetId,
|
||||||
|
type: assetType,
|
||||||
|
src: dataurl,
|
||||||
|
size,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else assetId = match.id
|
||||||
|
this.createImageOrVideoShapeAtPoint(id, shapeType, pagePoint, size, assetId)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.setIsLoading(false)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
this.setIsLoading(false)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
/* Event Handlers */
|
/* Event Handlers */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
@ -2821,6 +2971,20 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
|
|
||||||
/* ------------- Renderer Event Handlers ------------ */
|
/* ------------- Renderer Event Handlers ------------ */
|
||||||
|
|
||||||
|
onDragOver: TLDropEventHandler = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onDrop: TLDropEventHandler = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (this.disableAssets) return this
|
||||||
|
if (e.dataTransfer.files?.length) {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
this.addMediaFromFile(file, [e.clientX, e.clientY])
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
onPinchStart: TLPinchEventHandler = (info, e) => this.currentTool.onPinchStart?.(info, e)
|
onPinchStart: TLPinchEventHandler = (info, e) => this.currentTool.onPinchStart?.(info, e)
|
||||||
|
|
||||||
onPinchEnd: TLPinchEventHandler = (info, e) => this.currentTool.onPinchEnd?.(info, e)
|
onPinchEnd: TLPinchEventHandler = (info, e) => this.currentTool.onPinchEnd?.(info, e)
|
||||||
|
@ -3014,6 +3178,21 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
this.originPoint = this.getPagePoint(info.point)
|
this.originPoint = this.getPagePoint(info.point)
|
||||||
this.updateInputs(info, e)
|
this.updateInputs(info, e)
|
||||||
this.currentTool.onDoubleClickBoundsHandle?.(info, e)
|
this.currentTool.onDoubleClickBoundsHandle?.(info, e)
|
||||||
|
// hack time to reset the size / clipping of an image
|
||||||
|
if (this.selectedIds.length !== 1) return
|
||||||
|
const shape = this.getShape(this.selectedIds[0])
|
||||||
|
if (shape.type === TDShapeType.Image || shape.type === TDShapeType.Video) {
|
||||||
|
const asset = this.document.assets[shape.assetId]
|
||||||
|
const util = TLDR.getShapeUtil(shape)
|
||||||
|
const centerA = util.getCenter(shape)
|
||||||
|
const centerB = util.getCenter({ ...shape, size: asset.size })
|
||||||
|
const delta = Vec.sub(centerB, centerA)
|
||||||
|
this.updateShapes({
|
||||||
|
id: shape.id,
|
||||||
|
point: Vec.sub(shape.point, delta),
|
||||||
|
size: asset.size,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRightPointBoundsHandle: TLBoundsHandleEventHandler = (info, e) => {
|
onRightPointBoundsHandle: TLBoundsHandleEventHandler = (info, e) => {
|
||||||
|
@ -3181,12 +3360,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
|
|
||||||
getShapeUtil = TLDR.getShapeUtil
|
getShapeUtil = TLDR.getShapeUtil
|
||||||
|
|
||||||
static version = 14
|
static version = 15
|
||||||
|
|
||||||
static defaultDocument: TDDocument = {
|
static defaultDocument: TDDocument = {
|
||||||
id: 'doc',
|
id: 'doc',
|
||||||
name: 'New Document',
|
name: 'New Document',
|
||||||
version: 14,
|
version: 15,
|
||||||
pages: {
|
pages: {
|
||||||
page: {
|
page: {
|
||||||
id: 'page',
|
id: 'page',
|
||||||
|
@ -3206,6 +3385,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
assets: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultState: TDSnapshot = {
|
static defaultState: TDSnapshot = {
|
||||||
|
@ -3215,6 +3395,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
isZoomSnap: false,
|
isZoomSnap: false,
|
||||||
isFocusMode: false,
|
isFocusMode: false,
|
||||||
isSnapping: false,
|
isSnapping: false,
|
||||||
|
//@ts-ignore
|
||||||
isDebugMode: process.env.NODE_ENV === 'development',
|
isDebugMode: process.env.NODE_ENV === 'development',
|
||||||
isReadonlyMode: false,
|
isReadonlyMode: false,
|
||||||
nudgeDistanceLarge: 16,
|
nudgeDistanceLarge: 16,
|
||||||
|
@ -3234,6 +3415,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
isMenuOpen: false,
|
isMenuOpen: false,
|
||||||
isEmptyCanvas: false,
|
isEmptyCanvas: false,
|
||||||
snapLines: [],
|
snapLines: [],
|
||||||
|
isLoading: false,
|
||||||
|
disableAssets: false,
|
||||||
},
|
},
|
||||||
document: TldrawApp.defaultDocument,
|
document: TldrawApp.defaultDocument,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type { TDDocument, TDFile } from '~types'
|
import type { TDDocument, TDFile } from '~types'
|
||||||
import { fileSave, fileOpen, FileSystemHandle } from './browser-fs-access'
|
import { fileSave, fileOpen, FileSystemHandle } from './browser-fs-access'
|
||||||
import { get as getFromIdb, set as setToIdb } from 'idb-keyval'
|
import { get as getFromIdb, set as setToIdb } from 'idb-keyval'
|
||||||
|
import { IMAGE_EXTENSIONS, VIDEO_EXTENSIONS } from '~constants'
|
||||||
|
|
||||||
const options = { mode: 'readwrite' as const }
|
const options = { mode: 'readwrite' as const }
|
||||||
|
|
||||||
|
@ -96,3 +97,31 @@ export async function openFromFileSystem(): Promise<null | {
|
||||||
document: file.document,
|
document: file.document,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function openAssetFromFileSystem() {
|
||||||
|
return fileOpen({
|
||||||
|
description: 'Image or Video',
|
||||||
|
extensions: [...IMAGE_EXTENSIONS, ...VIDEO_EXTENSIONS],
|
||||||
|
multiple: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fileToBase64(file: Blob): Promise<string | ArrayBuffer | null> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => resolve(reader.result)
|
||||||
|
reader.onerror = (error) => reject(error)
|
||||||
|
reader.onabort = (error) => reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSizeFromDataurl(dataURL: string): Promise<number[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => resolve([img.width, img.height])
|
||||||
|
img.src = dataURL
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,23 @@ import { Decoration, FontStyle, TDDocument, TDShapeType, TextShape } from '~type
|
||||||
export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
||||||
const { version = 0 } = document
|
const { version = 0 } = document
|
||||||
|
|
||||||
|
// Remove unused assets when loading a document
|
||||||
|
if ('assets' in document) {
|
||||||
|
const assetIdsInUse = new Set<string>()
|
||||||
|
|
||||||
|
Object.values(document.pages).forEach((page) =>
|
||||||
|
Object.values(page.shapes).forEach((shape) => {
|
||||||
|
if (shape.assetId) assetIdsInUse.add(shape.assetId)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
Object.keys(document.assets).forEach((assetId) => {
|
||||||
|
if (!assetIdsInUse.has(assetId)) {
|
||||||
|
delete document.assets[assetId]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (version === newVersion) return document
|
if (version === newVersion) return document
|
||||||
|
|
||||||
if (version < 14) {
|
if (version < 14) {
|
||||||
|
@ -51,6 +68,10 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
||||||
document.name = 'New Document'
|
document.name = 'New Document'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version < 15) {
|
||||||
|
document.assets = {}
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
Object.values(document.pageStates).forEach((pageState) => {
|
Object.values(document.pageStates).forEach((pageState) => {
|
||||||
pageState.selectedIds = pageState.selectedIds.filter((id) => {
|
pageState.selectedIds = pageState.selectedIds.filter((id) => {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
SessionType,
|
SessionType,
|
||||||
ArrowBinding,
|
ArrowBinding,
|
||||||
TldrawPatch,
|
TldrawPatch,
|
||||||
|
TDShapeType,
|
||||||
} from '~types'
|
} from '~types'
|
||||||
import { SLOW_SPEED, SNAP_DISTANCE } from '~constants'
|
import { SLOW_SPEED, SNAP_DISTANCE } from '~constants'
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
|
@ -632,6 +633,11 @@ export class TranslateSession extends BaseSession {
|
||||||
childIndex: TLDR.getChildIndexAbove(this.app.state, shape.id, currentPageId),
|
childIndex: TLDR.getChildIndexAbove(this.app.state, shape.id, currentPageId),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clone.type === TDShapeType.Video) {
|
||||||
|
const element = document.getElementById(shape.id + '_video') as HTMLVideoElement
|
||||||
|
if (element) clone.currentTime = (element.currentTime + 16) % element.duration
|
||||||
|
}
|
||||||
|
|
||||||
clones.push(clone)
|
clones.push(clone)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Image } from '..'
|
||||||
|
|
||||||
|
describe('Image shape', () => {
|
||||||
|
it('Creates a shape', () => {
|
||||||
|
expect(Image.create({ id: 'image' })).toMatchSnapshot('image')
|
||||||
|
})
|
||||||
|
})
|
175
packages/tldraw/src/state/shapes/ImageUtil/ImageUtil.tsx
Normal file
175
packages/tldraw/src/state/shapes/ImageUtil/ImageUtil.tsx
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { Utils, HTMLContainer } from '@tldraw/core'
|
||||||
|
import { TDShapeType, TDMeta, ImageShape } from '~types'
|
||||||
|
import { GHOSTED_OPACITY } from '~constants'
|
||||||
|
import { TDShapeUtil } from '../TDShapeUtil'
|
||||||
|
import {
|
||||||
|
defaultStyle,
|
||||||
|
getBoundsRectangle,
|
||||||
|
transformRectangle,
|
||||||
|
transformSingleRectangle,
|
||||||
|
} from '~state/shapes/shared'
|
||||||
|
import { styled } from '@stitches/react'
|
||||||
|
|
||||||
|
type T = ImageShape
|
||||||
|
type E = HTMLDivElement
|
||||||
|
|
||||||
|
export class ImageUtil extends TDShapeUtil<T, E> {
|
||||||
|
type = TDShapeType.Image as const
|
||||||
|
|
||||||
|
canBind = true
|
||||||
|
|
||||||
|
canClone = true
|
||||||
|
|
||||||
|
isAspectRatioLocked = true
|
||||||
|
|
||||||
|
showCloneHandles = true
|
||||||
|
|
||||||
|
getShape = (props: Partial<T>): T => {
|
||||||
|
return Utils.deepMerge<T>(
|
||||||
|
{
|
||||||
|
id: 'image',
|
||||||
|
type: TDShapeType.Image,
|
||||||
|
name: 'Image',
|
||||||
|
parentId: 'page',
|
||||||
|
childIndex: 1,
|
||||||
|
point: [0, 0],
|
||||||
|
size: [1, 1],
|
||||||
|
rotation: 0,
|
||||||
|
style: defaultStyle,
|
||||||
|
assetId: 'assetId',
|
||||||
|
},
|
||||||
|
props
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||||
|
({ shape, asset = { src: '' }, isBinding, isGhost, meta, events, onShapeChange }, ref) => {
|
||||||
|
const { size } = shape
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (wrapperRef?.current) {
|
||||||
|
const [width, height] = size
|
||||||
|
wrapperRef.current.style.width = `${width}px`
|
||||||
|
wrapperRef.current.style.height = `${height}px`
|
||||||
|
}
|
||||||
|
}, [size])
|
||||||
|
|
||||||
|
const imgRef = React.useRef<HTMLImageElement>(null)
|
||||||
|
const wrapperRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const onImageLoad = React.useCallback(() => {
|
||||||
|
if (imgRef?.current && wrapperRef?.current) {
|
||||||
|
const { width, height } = imgRef?.current
|
||||||
|
wrapperRef.current.style.width = `${width}px`
|
||||||
|
wrapperRef.current.style.height = `${height}px`
|
||||||
|
onShapeChange?.({ id: shape.id, size: [width, height] })
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HTMLContainer ref={ref} {...events}>
|
||||||
|
{isBinding && (
|
||||||
|
<div
|
||||||
|
className="tl-binding-indicator"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: `calc(${-this.bindingDistance}px * var(--tl-zoom))`,
|
||||||
|
left: `calc(${-this.bindingDistance}px * var(--tl-zoom))`,
|
||||||
|
width: `calc(100% + ${this.bindingDistance * 2}px * var(--tl-zoom))`,
|
||||||
|
height: `calc(100% + ${this.bindingDistance * 2}px * var(--tl-zoom))`,
|
||||||
|
backgroundColor: 'var(--tl-selectFill)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Wrapper
|
||||||
|
ref={wrapperRef}
|
||||||
|
isDarkMode={meta.isDarkMode} //
|
||||||
|
isGhost={isGhost}
|
||||||
|
>
|
||||||
|
<ImageElement
|
||||||
|
ref={imgRef}
|
||||||
|
src={asset.src}
|
||||||
|
alt="tl_image_asset"
|
||||||
|
draggable={false}
|
||||||
|
onLoad={onImageLoad}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Indicator = TDShapeUtil.Indicator<T>(({ shape }) => {
|
||||||
|
const {
|
||||||
|
size: [width, height],
|
||||||
|
} = shape
|
||||||
|
|
||||||
|
return (
|
||||||
|
<rect x={0} y={0} rx={2} ry={2} width={Math.max(1, width)} height={Math.max(1, height)} />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
getBounds = (shape: T) => {
|
||||||
|
return getBoundsRectangle(shape, this.boundsCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRender = (prev: T, next: T) => {
|
||||||
|
return next.size !== prev.size || next.style !== prev.style
|
||||||
|
}
|
||||||
|
|
||||||
|
transform = transformRectangle
|
||||||
|
|
||||||
|
transformSingle = transformSingleRectangle
|
||||||
|
|
||||||
|
getSvgElement = (shape: ImageShape) => {
|
||||||
|
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}`)
|
||||||
|
return elm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = styled('div', {
|
||||||
|
pointerEvents: 'all',
|
||||||
|
position: 'relative',
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
fontSize: '2em',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: '3px',
|
||||||
|
perspective: '800px',
|
||||||
|
p: {
|
||||||
|
userSelect: 'none',
|
||||||
|
},
|
||||||
|
img: {
|
||||||
|
userSelect: 'none',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
isGhost: {
|
||||||
|
false: { opacity: 1 },
|
||||||
|
true: { transition: 'opacity .2s', opacity: GHOSTED_OPACITY },
|
||||||
|
},
|
||||||
|
isDarkMode: {
|
||||||
|
true: {
|
||||||
|
boxShadow:
|
||||||
|
'2px 3px 12px -2px rgba(0,0,0,.3), 1px 1px 4px rgba(0,0,0,.3), 1px 1px 2px rgba(0,0,0,.3)',
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
boxShadow:
|
||||||
|
'2px 3px 12px -2px rgba(0,0,0,.2), 1px 1px 4px rgba(0,0,0,.16), 1px 1px 2px rgba(0,0,0,.16)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const ImageElement = styled('img', {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
|
minWidth: '100%',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
objectFit: 'cover',
|
||||||
|
borderRadius: 2,
|
||||||
|
})
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Image shape Creates a shape: image 1`] = `
|
||||||
|
Object {
|
||||||
|
"assetId": "assetId",
|
||||||
|
"childIndex": 1,
|
||||||
|
"id": "image",
|
||||||
|
"name": "Image",
|
||||||
|
"parentId": "page",
|
||||||
|
"point": Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
"rotation": 0,
|
||||||
|
"size": Array [
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
"style": Object {
|
||||||
|
"color": "black",
|
||||||
|
"dash": "draw",
|
||||||
|
"isFilled": false,
|
||||||
|
"scale": 1,
|
||||||
|
"size": "small",
|
||||||
|
},
|
||||||
|
"type": "image",
|
||||||
|
}
|
||||||
|
`;
|
1
packages/tldraw/src/state/shapes/ImageUtil/index.ts
Normal file
1
packages/tldraw/src/state/shapes/ImageUtil/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './ImageUtil'
|
|
@ -419,10 +419,8 @@ const commonTextWrapping = {
|
||||||
|
|
||||||
const InnerWrapper = styled('div', {
|
const InnerWrapper = styled('div', {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 'var(--tl-padding)',
|
width: '100%',
|
||||||
left: 'var(--tl-padding)',
|
height: '100%',
|
||||||
width: 'calc(100% - (var(--tl-padding) * 2))',
|
|
||||||
height: 'calc(100% - (var(--tl-padding) * 2))',
|
|
||||||
padding: '4px',
|
padding: '4px',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
minHeight: 1,
|
minHeight: 1,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Video } from '..'
|
||||||
|
|
||||||
|
describe('Video shape', () => {
|
||||||
|
it('Creates a shape', () => {
|
||||||
|
expect(Video.create({ id: 'video' })).toMatchSnapshot('video')
|
||||||
|
})
|
||||||
|
})
|
209
packages/tldraw/src/state/shapes/VideoUtil/VideoUtil.tsx
Normal file
209
packages/tldraw/src/state/shapes/VideoUtil/VideoUtil.tsx
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { Utils, HTMLContainer } from '@tldraw/core'
|
||||||
|
import { TDShapeType, TDMeta, VideoShape } from '~types'
|
||||||
|
import { GHOSTED_OPACITY } from '~constants'
|
||||||
|
import { TDShapeUtil } from '../TDShapeUtil'
|
||||||
|
import {
|
||||||
|
defaultStyle,
|
||||||
|
getBoundsRectangle,
|
||||||
|
transformRectangle,
|
||||||
|
transformSingleRectangle,
|
||||||
|
} from '~state/shapes/shared'
|
||||||
|
import { styled } from '@stitches/react'
|
||||||
|
import Vec from '@tldraw/vec'
|
||||||
|
|
||||||
|
type T = VideoShape
|
||||||
|
type E = HTMLDivElement
|
||||||
|
|
||||||
|
export class VideoUtil extends TDShapeUtil<T, E> {
|
||||||
|
type = TDShapeType.Video as const
|
||||||
|
canBind = true
|
||||||
|
canEdit = true
|
||||||
|
canClone = true
|
||||||
|
isAspectRatioLocked = true
|
||||||
|
showCloneHandles = true
|
||||||
|
isStateful = true // don't unmount
|
||||||
|
|
||||||
|
getShape = (props: Partial<T>): T => {
|
||||||
|
return Utils.deepMerge<T>(
|
||||||
|
{
|
||||||
|
id: 'video',
|
||||||
|
type: TDShapeType.Video,
|
||||||
|
name: 'Video',
|
||||||
|
parentId: 'page',
|
||||||
|
childIndex: 1,
|
||||||
|
point: [0, 0],
|
||||||
|
size: [1, 1],
|
||||||
|
rotation: 0,
|
||||||
|
style: defaultStyle,
|
||||||
|
assetId: 'assetId',
|
||||||
|
isPlaying: true,
|
||||||
|
currentTime: 0,
|
||||||
|
},
|
||||||
|
props
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||||
|
({ shape, asset, isBinding, isEditing, isGhost, meta, events, onShapeChange }, ref) => {
|
||||||
|
const rVideo = React.useRef<HTMLVideoElement>(null)
|
||||||
|
const wrapperRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const { currentTime = 0, size, isPlaying } = shape
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (wrapperRef.current) {
|
||||||
|
const [width, height] = size
|
||||||
|
wrapperRef.current.style.width = `${width}px`
|
||||||
|
wrapperRef.current.style.height = `${height}px`
|
||||||
|
}
|
||||||
|
}, [size])
|
||||||
|
|
||||||
|
const onImageLoad = React.useCallback(() => {
|
||||||
|
if (rVideo.current && wrapperRef.current) {
|
||||||
|
if (!Vec.isEqual(size, [401.42, 401.42])) return
|
||||||
|
const { videoWidth, videoHeight } = rVideo.current
|
||||||
|
wrapperRef.current.style.width = `${videoWidth}px`
|
||||||
|
wrapperRef.current.style.height = `${videoHeight}px`
|
||||||
|
const newSize = [videoWidth, videoHeight]
|
||||||
|
const delta = Vec.sub(size, newSize)
|
||||||
|
onShapeChange?.({
|
||||||
|
id: shape.id,
|
||||||
|
point: Vec.add(shape.point, Vec.div(delta, 2)),
|
||||||
|
size: [videoWidth, videoHeight],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [size])
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
const video = rVideo.current
|
||||||
|
if (!video) return
|
||||||
|
if (isPlaying) video.play()
|
||||||
|
// throws error on safari
|
||||||
|
else video.pause()
|
||||||
|
}, [isPlaying])
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
const video = rVideo.current
|
||||||
|
if (!video) return
|
||||||
|
if (currentTime !== video.currentTime) {
|
||||||
|
video.currentTime = currentTime
|
||||||
|
}
|
||||||
|
}, [currentTime])
|
||||||
|
|
||||||
|
const handlePlay = React.useCallback(() => {
|
||||||
|
onShapeChange?.({ id: shape.id, isPlaying: true })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handlePause = React.useCallback(() => {
|
||||||
|
onShapeChange?.({ id: shape.id, isPlaying: false })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSetCurrentTime = React.useCallback(() => {
|
||||||
|
const video = rVideo.current
|
||||||
|
if (!video) return
|
||||||
|
if (!isEditing) return
|
||||||
|
onShapeChange?.({ id: shape.id, currentTime: video.currentTime })
|
||||||
|
}, [isEditing])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HTMLContainer ref={ref} {...events}>
|
||||||
|
{isBinding && (
|
||||||
|
<div
|
||||||
|
className="tl-binding-indicator"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: -this.bindingDistance,
|
||||||
|
left: -this.bindingDistance,
|
||||||
|
width: `calc(100% + ${this.bindingDistance * 2}px)`,
|
||||||
|
height: `calc(100% + ${this.bindingDistance * 2}px)`,
|
||||||
|
backgroundColor: 'var(--tl-selectFill)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Wrapper ref={wrapperRef} isDarkMode={meta.isDarkMode} isGhost={isGhost}>
|
||||||
|
<VideoElement
|
||||||
|
ref={rVideo}
|
||||||
|
id={shape.id + '_video'}
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
disableRemotePlayback
|
||||||
|
disablePictureInPicture
|
||||||
|
controls={isEditing}
|
||||||
|
autoPlay={isPlaying}
|
||||||
|
onPlay={handlePlay}
|
||||||
|
onPause={handlePause}
|
||||||
|
onTimeUpdate={handleSetCurrentTime}
|
||||||
|
onLoadedMetadata={onImageLoad}
|
||||||
|
>
|
||||||
|
<source src={asset?.src} />
|
||||||
|
</VideoElement>
|
||||||
|
</Wrapper>
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Indicator = TDShapeUtil.Indicator<T>(({ shape }) => {
|
||||||
|
const {
|
||||||
|
size: [width, height],
|
||||||
|
} = shape
|
||||||
|
|
||||||
|
return (
|
||||||
|
<rect x={0} y={0} rx={2} ry={2} width={Math.max(1, width)} height={Math.max(1, height)} />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
getBounds = (shape: T) => {
|
||||||
|
return getBoundsRectangle(shape, this.boundsCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRender = (prev: T, next: T) => {
|
||||||
|
return next.size !== prev.size || next.style !== prev.style || next.isPlaying !== prev.isPlaying
|
||||||
|
}
|
||||||
|
|
||||||
|
transform = transformRectangle
|
||||||
|
|
||||||
|
transformSingle = transformSingleRectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = styled('div', {
|
||||||
|
pointerEvents: 'all',
|
||||||
|
position: 'relative',
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
fontSize: '2em',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
borderRadius: '3px',
|
||||||
|
perspective: '800px',
|
||||||
|
p: {
|
||||||
|
userSelect: 'none',
|
||||||
|
},
|
||||||
|
img: {
|
||||||
|
userSelect: 'none',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
isGhost: {
|
||||||
|
false: { opacity: 1 },
|
||||||
|
true: { transition: 'opacity .2s', opacity: GHOSTED_OPACITY },
|
||||||
|
},
|
||||||
|
isDarkMode: {
|
||||||
|
true: {
|
||||||
|
boxShadow:
|
||||||
|
'2px 3px 12px -2px rgba(0,0,0,.3), 1px 1px 4px rgba(0,0,0,.3), 1px 1px 2px rgba(0,0,0,.3)',
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
boxShadow:
|
||||||
|
'2px 3px 12px -2px rgba(0,0,0,.2), 1px 1px 4px rgba(0,0,0,.16), 1px 1px 2px rgba(0,0,0,.16)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const VideoElement = styled('video', {
|
||||||
|
maxWidth: '100%',
|
||||||
|
minWidth: '100%',
|
||||||
|
pointerEvents: 'all',
|
||||||
|
borderRadius: 2,
|
||||||
|
})
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Video shape Creates a shape: video 1`] = `
|
||||||
|
Object {
|
||||||
|
"assetId": "assetId",
|
||||||
|
"childIndex": 1,
|
||||||
|
"currentTime": 0,
|
||||||
|
"id": "video",
|
||||||
|
"isPlaying": true,
|
||||||
|
"name": "Video",
|
||||||
|
"parentId": "page",
|
||||||
|
"point": Array [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
"rotation": 0,
|
||||||
|
"size": Array [
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
"style": Object {
|
||||||
|
"color": "black",
|
||||||
|
"dash": "draw",
|
||||||
|
"isFilled": false,
|
||||||
|
"scale": 1,
|
||||||
|
"size": "small",
|
||||||
|
},
|
||||||
|
"type": "video",
|
||||||
|
}
|
||||||
|
`;
|
1
packages/tldraw/src/state/shapes/VideoUtil/index.ts
Normal file
1
packages/tldraw/src/state/shapes/VideoUtil/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './VideoUtil'
|
|
@ -7,7 +7,9 @@ import { GroupUtil } from './GroupUtil'
|
||||||
import { StickyUtil } from './StickyUtil'
|
import { StickyUtil } from './StickyUtil'
|
||||||
import { TextUtil } from './TextUtil'
|
import { TextUtil } from './TextUtil'
|
||||||
import { DrawUtil } from './DrawUtil'
|
import { DrawUtil } from './DrawUtil'
|
||||||
|
import { ImageUtil } from './ImageUtil'
|
||||||
import { TDShape, TDShapeType } from '~types'
|
import { TDShape, TDShapeType } from '~types'
|
||||||
|
import { VideoUtil } from './VideoUtil'
|
||||||
|
|
||||||
export const Rectangle = new RectangleUtil()
|
export const Rectangle = new RectangleUtil()
|
||||||
export const Triangle = new TriangleUtil()
|
export const Triangle = new TriangleUtil()
|
||||||
|
@ -17,6 +19,8 @@ export const Arrow = new ArrowUtil()
|
||||||
export const Text = new TextUtil()
|
export const Text = new TextUtil()
|
||||||
export const Group = new GroupUtil()
|
export const Group = new GroupUtil()
|
||||||
export const Sticky = new StickyUtil()
|
export const Sticky = new StickyUtil()
|
||||||
|
export const Image = new ImageUtil()
|
||||||
|
export const Video = new VideoUtil()
|
||||||
|
|
||||||
export const shapeUtils = {
|
export const shapeUtils = {
|
||||||
[TDShapeType.Rectangle]: Rectangle,
|
[TDShapeType.Rectangle]: Rectangle,
|
||||||
|
@ -27,6 +31,8 @@ export const shapeUtils = {
|
||||||
[TDShapeType.Text]: Text,
|
[TDShapeType.Text]: Text,
|
||||||
[TDShapeType.Group]: Group,
|
[TDShapeType.Group]: Group,
|
||||||
[TDShapeType.Sticky]: Sticky,
|
[TDShapeType.Sticky]: Sticky,
|
||||||
|
[TDShapeType.Image]: Image,
|
||||||
|
[TDShapeType.Video]: Video,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getShapeUtil = <T extends TDShape>(shape: T | T['type']) => {
|
export const getShapeUtil = <T extends TDShape>(shape: T | T['type']) => {
|
||||||
|
|
|
@ -173,7 +173,11 @@ export class SelectTool extends BaseTool<Status> {
|
||||||
/* ----------------- Event Handlers ----------------- */
|
/* ----------------- Event Handlers ----------------- */
|
||||||
|
|
||||||
onCancel = () => {
|
onCancel = () => {
|
||||||
this.selectNone()
|
if (this.app.pageState.editingId) {
|
||||||
|
this.app.setEditingId()
|
||||||
|
} else {
|
||||||
|
this.selectNone()
|
||||||
|
}
|
||||||
this.app.cancelSession()
|
this.app.cancelSession()
|
||||||
this.setStatus(Status.Idle)
|
this.setStatus(Status.Idle)
|
||||||
}
|
}
|
||||||
|
@ -372,7 +376,7 @@ export class SelectTool extends BaseTool<Status> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPointerUp: TLPointerEventHandler = (info, e) => {
|
onPointerUp: TLPointerEventHandler = (info) => {
|
||||||
if (this.status === Status.MiddleWheelPanning) {
|
if (this.status === Status.MiddleWheelPanning) {
|
||||||
this.setStatus(Status.Idle)
|
this.setStatus(Status.Idle)
|
||||||
return
|
return
|
||||||
|
|
|
@ -64,4 +64,5 @@ export const mockDocument: TDDocument = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
assets: {},
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import type {
|
||||||
TLBoundsHandleEventHandler,
|
TLBoundsHandleEventHandler,
|
||||||
TLShapeBlurHandler,
|
TLShapeBlurHandler,
|
||||||
TLShapeCloneHandler,
|
TLShapeCloneHandler,
|
||||||
|
TLAssets,
|
||||||
} from '@tldraw/core'
|
} from '@tldraw/core'
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
@ -102,6 +103,8 @@ export interface TDSnapshot {
|
||||||
isMenuOpen: boolean
|
isMenuOpen: boolean
|
||||||
status: string
|
status: string
|
||||||
snapLines: TLSnapLine[]
|
snapLines: TLSnapLine[]
|
||||||
|
isLoading: boolean
|
||||||
|
disableAssets: boolean
|
||||||
}
|
}
|
||||||
document: TDDocument
|
document: TDDocument
|
||||||
room?: {
|
room?: {
|
||||||
|
@ -130,6 +133,7 @@ export interface TDDocument {
|
||||||
version: number
|
version: number
|
||||||
pages: Record<string, TDPage>
|
pages: Record<string, TDPage>
|
||||||
pageStates: Record<string, TLPageState>
|
pageStates: Record<string, TLPageState>
|
||||||
|
assets: TLAssets
|
||||||
}
|
}
|
||||||
|
|
||||||
// The shape of a single page in the Tldraw document
|
// The shape of a single page in the Tldraw document
|
||||||
|
@ -277,6 +281,8 @@ export enum TDShapeType {
|
||||||
Line = 'line',
|
Line = 'line',
|
||||||
Text = 'text',
|
Text = 'text',
|
||||||
Group = 'group',
|
Group = 'group',
|
||||||
|
Image = 'image',
|
||||||
|
Video = 'video',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Decoration {
|
export enum Decoration {
|
||||||
|
@ -338,6 +344,20 @@ export interface RectangleShape extends TDBaseShape {
|
||||||
size: number[]
|
size: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageShape extends TDBaseShape {
|
||||||
|
type: TDShapeType.Image
|
||||||
|
size: number[]
|
||||||
|
assetId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoShape extends TDBaseShape {
|
||||||
|
type: TDShapeType.Video
|
||||||
|
size: number[]
|
||||||
|
assetId: string
|
||||||
|
isPlaying: boolean
|
||||||
|
currentTime: number
|
||||||
|
}
|
||||||
|
|
||||||
// The shape created by the Triangle tool
|
// The shape created by the Triangle tool
|
||||||
export interface TriangleShape extends TDBaseShape {
|
export interface TriangleShape extends TDBaseShape {
|
||||||
type: TDShapeType.Triangle
|
type: TDShapeType.Triangle
|
||||||
|
@ -374,6 +394,8 @@ export type TDShape =
|
||||||
| TextShape
|
| TextShape
|
||||||
| GroupShape
|
| GroupShape
|
||||||
| StickyShape
|
| StickyShape
|
||||||
|
| ImageShape
|
||||||
|
| VideoShape
|
||||||
|
|
||||||
/* ------------------ Shape Styles ------------------ */
|
/* ------------------ Shape Styles ------------------ */
|
||||||
|
|
||||||
|
|
615
yarn.lock
615
yarn.lock
|
@ -1077,6 +1077,385 @@
|
||||||
unique-filename "^1.1.1"
|
unique-filename "^1.1.1"
|
||||||
which "^1.3.1"
|
which "^1.3.1"
|
||||||
|
|
||||||
|
"@firebase/analytics-compat@0.1.5":
|
||||||
|
version "0.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.1.5.tgz#9fd587b1b6fa283354428a0f96a19db2389e7da4"
|
||||||
|
integrity sha512-5cfr0uWwlhoHQYAr6UtQCHwnGjs/3J/bWrfA3INNtzaN4/tTTLTD02iobbccRcM7dM5TR0sZFWS5orfAU3OBFg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/analytics" "0.7.4"
|
||||||
|
"@firebase/analytics-types" "0.7.0"
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/analytics-types@0.7.0":
|
||||||
|
version "0.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.7.0.tgz#91960e7c87ce8bf18cf8dd9e55ccbf5dc3989b5d"
|
||||||
|
integrity sha512-DNE2Waiwy5+zZnCfintkDtBfaW6MjIG883474v6Z0K1XZIvl76cLND4iv0YUb48leyF+PJK1KO2XrgHb/KpmhQ==
|
||||||
|
|
||||||
|
"@firebase/analytics@0.7.4":
|
||||||
|
version "0.7.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.7.4.tgz#33b3d6a34736e1a726652e48b6bd39163e6561c2"
|
||||||
|
integrity sha512-AU3XMwHW7SFGCNeUKKNW2wXGTdmS164ackt/Epu2bDXCT1OcauPE1AVd+ofULSIDCaDUAQVmvw3JrobgogEU7Q==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/installations" "0.5.4"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/app-check-compat@0.2.2":
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.2.2.tgz#7d6c04464a78cbc6a717cb4f33871e2f980cdb02"
|
||||||
|
integrity sha512-nX2Ou8Rwo+TMMNDecQOGH78kFw6sORLrsGyu0eC95M853JjisVxTngN1TU/RL5h83ElJ0HhNlz6C3FYAuGNqqA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/app-check" "0.5.2"
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/app-check-interop-types@0.1.0":
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.1.0.tgz#83afd9d41f99166c2bdb2d824e5032e9edd8fe53"
|
||||||
|
integrity sha512-uZfn9s4uuRsaX5Lwx+gFP3B6YsyOKUE+Rqa6z9ojT4VSRAsZFko9FRn6OxQUA1z5t5d08fY4pf+/+Dkd5wbdbA==
|
||||||
|
|
||||||
|
"@firebase/app-check@0.5.2":
|
||||||
|
version "0.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.5.2.tgz#5166aeed767efb8e5f0c719b83439e58abbee0fd"
|
||||||
|
integrity sha512-DJrvxcn5QPO5dU735GA9kYpf+GwmCmnd/oQdWVExrRG+yjaLnP0rSJ2HKQ4bZKGo8qig3P7fwQpdMOgP2BXFjQ==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/app-compat@0.1.12":
|
||||||
|
version "0.1.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.1.12.tgz#8a5fc169ad52c1fe9fe5119d543f12f9335cc8b2"
|
||||||
|
integrity sha512-hRzCCFjwTwrFsAFcuUW2TPpyShJ/OaoA1Yxp4QJr6Xod8g+CQxTMZ4RJ51I5t9fErXvl65VxljhfqFEyB3ZmJA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/app" "0.7.11"
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/app-types@0.7.0":
|
||||||
|
version "0.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.7.0.tgz#c9e16d1b8bed1a991840b8d2a725fb58d0b5899f"
|
||||||
|
integrity sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==
|
||||||
|
|
||||||
|
"@firebase/app@0.7.11":
|
||||||
|
version "0.7.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.7.11.tgz#b85d553dc44620ee0f795ecb6aeabd6c43737390"
|
||||||
|
integrity sha512-GnG2XxlMrqd8zRa14Y3gvkPpr0tKTLZtxhUnShWkeSM5bQqk1DK2k9qDsf6D3cYfKCWv+JIg1zmL3oalxfhNNA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/auth-compat@0.2.4":
|
||||||
|
version "0.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.2.4.tgz#e2862ed0177520b34abc6be6adca9f220a928ed9"
|
||||||
|
integrity sha512-2OpV6o8U33xiC98G9UrlhEMOOHfXmoum74VghP85BufLroi7erLKawBaDbYiHWK2QYudd8cbOPkk5GDocl1KNQ==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/auth" "0.19.4"
|
||||||
|
"@firebase/auth-types" "0.11.0"
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
node-fetch "2.6.5"
|
||||||
|
selenium-webdriver "^4.0.0-beta.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/auth-interop-types@0.1.6":
|
||||||
|
version "0.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz#5ce13fc1c527ad36f1bb1322c4492680a6cf4964"
|
||||||
|
integrity sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==
|
||||||
|
|
||||||
|
"@firebase/auth-types@0.11.0":
|
||||||
|
version "0.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.11.0.tgz#b9c73c60ca07945b3bbd7a097633e5f78fa9e886"
|
||||||
|
integrity sha512-q7Bt6cx+ySj9elQHTsKulwk3+qDezhzRBFC9zlQ1BjgMueUOnGMcvqmU0zuKlQ4RhLSH7MNAdBV2znVaoN3Vxw==
|
||||||
|
|
||||||
|
"@firebase/auth@0.19.4":
|
||||||
|
version "0.19.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.19.4.tgz#7d4962e70578e915d1a887be3d662c1fb030471e"
|
||||||
|
integrity sha512-0FefLGnP0mbgvSSan7j2e25i3pllqF9+KYO5fwuAo3YcgjCyNMBJKaXPlz/J+z6jRHa2itjh4W48jD4Y/FCMqw==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
node-fetch "2.6.5"
|
||||||
|
selenium-webdriver "4.0.0-rc-1"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/component@0.5.9":
|
||||||
|
version "0.5.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.5.9.tgz#a859f655bd6e5b691bc5596fe43a91b12a443052"
|
||||||
|
integrity sha512-oLCY3x9WbM5rn06qmUvbtJuPj4dIw/C9T4Th52IiHF5tiCRC5k6YthvhfUVcTwfoUhK0fOgtwuKJKA/LpCPjgA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/database-compat@0.1.4":
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-0.1.4.tgz#9bad05a4a14e557271b887b9ab97f8b39f91f5aa"
|
||||||
|
integrity sha512-dIJiZLDFF3U+MoEwoPBy7zxWmBUro1KefmwSHlpOoxmPv76tuoPm85NumpW/HmMrtTcTkC2qowtb6NjGE8X7mw==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/database" "0.12.4"
|
||||||
|
"@firebase/database-types" "0.9.3"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/database-types@0.9.3":
|
||||||
|
version "0.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.9.3.tgz#d1a8ee34601136fd0047817d94432d89fdba5fef"
|
||||||
|
integrity sha512-R+YXLWy/Q7mNUxiUYiMboTwvVoprrgfyvf1Viyevskw6IoH1q8HV1UjlkLSgmRsOT9HPWt7XZUEStVZJFknHwg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/app-types" "0.7.0"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
|
||||||
|
"@firebase/database@0.12.4":
|
||||||
|
version "0.12.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.12.4.tgz#7ad26393f59ede2b93444406651f976a7008114d"
|
||||||
|
integrity sha512-XkrL1kXELRNkqKcltuT4hfG1gWmFiGvjFY+z7Lhb//12MqdkLjwa9YMK8c6Lo+Ro+IkWcJArQaOQYe3GkU5Wgg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/auth-interop-types" "0.1.6"
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
faye-websocket "0.11.4"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/firestore-compat@0.1.10":
|
||||||
|
version "0.1.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.1.10.tgz#910ba0304ec9cb9202b08852dab206d3511833ec"
|
||||||
|
integrity sha512-wnyUzx5bHatnsP+3nX0FmA1jxfDxVW5gCdM59sXxd0PWf4oUOONRlqVstVAHVUH123huGaNdEXY6LUlP7H0EnA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/firestore" "3.4.1"
|
||||||
|
"@firebase/firestore-types" "2.5.0"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/firestore-types@2.5.0":
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-2.5.0.tgz#16fca40b6980fdb000de86042d7a96635f2bcdd7"
|
||||||
|
integrity sha512-I6c2m1zUhZ5SH0cWPmINabDyH5w0PPFHk2UHsjBpKdZllzJZ2TwTkXbDtpHUZNmnc/zAa0WNMNMvcvbb/xJLKA==
|
||||||
|
|
||||||
|
"@firebase/firestore@3.4.1":
|
||||||
|
version "3.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-3.4.1.tgz#b988a25213e51b112db4fef8d939634957f35b9f"
|
||||||
|
integrity sha512-KSXuaiavHUqk3+0qRe4U8QZ1vfpOc4PuesohLcjA824HexBzXd+6NoUmBs/F9pyS9Ka1rJeECXzXgpk0pInSBw==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
"@firebase/webchannel-wrapper" "0.6.1"
|
||||||
|
"@grpc/grpc-js" "^1.3.2"
|
||||||
|
"@grpc/proto-loader" "^0.6.0"
|
||||||
|
node-fetch "2.6.5"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/functions-compat@0.1.7":
|
||||||
|
version "0.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.1.7.tgz#0c73acedbf2701715fbec6b293ba1cd2549812c5"
|
||||||
|
integrity sha512-Rv3mAUIhsLTxIgPWJSESUcmE1tzNHzUlqQStPnxHn6eFFgHVhkU2wg/NMrKZWTFlb51jpKTjh51AQDhRdT3n3A==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/functions" "0.7.6"
|
||||||
|
"@firebase/functions-types" "0.5.0"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/functions-types@0.5.0":
|
||||||
|
version "0.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.5.0.tgz#b50ba95ccce9e96f7cda453228ffe1684645625b"
|
||||||
|
integrity sha512-qza0M5EwX+Ocrl1cYI14zoipUX4gI/Shwqv0C1nB864INAD42Dgv4v94BCyxGHBg2kzlWy8PNafdP7zPO8aJQA==
|
||||||
|
|
||||||
|
"@firebase/functions@0.7.6":
|
||||||
|
version "0.7.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.7.6.tgz#c2ae5866943d812580bda26200c0b17295505dc3"
|
||||||
|
integrity sha512-Kl6a2PbRkOlSlOWJSgYuNp3e53G3cb+axF+r7rbWhJIHiaelG16GerBMxZTSxyiCz77C24LwiA2TKNwe85ObZg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/app-check-interop-types" "0.1.0"
|
||||||
|
"@firebase/auth-interop-types" "0.1.6"
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/messaging-interop-types" "0.1.0"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
node-fetch "2.6.5"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/installations@0.5.4":
|
||||||
|
version "0.5.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.5.4.tgz#c6f5a40eee930d447c909d84f01f5ebfe2f5f46e"
|
||||||
|
integrity sha512-rYb6Ju/tIBhojmM8FsgS96pErKl6gPgJFnffMO4bKH7HilXhOfgLfKU9k51ZDcps8N0npDx9+AJJ6pL1aYuYZQ==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
idb "3.0.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/logger@0.3.2":
|
||||||
|
version "0.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.3.2.tgz#5046ffa8295c577846d54b6ca95645a03809800e"
|
||||||
|
integrity sha512-lzLrcJp9QBWpo40OcOM9B8QEtBw2Fk1zOZQdvv+rWS6gKmhQBCEMc4SMABQfWdjsylBcDfniD1Q+fUX1dcBTXA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/messaging-compat@0.1.4":
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.1.4.tgz#14dffa349e241557b10d8fb7f5896a04d3f857a7"
|
||||||
|
integrity sha512-6477jBw7w7hk0uhnTUMsPoukalpcwbxTTo9kMguHVSXe0t3OdoxeXEaapaNJlOmU4Kgc8j3rsms8IDLdKVpvlA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/messaging" "0.9.4"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/messaging-interop-types@0.1.0":
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.1.0.tgz#bdac02dd31edd5cb9eec37b1db698ea5e2c1a631"
|
||||||
|
integrity sha512-DbvUl/rXAZpQeKBnwz0NYY5OCqr2nFA0Bj28Fmr3NXGqR4PAkfTOHuQlVtLO1Nudo3q0HxAYLa68ZDAcuv2uKQ==
|
||||||
|
|
||||||
|
"@firebase/messaging@0.9.4":
|
||||||
|
version "0.9.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.9.4.tgz#a1cd38ad92eb92cde908dc695767362087137f6d"
|
||||||
|
integrity sha512-OvYV4MLPfDpdP/yltLqZXZRx6rXWz52bEilS2jL2B4sGiuTaXSkR6BIHB54EPTblu32nbyZYdlER4fssz4TfXw==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/installations" "0.5.4"
|
||||||
|
"@firebase/messaging-interop-types" "0.1.0"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
idb "3.0.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/performance-compat@0.1.4":
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.1.4.tgz#0e887e9d707515db0594117072375e18200703a9"
|
||||||
|
integrity sha512-YuGfmpC0o+YvEBlEZCbPdNbT4Nn2qhi5uMXjqKnNIUepmXUsgOYDiAqM9nxHPoE/6IkvoFMdCj5nTUYVLCFXgg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/performance" "0.5.4"
|
||||||
|
"@firebase/performance-types" "0.1.0"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/performance-types@0.1.0":
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.1.0.tgz#5e6efa9dc81860aee2cb7121b39ae8fa137e69fc"
|
||||||
|
integrity sha512-6p1HxrH0mpx+622Ql6fcxFxfkYSBpE3LSuwM7iTtYU2nw91Hj6THC8Bc8z4nboIq7WvgsT/kOTYVVZzCSlXl8w==
|
||||||
|
|
||||||
|
"@firebase/performance@0.5.4":
|
||||||
|
version "0.5.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.5.4.tgz#480bf61a8ff248e55506172be267029270457743"
|
||||||
|
integrity sha512-ES6aS4eoMhf9CczntBADDsXhaFea/3a0FADwy/VpWXXBxVb8tqc5tPcoTwd9L5M/aDeSiQMy344rhrSsTbIZEg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/installations" "0.5.4"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/polyfill@0.3.36":
|
||||||
|
version "0.3.36"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.36.tgz#c057cce6748170f36966b555749472b25efdb145"
|
||||||
|
integrity sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg==
|
||||||
|
dependencies:
|
||||||
|
core-js "3.6.5"
|
||||||
|
promise-polyfill "8.1.3"
|
||||||
|
whatwg-fetch "2.0.4"
|
||||||
|
|
||||||
|
"@firebase/remote-config-compat@0.1.4":
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.1.4.tgz#25561c070b2ba8e41e3f33aa9e9db592bbec5a37"
|
||||||
|
integrity sha512-6WeKR7E9KJ1RIF9GZiyle1uD4IsIPUBKUnUnFkQhj3FV6cGvQwbeG0rbh7QQLvd0IWuh9lABYjHXWp+rGHQk8A==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/remote-config" "0.3.3"
|
||||||
|
"@firebase/remote-config-types" "0.2.0"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/remote-config-types@0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.2.0.tgz#1e2759fc01f20b58c564db42196f075844c3d1fd"
|
||||||
|
integrity sha512-hqK5sCPeZvcHQ1D6VjJZdW6EexLTXNMJfPdTwbD8NrXUw6UjWC4KWhLK/TSlL0QPsQtcKRkaaoP+9QCgKfMFPw==
|
||||||
|
|
||||||
|
"@firebase/remote-config@0.3.3":
|
||||||
|
version "0.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.3.3.tgz#dedee2de508e2392ec2f254368adb7c2d969fc16"
|
||||||
|
integrity sha512-9hZWfB3k3IYsjHbWeUfhv/SDCcOgv/JMJpLXlUbTppXPm1IZ3X9ZW4I9bS86gGYr7m/kSv99U0oxQ7N9PoR8Iw==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/installations" "0.5.4"
|
||||||
|
"@firebase/logger" "0.3.2"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/storage-compat@0.1.8":
|
||||||
|
version "0.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.1.8.tgz#edbd9e2d8178c5695817e75f1da5c570c11f44dd"
|
||||||
|
integrity sha512-L5R0DQoHCDKIgcBbqTx+6+RQ2533WFKeV3cfLAZCTGjyMUustj0eYDsr7fLhGexwsnpT3DaxhlbzT3icUWoDaA==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/storage" "0.9.0"
|
||||||
|
"@firebase/storage-types" "0.6.0"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/storage-types@0.6.0":
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.6.0.tgz#0b1af64a2965af46fca138e5b70700e9b7e6312a"
|
||||||
|
integrity sha512-1LpWhcCb1ftpkP/akhzjzeFxgVefs6eMD2QeKiJJUGH1qOiows2w5o0sKCUSQrvrRQS1lz3SFGvNR1Ck/gqxeA==
|
||||||
|
|
||||||
|
"@firebase/storage@0.9.0":
|
||||||
|
version "0.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.9.0.tgz#e33d2dea4c056d70d801a20521aa96fa2e4fbfb8"
|
||||||
|
integrity sha512-1gSYdrwP9kECmugH9L3tvNMvSjnNJGamj91rrESOFk2ZHDO93qKR90awc68NnhmzFAJOT/eJzVm35LKU6SqUNg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/component" "0.5.9"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
node-fetch "2.6.5"
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/util@1.4.2":
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.4.2.tgz#271c63bb7cce4607f7679dc5624ef241c4cf2498"
|
||||||
|
integrity sha512-JMiUo+9QE9lMBvEtBjqsOFdmJgObFvi7OL1A0uFGwTmlCI1ZeNPOEBrwXkgTOelVCdiMO15mAebtEyxFuQ6FsA==
|
||||||
|
dependencies:
|
||||||
|
tslib "^2.1.0"
|
||||||
|
|
||||||
|
"@firebase/webchannel-wrapper@0.6.1":
|
||||||
|
version "0.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.6.1.tgz#0c74724ba6e9ea6ad25a391eab60a79eaba4c556"
|
||||||
|
integrity sha512-9FqhNjKQWpQ3fGnSOCovHOm+yhhiorKEqYLAfd525jWavunDJcx8rOW6i6ozAh+FbwcYMkL7b+3j4UR/30MpoQ==
|
||||||
|
|
||||||
|
"@grpc/grpc-js@^1.3.2":
|
||||||
|
version "1.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.4.5.tgz#0cd840b47180624eeedf066f2cdc422d052401f8"
|
||||||
|
integrity sha512-A6cOzSu7dqXZ7rzvh/9JZf+Jg/MOpLEMP0IdT8pT8hrWJZ6TB4ydN/MRuqOtAugInJe/VQ9F8BPricUpYZSaZA==
|
||||||
|
dependencies:
|
||||||
|
"@grpc/proto-loader" "^0.6.4"
|
||||||
|
"@types/node" ">=12.12.47"
|
||||||
|
|
||||||
|
"@grpc/proto-loader@^0.6.0", "@grpc/proto-loader@^0.6.4":
|
||||||
|
version "0.6.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.7.tgz#e62a202f4cf5897bdd0e244dec1dbc80d84bdfa1"
|
||||||
|
integrity sha512-QzTPIyJxU0u+r2qGe8VMl3j/W2ryhEvBv7hc42OjYfthSj370fUrb7na65rG6w3YLZS/fb8p89iTBobfWGDgdw==
|
||||||
|
dependencies:
|
||||||
|
"@types/long" "^4.0.1"
|
||||||
|
lodash.camelcase "^4.3.0"
|
||||||
|
long "^4.0.0"
|
||||||
|
protobufjs "^6.10.0"
|
||||||
|
yargs "^16.1.1"
|
||||||
|
|
||||||
"@hapi/accept@5.0.2":
|
"@hapi/accept@5.0.2":
|
||||||
version "5.0.2"
|
version "5.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523"
|
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523"
|
||||||
|
@ -2378,6 +2757,59 @@
|
||||||
resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.0.1.tgz#ed0da773bd5f794d0603f5a5b5cee6d2354e5660"
|
resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.0.1.tgz#ed0da773bd5f794d0603f5a5b5cee6d2354e5660"
|
||||||
integrity sha512-mMyQ9vjpuFqePkfe5bZVIf/H3Dmk6wA8Kjxff9RcO4kqzJo+Ek9pGKwZHpeMr7Eku0QhLXMCd7fNCSnEnRMubg==
|
integrity sha512-mMyQ9vjpuFqePkfe5bZVIf/H3Dmk6wA8Kjxff9RcO4kqzJo+Ek9pGKwZHpeMr7Eku0QhLXMCd7fNCSnEnRMubg==
|
||||||
|
|
||||||
|
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
||||||
|
integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
|
||||||
|
|
||||||
|
"@protobufjs/base64@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
|
||||||
|
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
|
||||||
|
|
||||||
|
"@protobufjs/codegen@^2.0.4":
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
|
||||||
|
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
|
||||||
|
|
||||||
|
"@protobufjs/eventemitter@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
|
||||||
|
integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
|
||||||
|
|
||||||
|
"@protobufjs/fetch@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
|
||||||
|
integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
|
||||||
|
dependencies:
|
||||||
|
"@protobufjs/aspromise" "^1.1.1"
|
||||||
|
"@protobufjs/inquire" "^1.1.0"
|
||||||
|
|
||||||
|
"@protobufjs/float@^1.0.2":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
|
||||||
|
integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
|
||||||
|
|
||||||
|
"@protobufjs/inquire@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
|
||||||
|
integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
|
||||||
|
|
||||||
|
"@protobufjs/path@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
|
||||||
|
integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
|
||||||
|
|
||||||
|
"@protobufjs/pool@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
|
||||||
|
integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
|
||||||
|
|
||||||
|
"@protobufjs/utf8@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||||
|
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
|
||||||
|
|
||||||
"@radix-ui/popper@0.1.0":
|
"@radix-ui/popper@0.1.0":
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/popper/-/popper-0.1.0.tgz#c387a38f31b7799e1ea0d2bb1ca0c91c2931b063"
|
resolved "https://registry.yarnpkg.com/@radix-ui/popper/-/popper-0.1.0.tgz#c387a38f31b7799e1ea0d2bb1ca0c91c2931b063"
|
||||||
|
@ -3248,6 +3680,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||||
|
|
||||||
|
"@types/long@^4.0.1":
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
|
||||||
|
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
|
||||||
|
|
||||||
"@types/minimatch@*", "@types/minimatch@^3.0.3":
|
"@types/minimatch@*", "@types/minimatch@^3.0.3":
|
||||||
version "3.0.5"
|
version "3.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
|
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
|
||||||
|
@ -3275,6 +3712,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.11.tgz#6ea7342dfb379ea1210835bada87b3c512120234"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.11.tgz#6ea7342dfb379ea1210835bada87b3c512120234"
|
||||||
integrity sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw==
|
integrity sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw==
|
||||||
|
|
||||||
|
"@types/node@>=12.12.47", "@types/node@>=13.7.0":
|
||||||
|
version "17.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.2.tgz#a4c07d47ff737e8ee7e586fe636ff0e1ddff070a"
|
||||||
|
integrity sha512-JepeIUPFDARgIs0zD/SKPgFsJEAF0X5/qO80llx59gOxFTboS9Amv3S+QfB7lqBId5sFXJ99BN0J6zFRvL9dDA==
|
||||||
|
|
||||||
"@types/node@^14.14.35", "@types/node@^14.6.2":
|
"@types/node@^14.14.35", "@types/node@^14.6.2":
|
||||||
version "14.18.0"
|
version "14.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.0.tgz#98df2397f6936bfbff4f089e40e06fa5dd88d32a"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.0.tgz#98df2397f6936bfbff4f089e40e06fa5dd88d32a"
|
||||||
|
@ -5308,6 +5750,11 @@ core-js-pure@^3.19.0:
|
||||||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.19.2.tgz#26b5bfb503178cff6e3e115bc2ba6c6419383680"
|
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.19.2.tgz#26b5bfb503178cff6e3e115bc2ba6c6419383680"
|
||||||
integrity sha512-5LkcgQEy8pFeVnd/zomkUBSwnmIxuF1C8E9KrMAbOc8f34IBT9RGvTYeNDdp1PnvMJrrVhvk1hg/yVV5h/znlg==
|
integrity sha512-5LkcgQEy8pFeVnd/zomkUBSwnmIxuF1C8E9KrMAbOc8f34IBT9RGvTYeNDdp1PnvMJrrVhvk1hg/yVV5h/znlg==
|
||||||
|
|
||||||
|
core-js@3.6.5:
|
||||||
|
version "3.6.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
||||||
|
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
|
||||||
|
|
||||||
core-js@^2.5.3:
|
core-js@^2.5.3:
|
||||||
version "2.6.12"
|
version "2.6.12"
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
||||||
|
@ -6108,11 +6555,6 @@ dotenv@8.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||||
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
|
||||||
|
|
||||||
dotenv@^10.0.0:
|
|
||||||
version "10.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
|
|
||||||
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
|
|
||||||
|
|
||||||
dotenv@^9.0.2:
|
dotenv@^9.0.2:
|
||||||
version "9.0.2"
|
version "9.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05"
|
||||||
|
@ -7047,6 +7489,13 @@ fastq@^1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify "^1.0.4"
|
reusify "^1.0.4"
|
||||||
|
|
||||||
|
faye-websocket@0.11.4:
|
||||||
|
version "0.11.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
|
||||||
|
integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==
|
||||||
|
dependencies:
|
||||||
|
websocket-driver ">=0.5.1"
|
||||||
|
|
||||||
fb-watchman@^2.0.0:
|
fb-watchman@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
|
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
|
||||||
|
@ -7212,6 +7661,38 @@ find-up@^4.0.0, find-up@^4.1.0:
|
||||||
locate-path "^5.0.0"
|
locate-path "^5.0.0"
|
||||||
path-exists "^4.0.0"
|
path-exists "^4.0.0"
|
||||||
|
|
||||||
|
firebase@^9.6.1:
|
||||||
|
version "9.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/firebase/-/firebase-9.6.1.tgz#08e0fd0799f57a885f895b86a6ed2bc0083412fe"
|
||||||
|
integrity sha512-d4wbkVMRiSREa1jfFx2z/Kq3KueEKfNWApvdrEAxvzDRN4eiFLeZSZM/MOxj7TR01e/hANnw2lrYKMUpg21ukg==
|
||||||
|
dependencies:
|
||||||
|
"@firebase/analytics" "0.7.4"
|
||||||
|
"@firebase/analytics-compat" "0.1.5"
|
||||||
|
"@firebase/app" "0.7.11"
|
||||||
|
"@firebase/app-check" "0.5.2"
|
||||||
|
"@firebase/app-check-compat" "0.2.2"
|
||||||
|
"@firebase/app-compat" "0.1.12"
|
||||||
|
"@firebase/app-types" "0.7.0"
|
||||||
|
"@firebase/auth" "0.19.4"
|
||||||
|
"@firebase/auth-compat" "0.2.4"
|
||||||
|
"@firebase/database" "0.12.4"
|
||||||
|
"@firebase/database-compat" "0.1.4"
|
||||||
|
"@firebase/firestore" "3.4.1"
|
||||||
|
"@firebase/firestore-compat" "0.1.10"
|
||||||
|
"@firebase/functions" "0.7.6"
|
||||||
|
"@firebase/functions-compat" "0.1.7"
|
||||||
|
"@firebase/installations" "0.5.4"
|
||||||
|
"@firebase/messaging" "0.9.4"
|
||||||
|
"@firebase/messaging-compat" "0.1.4"
|
||||||
|
"@firebase/performance" "0.5.4"
|
||||||
|
"@firebase/performance-compat" "0.1.4"
|
||||||
|
"@firebase/polyfill" "0.3.36"
|
||||||
|
"@firebase/remote-config" "0.3.3"
|
||||||
|
"@firebase/remote-config-compat" "0.1.4"
|
||||||
|
"@firebase/storage" "0.9.0"
|
||||||
|
"@firebase/storage-compat" "0.1.8"
|
||||||
|
"@firebase/util" "1.4.2"
|
||||||
|
|
||||||
flat-cache@^3.0.4:
|
flat-cache@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
|
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
|
||||||
|
@ -8001,6 +8482,11 @@ http-errors@~1.6.2:
|
||||||
setprototypeof "1.1.0"
|
setprototypeof "1.1.0"
|
||||||
statuses ">= 1.4.0 < 2"
|
statuses ">= 1.4.0 < 2"
|
||||||
|
|
||||||
|
http-parser-js@>=0.5.1:
|
||||||
|
version "0.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.5.tgz#d7c30d5d3c90d865b4a2e870181f9d6f22ac7ac5"
|
||||||
|
integrity sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA==
|
||||||
|
|
||||||
http-proxy-agent@^2.1.0:
|
http-proxy-agent@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
|
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
|
||||||
|
@ -8108,6 +8594,11 @@ idb-keyval@^6.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
safari-14-idb-fix "^3.0.0"
|
safari-14-idb-fix "^3.0.0"
|
||||||
|
|
||||||
|
idb@3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz#c8e9122d5ddd40f13b60ae665e4862f8b13fa384"
|
||||||
|
integrity sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==
|
||||||
|
|
||||||
idb@^6.1.4:
|
idb@^6.1.4:
|
||||||
version "6.1.5"
|
version "6.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.5.tgz#dbc53e7adf1ac7c59f9b2bf56e00b4ea4fce8c7b"
|
resolved "https://registry.yarnpkg.com/idb/-/idb-6.1.5.tgz#dbc53e7adf1ac7c59f9b2bf56e00b4ea4fce8c7b"
|
||||||
|
@ -9462,6 +9953,16 @@ jsprim@^1.2.2:
|
||||||
array-includes "^3.1.3"
|
array-includes "^3.1.3"
|
||||||
object.assign "^4.1.2"
|
object.assign "^4.1.2"
|
||||||
|
|
||||||
|
jszip@^3.6.0:
|
||||||
|
version "3.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
|
||||||
|
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
|
||||||
|
dependencies:
|
||||||
|
lie "~3.3.0"
|
||||||
|
pako "~1.0.2"
|
||||||
|
readable-stream "~2.3.6"
|
||||||
|
set-immediate-shim "~1.0.1"
|
||||||
|
|
||||||
keygrip@~1.1.0:
|
keygrip@~1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
|
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
|
||||||
|
@ -9663,6 +10164,13 @@ lie@3.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
immediate "~3.0.5"
|
immediate "~3.0.5"
|
||||||
|
|
||||||
|
lie@~3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
|
||||||
|
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
|
||||||
|
dependencies:
|
||||||
|
immediate "~3.0.5"
|
||||||
|
|
||||||
lines-and-columns@^1.1.6:
|
lines-and-columns@^1.1.6:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
|
||||||
|
@ -9816,6 +10324,11 @@ lodash._reinterpolate@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||||
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
|
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
|
||||||
|
|
||||||
|
lodash.camelcase@^4.3.0:
|
||||||
|
version "4.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||||
|
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
|
||||||
|
|
||||||
lodash.clonedeep@^4.5.0:
|
lodash.clonedeep@^4.5.0:
|
||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||||
|
@ -9899,6 +10412,11 @@ log-update@^4.0.0:
|
||||||
slice-ansi "^4.0.0"
|
slice-ansi "^4.0.0"
|
||||||
wrap-ansi "^6.2.0"
|
wrap-ansi "^6.2.0"
|
||||||
|
|
||||||
|
long@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
|
||||||
|
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
|
||||||
|
|
||||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
|
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
|
@ -10715,6 +11233,13 @@ node-fetch@2.6.1:
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||||
|
|
||||||
|
node-fetch@2.6.5:
|
||||||
|
version "2.6.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
|
||||||
|
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
|
||||||
|
dependencies:
|
||||||
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
|
node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
|
||||||
version "2.6.6"
|
version "2.6.6"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
|
||||||
|
@ -11283,7 +11808,7 @@ package-json@^6.3.0:
|
||||||
registry-url "^5.0.0"
|
registry-url "^5.0.0"
|
||||||
semver "^6.2.0"
|
semver "^6.2.0"
|
||||||
|
|
||||||
pako@~1.0.5:
|
pako@~1.0.2, pako@~1.0.5:
|
||||||
version "1.0.11"
|
version "1.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||||
|
@ -11778,6 +12303,11 @@ promise-inflight@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
||||||
|
|
||||||
|
promise-polyfill@8.1.3:
|
||||||
|
version "8.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116"
|
||||||
|
integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==
|
||||||
|
|
||||||
promise-retry@^1.1.1:
|
promise-retry@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d"
|
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d"
|
||||||
|
@ -11824,6 +12354,25 @@ proto-list@~1.2.1:
|
||||||
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||||
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
|
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
|
||||||
|
|
||||||
|
protobufjs@^6.10.0:
|
||||||
|
version "6.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b"
|
||||||
|
integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==
|
||||||
|
dependencies:
|
||||||
|
"@protobufjs/aspromise" "^1.1.2"
|
||||||
|
"@protobufjs/base64" "^1.1.2"
|
||||||
|
"@protobufjs/codegen" "^2.0.4"
|
||||||
|
"@protobufjs/eventemitter" "^1.1.0"
|
||||||
|
"@protobufjs/fetch" "^1.1.0"
|
||||||
|
"@protobufjs/float" "^1.0.2"
|
||||||
|
"@protobufjs/inquire" "^1.1.0"
|
||||||
|
"@protobufjs/path" "^1.1.2"
|
||||||
|
"@protobufjs/pool" "^1.1.0"
|
||||||
|
"@protobufjs/utf8" "^1.1.0"
|
||||||
|
"@types/long" "^4.0.1"
|
||||||
|
"@types/node" ">=13.7.0"
|
||||||
|
long "^4.0.0"
|
||||||
|
|
||||||
protocols@^1.1.0, protocols@^1.4.0:
|
protocols@^1.1.0, protocols@^1.4.0:
|
||||||
version "1.4.8"
|
version "1.4.8"
|
||||||
resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8"
|
resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8"
|
||||||
|
@ -12658,7 +13207,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||||
|
|
||||||
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
|
safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||||
|
@ -12734,6 +13283,25 @@ seek-bzip@^1.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander "^2.8.1"
|
commander "^2.8.1"
|
||||||
|
|
||||||
|
selenium-webdriver@4.0.0-rc-1:
|
||||||
|
version "4.0.0-rc-1"
|
||||||
|
resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-rc-1.tgz#b1e7e5821298c8a071e988518dd6b759f0c41281"
|
||||||
|
integrity sha512-bcrwFPRax8fifRP60p7xkWDGSJJoMkPAzufMlk5K2NyLPht/YZzR2WcIk1+3gR8VOCLlst1P2PI+MXACaFzpIw==
|
||||||
|
dependencies:
|
||||||
|
jszip "^3.6.0"
|
||||||
|
rimraf "^3.0.2"
|
||||||
|
tmp "^0.2.1"
|
||||||
|
ws ">=7.4.6"
|
||||||
|
|
||||||
|
selenium-webdriver@^4.0.0-beta.2:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.1.0.tgz#d11e5d43674e2718265a30684bcbf6ec734fd3bd"
|
||||||
|
integrity sha512-kUDH4N8WruYprTzvug4Pl73Th+WKb5YiLz8z/anOpHyUNUdM3UzrdTOxmSNaf9AczzBeY+qXihzku8D1lMaKOg==
|
||||||
|
dependencies:
|
||||||
|
jszip "^3.6.0"
|
||||||
|
tmp "^0.2.1"
|
||||||
|
ws ">=7.4.6"
|
||||||
|
|
||||||
semver-compare@^1.0.0:
|
semver-compare@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
||||||
|
@ -12794,6 +13362,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||||
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
||||||
|
|
||||||
|
set-immediate-shim@~1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
|
||||||
|
integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=
|
||||||
|
|
||||||
set-value@^2.0.0, set-value@^2.0.1:
|
set-value@^2.0.0, set-value@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
|
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
|
||||||
|
@ -13936,7 +14509,7 @@ tslib@^1.0.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
||||||
tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1:
|
tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||||
|
@ -14535,6 +15108,20 @@ webpack-sources@^1.4.3:
|
||||||
source-list-map "^2.0.0"
|
source-list-map "^2.0.0"
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
|
websocket-driver@>=0.5.1:
|
||||||
|
version "0.7.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
|
||||||
|
integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==
|
||||||
|
dependencies:
|
||||||
|
http-parser-js ">=0.5.1"
|
||||||
|
safe-buffer ">=5.1.0"
|
||||||
|
websocket-extensions ">=0.1.1"
|
||||||
|
|
||||||
|
websocket-extensions@>=0.1.1:
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
|
||||||
|
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
|
||||||
|
|
||||||
whatwg-encoding@^1.0.5:
|
whatwg-encoding@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
|
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
|
||||||
|
@ -14542,6 +15129,11 @@ whatwg-encoding@^1.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
iconv-lite "0.4.24"
|
iconv-lite "0.4.24"
|
||||||
|
|
||||||
|
whatwg-fetch@2.0.4:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
|
||||||
|
integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==
|
||||||
|
|
||||||
whatwg-mimetype@^2.3.0:
|
whatwg-mimetype@^2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||||
|
@ -14905,6 +15497,11 @@ write-pkg@^3.1.0:
|
||||||
sort-keys "^2.0.0"
|
sort-keys "^2.0.0"
|
||||||
write-json-file "^2.2.0"
|
write-json-file "^2.2.0"
|
||||||
|
|
||||||
|
ws@>=7.4.6:
|
||||||
|
version "8.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.0.tgz#f05e982a0a88c604080e8581576e2a063802bed6"
|
||||||
|
integrity sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ==
|
||||||
|
|
||||||
ws@^7.4.3, ws@^7.4.6:
|
ws@^7.4.3, ws@^7.4.6:
|
||||||
version "7.5.6"
|
version "7.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b"
|
||||||
|
@ -15011,7 +15608,7 @@ yargs-unparser@2.0.0:
|
||||||
flat "^5.0.2"
|
flat "^5.0.2"
|
||||||
is-plain-obj "^2.1.0"
|
is-plain-obj "^2.1.0"
|
||||||
|
|
||||||
yargs@16.2.0, yargs@^16.2.0:
|
yargs@16.2.0, yargs@^16.1.1, yargs@^16.2.0:
|
||||||
version "16.2.0"
|
version "16.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||||
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
|
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
|
||||||
|
|
Loading…
Reference in a new issue