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.map
|
||||
.env
|
||||
firebase.config.*
|
|
@ -51,6 +51,7 @@ function Editor({
|
|||
<div className="tldraw">
|
||||
<Tldraw
|
||||
autofocus
|
||||
disableAssets
|
||||
showPages={false}
|
||||
showSponsorLink={!isSponsor}
|
||||
onSignIn={isSponsor ? undefined : onSignIn}
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
"@types/react-router-dom": "^5.1.8",
|
||||
"concurrently": "6.0.1",
|
||||
"create-serve": "1.0.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"esbuild-envfile-plugin": "^1.0.1",
|
||||
"esbuild-serve": "^1.0.1",
|
||||
"firebase": "^9.6.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router": "^5.2.1",
|
||||
|
|
|
@ -35,7 +35,7 @@ export default function Develop(): JSX.Element {
|
|||
onSignIn={handleSignIn}
|
||||
onSignOut={handleSignOut}
|
||||
onPersist={handlePersist}
|
||||
showSponsorLink={true}
|
||||
showSponsorLink={false}
|
||||
/>
|
||||
</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 }) {
|
||||
const { error, ...events } = useMultiplayerState(roomId)
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<Tldraw showPages={false} {...events} />
|
||||
<Tldraw showPages={false} {...events} disableAssets={true} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ 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'
|
||||
import { Utils } from '@tldraw/core'
|
||||
|
||||
declare const window: Window & { app: TldrawApp }
|
||||
|
||||
|
@ -188,7 +187,6 @@ export function useMultiplayerState(roomId: string) {
|
|||
|
||||
// Update the document with initial content
|
||||
handleChanges()
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export const CenterHandle = observer<CenterHandleProps>(function CenterHandle({
|
|||
}): JSX.Element {
|
||||
return (
|
||||
<rect
|
||||
className={isLocked ? 'tl-bounds-center tl-dashed' : 'tl-bounds-center'}
|
||||
className={['tl-bounds-center', isLocked ? 'tl-dashed' : ''].join(' ')}
|
||||
x={-1}
|
||||
y={-1}
|
||||
width={bounds.width + 2}
|
||||
|
|
|
@ -27,26 +27,27 @@ export const CloneButton = observer<CloneButtonProps>(function CloneButton({
|
|||
targetSize,
|
||||
size,
|
||||
}: CloneButtonProps) {
|
||||
const s = targetSize * 2
|
||||
const x = {
|
||||
left: -44,
|
||||
topLeft: -44,
|
||||
bottomLeft: -44,
|
||||
right: bounds.width + 44,
|
||||
topRight: bounds.width + 44,
|
||||
bottomRight: bounds.width + 44,
|
||||
top: bounds.width / 2,
|
||||
bottom: bounds.width / 2,
|
||||
left: -s,
|
||||
topLeft: -s,
|
||||
bottomLeft: -s,
|
||||
right: bounds.width,
|
||||
topRight: bounds.width,
|
||||
bottomRight: bounds.width,
|
||||
top: bounds.width / 2 - s / 2,
|
||||
bottom: bounds.width / 2 - s / 2,
|
||||
}[side]
|
||||
|
||||
const y = {
|
||||
left: bounds.height / 2,
|
||||
right: bounds.height / 2,
|
||||
top: -44,
|
||||
topLeft: -44,
|
||||
topRight: -44,
|
||||
bottom: bounds.height + 44,
|
||||
bottomLeft: bounds.height + 44,
|
||||
bottomRight: bounds.height + 44,
|
||||
left: bounds.height / 2 - s / 2,
|
||||
right: bounds.height / 2 - s / 2,
|
||||
top: -s * 2,
|
||||
topLeft: -s,
|
||||
topRight: -s,
|
||||
bottom: bounds.height,
|
||||
bottomLeft: bounds.height,
|
||||
bottomRight: bounds.height,
|
||||
}[side]
|
||||
|
||||
const { callbacks, inputs } = useTLContext()
|
||||
|
@ -62,17 +63,11 @@ export const CloneButton = observer<CloneButtonProps>(function CloneButton({
|
|||
|
||||
return (
|
||||
<g className="tl-clone-target" transform={`translate(${x}, ${y})`} aria-label="clone button">
|
||||
<rect
|
||||
className="tl-transparent"
|
||||
width={targetSize * 4}
|
||||
height={targetSize * 4}
|
||||
x={-targetSize * 2}
|
||||
y={-targetSize * 2}
|
||||
/>
|
||||
<rect className="tl-transparent" width={targetSize * 2} height={targetSize * 2} />
|
||||
<g
|
||||
className="tl-clone-button-target"
|
||||
onPointerDown={handleClick}
|
||||
transform={`rotate(${ROTATIONS[side]})`}
|
||||
transform={`translate(${targetSize}, ${targetSize}) rotate(${ROTATIONS[side]})`}
|
||||
>
|
||||
<circle className="tl-transparent " r={targetSize} />
|
||||
<path
|
||||
|
|
|
@ -28,17 +28,18 @@ describe('CloneButton', () => {
|
|||
|
||||
const cloneBtn = screen.getByLabelText('clone button')
|
||||
|
||||
expect(cloneBtn).toHaveAttribute('transform', 'translate(50, -44)')
|
||||
expect(cloneBtn).toHaveAttribute('transform', 'translate(30, -80)')
|
||||
|
||||
// transparent rect
|
||||
const rect = cloneBtn.querySelector('rect')
|
||||
|
||||
expect(rect).toHaveAttribute('height', '80')
|
||||
expect(rect).toHaveAttribute('width', '80')
|
||||
expect(rect).toHaveAttribute('x', '-40')
|
||||
expect(rect).toHaveAttribute('y', '-40')
|
||||
expect(rect).toHaveAttribute('height', '40')
|
||||
expect(rect).toHaveAttribute('width', '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('path')).toHaveAttribute('d', 'M -5,-5 L 5,0 -5,5 Z')
|
||||
})
|
||||
|
|
|
@ -19,6 +19,7 @@ describe('page', () => {
|
|||
onBoundsChange={() => {
|
||||
// noop
|
||||
}}
|
||||
assets={{}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -9,7 +9,16 @@ import {
|
|||
useCameraCss,
|
||||
useKeyEvents,
|
||||
} 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 { Page } from '~components/Page'
|
||||
import { Users } from '~components/Users'
|
||||
|
@ -20,13 +29,10 @@ import { SnapLines } from '~components/SnapLines/SnapLines'
|
|||
import { Grid } from '~components/Grid'
|
||||
import { Overlay } from '~components/Overlay'
|
||||
|
||||
function resetError() {
|
||||
void null
|
||||
}
|
||||
|
||||
interface CanvasProps<T extends TLShape, M extends Record<string, unknown>> {
|
||||
page: TLPage<T, TLBinding>
|
||||
pageState: TLPageState
|
||||
assets: TLAssets
|
||||
snapLines?: TLSnapLine[]
|
||||
grid?: number
|
||||
users?: TLUsers<T>
|
||||
|
@ -52,6 +58,7 @@ export const Canvas = observer(function _Canvas<
|
|||
id,
|
||||
page,
|
||||
pageState,
|
||||
assets,
|
||||
snapLines,
|
||||
grid,
|
||||
users,
|
||||
|
@ -96,6 +103,7 @@ export const Canvas = observer(function _Canvas<
|
|||
<Page
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
assets={assets}
|
||||
hideBounds={hideBounds}
|
||||
hideIndicators={hideIndicators}
|
||||
hideHandles={hideHandles}
|
||||
|
|
|
@ -11,7 +11,7 @@ export const HTMLContainer = React.forwardRef<HTMLDivElement, HTMLContainerProps
|
|||
<Observer>
|
||||
{() => (
|
||||
<div ref={ref} className={`tl-positioned-div ${className}`} {...rest}>
|
||||
{children}
|
||||
<div className="tl-inner-div">{children}</div>
|
||||
</div>
|
||||
)}
|
||||
</Observer>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { observer } from 'mobx-react-lite'
|
||||
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 { Bounds } from '~components/Bounds'
|
||||
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>> {
|
||||
page: TLPage<T, TLBinding>
|
||||
pageState: TLPageState
|
||||
assets: TLAssets
|
||||
hideBounds: boolean
|
||||
hideHandles: 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>>({
|
||||
page,
|
||||
pageState,
|
||||
assets,
|
||||
hideBounds,
|
||||
hideHandles,
|
||||
hideIndicators,
|
||||
|
@ -40,30 +42,29 @@ export const Page = observer(function _Page<T extends TLShape, M extends Record<
|
|||
}: PageProps<T, M>): JSX.Element {
|
||||
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 {
|
||||
selectedIds,
|
||||
hoveredId,
|
||||
editingId,
|
||||
camera: { zoom },
|
||||
} = pageState
|
||||
|
||||
let _hideCloneHandles = true
|
||||
let _isEditing = false
|
||||
|
||||
// Does the selected shape have handles?
|
||||
let shapeWithHandles: TLShape | undefined = undefined
|
||||
|
||||
const selectedShapes = selectedIds.map((id) => page.shapes[id])
|
||||
|
||||
if (selectedShapes.length === 1) {
|
||||
const shape = selectedShapes[0]
|
||||
|
||||
_isEditing = editingId === shape.id
|
||||
const utils = shapeUtils[shape.type] as TLShapeUtil<any, any>
|
||||
|
||||
_hideCloneHandles = hideCloneHandles || !utils.showCloneHandles
|
||||
|
||||
if (shape.handles !== undefined) {
|
||||
shapeWithHandles = shape
|
||||
}
|
||||
|
@ -82,9 +83,10 @@ export const Page = observer(function _Page<T extends TLShape, M extends Record<
|
|||
shape={shape}
|
||||
meta={meta as any}
|
||||
isSelected
|
||||
isEditing={_isEditing}
|
||||
/>
|
||||
))}
|
||||
{!hideIndicators && hoveredId && (
|
||||
{!hideIndicators && hoveredId && hoveredId !== editingId && (
|
||||
<ShapeIndicator
|
||||
key={'hovered_' + hoveredId}
|
||||
shape={page.shapes[hoveredId]}
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
TLBinding,
|
||||
TLSnapLine,
|
||||
TLUsers,
|
||||
TLAssets,
|
||||
} from '../../types'
|
||||
import { Canvas } from '../Canvas'
|
||||
import { Inputs } from '../../inputs'
|
||||
|
@ -30,6 +31,10 @@ export interface RendererProps<T extends TLShape, M = any> extends Partial<TLCal
|
|||
* The current page state.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -121,6 +126,7 @@ export const Renderer = observer(function _Renderer<
|
|||
shapeUtils,
|
||||
page,
|
||||
pageState,
|
||||
assets,
|
||||
users,
|
||||
userId,
|
||||
theme,
|
||||
|
@ -177,6 +183,7 @@ export const Renderer = observer(function _Renderer<
|
|||
id={id}
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
assets={assets}
|
||||
snapLines={snapLines}
|
||||
grid={grid}
|
||||
users={users}
|
||||
|
|
|
@ -6,7 +6,13 @@ import { boxShape } from '~TLShapeUtil/TLShapeUtil.spec'
|
|||
describe('shape indicator', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
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
|
||||
isSelected?: boolean
|
||||
isHovered?: boolean
|
||||
isEditing?: boolean
|
||||
user?: TLUser<T>
|
||||
}
|
||||
|
||||
export const ShapeIndicator = observer(function ShapeIndicator<T extends TLShape, M>({
|
||||
isHovered = false,
|
||||
isSelected = false,
|
||||
isEditing = false,
|
||||
shape,
|
||||
user,
|
||||
meta,
|
||||
|
@ -26,9 +28,12 @@ export const ShapeIndicator = observer(function ShapeIndicator<T extends TLShape
|
|||
return (
|
||||
<div
|
||||
ref={rPositioned}
|
||||
className={
|
||||
'tl-indicator tl-absolute ' + (user ? '' : isSelected ? 'tl-selected' : 'tl-hovered')
|
||||
}
|
||||
className={[
|
||||
'tl-indicator',
|
||||
'tl-absolute',
|
||||
isSelected && !user ? 'tl-selected' : 'tl-hovered',
|
||||
isEditing ? 'tl-editing' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
<svg width="100%" height="100%">
|
||||
<g className="tl-centered-g" stroke={user?.color}>
|
||||
|
|
|
@ -11,10 +11,10 @@ export function useBoundsHandleEvents(
|
|||
(e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
e.stopPropagation()
|
||||
e.currentTarget?.setPointerCapture(e.pointerId)
|
||||
const info = inputs.pointerDown(e, id)
|
||||
|
||||
if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) {
|
||||
callbacks.onDoubleClickBoundsHandle?.(info, e)
|
||||
}
|
||||
callbacks.onPointBoundsHandle?.(info, e)
|
||||
callbacks.onPointerDown?.(info, e)
|
||||
},
|
||||
|
@ -25,18 +25,7 @@ export function useBoundsHandleEvents(
|
|||
(e: React.PointerEvent) => {
|
||||
if (e.button !== 0) return
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
e.stopPropagation()
|
||||
const isDoubleClick = inputs.isDoubleClick()
|
||||
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.onPointerUp?.(info, e)
|
||||
},
|
||||
|
@ -47,7 +36,6 @@ export function useBoundsHandleEvents(
|
|||
(e: React.PointerEvent) => {
|
||||
if (!inputs.pointerIsValid(e)) return
|
||||
e.stopPropagation()
|
||||
|
||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||
callbacks.onDragBoundsHandle?.(inputs.pointerMove(e, id), e)
|
||||
}
|
||||
|
|
|
@ -61,5 +61,7 @@ export function useCanvasEvents() {
|
|||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp,
|
||||
onDrop: callbacks.onDrop,
|
||||
onDragOver: callbacks.onDragOver,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
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 { Vec } from '@tldraw/vec'
|
||||
import { useTLContext } from '~hooks'
|
||||
|
@ -13,6 +21,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
|||
pageState: TLPageState & {
|
||||
bindingTargetId?: string | null
|
||||
},
|
||||
assets: TLAssets,
|
||||
isChildOfGhost = false,
|
||||
isChildOfSelected = false,
|
||||
meta?: M
|
||||
|
@ -20,6 +29,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
|||
// Create a node for this shape
|
||||
const node: IShapeTreeNode<T, M> = {
|
||||
shape,
|
||||
asset: shape.assetId ? assets[shape.assetId] : undefined,
|
||||
meta: meta as any,
|
||||
isChildOfSelected,
|
||||
isGhost: shape.isGhost || isChildOfGhost,
|
||||
|
@ -54,6 +64,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
|||
node.children!,
|
||||
shapes,
|
||||
pageState,
|
||||
assets,
|
||||
node.isGhost,
|
||||
node.isSelected || node.isChildOfSelected,
|
||||
meta
|
||||
|
@ -69,6 +80,7 @@ function shapeIsInViewport(bounds: TLBounds, viewport: TLBounds) {
|
|||
export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
|
||||
page: TLPage<T, TLBinding>,
|
||||
pageState: TLPageState,
|
||||
assets: TLAssets,
|
||||
meta?: M
|
||||
) {
|
||||
const { callbacks, shapeUtils, bounds } = useTLContext()
|
||||
|
@ -154,6 +166,7 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
|
|||
tree,
|
||||
page.shapes,
|
||||
{ ...pageState, bindingTargetId },
|
||||
assets,
|
||||
shape.isGhost,
|
||||
false,
|
||||
meta
|
||||
|
|
|
@ -219,6 +219,12 @@ const tlcss = css`
|
|||
contain: layout style size;
|
||||
}
|
||||
|
||||
.tl-inner-div {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tl-stroke-hitarea {
|
||||
cursor: pointer;
|
||||
fill: none;
|
||||
|
@ -309,12 +315,16 @@ const tlcss = css`
|
|||
border-width: calc(1px * var(--tl-scale));
|
||||
}
|
||||
|
||||
.tl-hovered {
|
||||
stroke: var(--tl-selectStroke);
|
||||
}
|
||||
|
||||
.tl-selected {
|
||||
stroke: var(--tl-selectStroke);
|
||||
}
|
||||
|
||||
.tl-hovered {
|
||||
stroke: var(--tl-selectStroke);
|
||||
.tl-editing {
|
||||
stroke-width: calc(2.5px * min(5, var(--tl-scale)));
|
||||
}
|
||||
|
||||
.tl-clone-target {
|
||||
|
|
|
@ -4,6 +4,32 @@
|
|||
|
||||
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 TLForwardedRef<T> =
|
||||
|
@ -57,6 +83,7 @@ export interface TLShape {
|
|||
childIndex: number
|
||||
name: string
|
||||
point: number[]
|
||||
assetId?: string
|
||||
rotation?: number
|
||||
children?: string[]
|
||||
handles?: Record<string, TLHandle>
|
||||
|
@ -69,6 +96,7 @@ export interface TLShape {
|
|||
|
||||
export interface TLComponentProps<T extends TLShape, E = any, M = any> {
|
||||
shape: T
|
||||
asset?: TLAsset
|
||||
isEditing: boolean
|
||||
isBinding: boolean
|
||||
isHovered: boolean
|
||||
|
@ -117,6 +145,8 @@ export type TLWheelEventHandler = (
|
|||
e: React.WheelEvent<Element> | WheelEvent
|
||||
) => void
|
||||
|
||||
export type TLDropEventHandler = (e: React.DragEvent<Element>) => void
|
||||
|
||||
export type TLPinchEventHandler = (
|
||||
info: TLPointerInfo<string>,
|
||||
e:
|
||||
|
@ -176,6 +206,8 @@ export interface TLCallbacks<T extends TLShape> {
|
|||
onRightPointCanvas: TLCanvasEventHandler
|
||||
onDragCanvas: TLCanvasEventHandler
|
||||
onReleaseCanvas: TLCanvasEventHandler
|
||||
onDragOver: TLDropEventHandler
|
||||
onDrop: TLDropEventHandler
|
||||
|
||||
// Shape
|
||||
onPointShape: TLPointerEventHandler
|
||||
|
@ -314,6 +346,7 @@ export type Snap =
|
|||
|
||||
export interface IShapeTreeNode<T extends TLShape, M = any> {
|
||||
shape: T
|
||||
asset?: TLAsset
|
||||
children?: IShapeTreeNode<TLShape, M>[]
|
||||
isGhost: boolean
|
||||
isChildOfSelected: boolean
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ContextMenu } from '~components/ContextMenu'
|
|||
import { FocusButton } from '~components/FocusButton'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import { GRID_SIZE } from '~constants'
|
||||
import { Loading } from '~components/Loading'
|
||||
|
||||
export interface TldrawProps extends TDCallbacks {
|
||||
/**
|
||||
|
@ -78,6 +79,14 @@ export interface TldrawProps extends TDCallbacks {
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -142,6 +151,16 @@ export interface TldrawProps extends TDCallbacks {
|
|||
*/
|
||||
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?: (
|
||||
app: TldrawApp,
|
||||
shapes: Record<string, TDShape | undefined>,
|
||||
|
@ -153,7 +172,6 @@ export function Tldraw({
|
|||
id,
|
||||
document,
|
||||
currentPageId,
|
||||
darkMode = false,
|
||||
autofocus = true,
|
||||
showMenu = true,
|
||||
showPages = true,
|
||||
|
@ -163,6 +181,7 @@ export function Tldraw({
|
|||
showUI = true,
|
||||
readOnly = false,
|
||||
showSponsorLink = false,
|
||||
disableAssets = false,
|
||||
onMount,
|
||||
onChange,
|
||||
onChangePresence,
|
||||
|
@ -170,6 +189,7 @@ export function Tldraw({
|
|||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onOpenMedia,
|
||||
onSignOut,
|
||||
onSignIn,
|
||||
onUndo,
|
||||
|
@ -178,13 +198,14 @@ export function Tldraw({
|
|||
onPatch,
|
||||
onCommand,
|
||||
onChangePage,
|
||||
onImageCreate,
|
||||
onImageDelete,
|
||||
}: TldrawProps) {
|
||||
const [sId, setSId] = React.useState(id)
|
||||
|
||||
// Create a new app when the component mounts.
|
||||
const [app, setApp] = React.useState(
|
||||
() =>
|
||||
new TldrawApp(id, {
|
||||
const [app, setApp] = React.useState(() => {
|
||||
const app = new TldrawApp(id, {
|
||||
onMount,
|
||||
onChange,
|
||||
onChangePresence,
|
||||
|
@ -192,6 +213,7 @@ export function Tldraw({
|
|||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onOpenMedia,
|
||||
onSignOut,
|
||||
onSignIn,
|
||||
onUndo,
|
||||
|
@ -200,8 +222,11 @@ export function Tldraw({
|
|||
onPatch,
|
||||
onCommand,
|
||||
onChangePage,
|
||||
onImageDelete,
|
||||
onImageCreate,
|
||||
})
|
||||
return app
|
||||
})
|
||||
)
|
||||
|
||||
// Create a new app if the `id` prop changes.
|
||||
React.useEffect(() => {
|
||||
|
@ -215,6 +240,7 @@ export function Tldraw({
|
|||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onOpenMedia,
|
||||
onSignOut,
|
||||
onSignIn,
|
||||
onUndo,
|
||||
|
@ -223,10 +249,10 @@ export function Tldraw({
|
|||
onPatch,
|
||||
onCommand,
|
||||
onChangePage,
|
||||
onImageDelete,
|
||||
onImageCreate,
|
||||
})
|
||||
|
||||
setSId(id)
|
||||
|
||||
setApp(newApp)
|
||||
}, [sId, id])
|
||||
|
||||
|
@ -234,7 +260,6 @@ export function Tldraw({
|
|||
// are the same, or else load a new document if the ids are different.
|
||||
React.useEffect(() => {
|
||||
if (!document) return
|
||||
|
||||
if (document.id === app.document.id) {
|
||||
app.updateDocument(document)
|
||||
} else {
|
||||
|
@ -242,24 +267,22 @@ export function Tldraw({
|
|||
}
|
||||
}, [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(() => {
|
||||
if (!currentPageId) return
|
||||
app.changePage(currentPageId)
|
||||
}, [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(() => {
|
||||
app.readOnly = 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.
|
||||
React.useEffect(() => {
|
||||
app.callbacks = {
|
||||
|
@ -270,6 +293,7 @@ export function Tldraw({
|
|||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onOpenMedia,
|
||||
onSignOut,
|
||||
onSignIn,
|
||||
onUndo,
|
||||
|
@ -278,6 +302,8 @@ export function Tldraw({
|
|||
onPatch,
|
||||
onCommand,
|
||||
onChangePage,
|
||||
onImageDelete,
|
||||
onImageCreate,
|
||||
}
|
||||
}, [
|
||||
onMount,
|
||||
|
@ -287,6 +313,7 @@ export function Tldraw({
|
|||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onOpenMedia,
|
||||
onSignOut,
|
||||
onSignIn,
|
||||
onUndo,
|
||||
|
@ -295,6 +322,8 @@ export function Tldraw({
|
|||
onPatch,
|
||||
onCommand,
|
||||
onChangePage,
|
||||
onImageDelete,
|
||||
onImageCreate,
|
||||
])
|
||||
|
||||
// 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 pageState = document.pageStates[page.id]
|
||||
const assets = document.assets
|
||||
const { selectedIds } = pageState
|
||||
|
||||
const isHideBoundsShape =
|
||||
|
@ -366,22 +396,6 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
page.shapes[selectedIds[0]] &&
|
||||
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
|
||||
const meta = React.useMemo(() => {
|
||||
return { isDarkMode: settings.isDarkMode }
|
||||
|
@ -414,8 +428,28 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
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 (
|
||||
<StyledLayout ref={rWrapper} tabIndex={-0} className={settings.isDarkMode ? dark : ''}>
|
||||
<Loading />
|
||||
<OneOff focusableRef={rWrapper} autofocus={autofocus} />
|
||||
<ContextMenu onBlur={handleMenuBlur}>
|
||||
<Renderer
|
||||
|
@ -424,6 +458,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
shapeUtils={shapeUtils}
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
assets={assets}
|
||||
snapLines={appState.snapLines}
|
||||
grid={GRID_SIZE}
|
||||
users={room?.users}
|
||||
|
@ -435,7 +470,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
hideResizeHandles={isHideResizeHandlesShape}
|
||||
hideIndicators={hideIndicators}
|
||||
hideBindingHandles={!settings.showBindingHandles}
|
||||
hideCloneHandles={!settings.showCloneHandles}
|
||||
hideCloneHandles={hideCloneHandles}
|
||||
hideRotateHandles={!settings.showRotateHandles}
|
||||
hideGrid={!settings.showGrid}
|
||||
onPinchStart={app.onPinchStart}
|
||||
|
@ -487,6 +522,8 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
onBoundsChange={app.updateBounds}
|
||||
onKeyDown={app.onKeyDown}
|
||||
onKeyUp={app.onKeyUp}
|
||||
onDragOver={app.onDragOver}
|
||||
onDrop={app.onDrop}
|
||||
/>
|
||||
</ContextMenu>
|
||||
{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 { DiscordIcon } from '~components/Primitives/icons'
|
||||
import type { TDSnapshot } from '~types'
|
||||
import { Divider } from '~components/Primitives/Divider'
|
||||
|
||||
interface MenuProps {
|
||||
showSponsorLink: boolean
|
||||
|
@ -26,9 +27,14 @@ const numberOfSelectedIdsSelector = (s: TDSnapshot) => {
|
|||
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) {
|
||||
const app = useTldrawApp()
|
||||
const numberOfSelectedIds = app.useStore(numberOfSelectedIdsSelector)
|
||||
const disableAssets = app.useStore(disableAssetsSelector)
|
||||
|
||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
||||
|
||||
|
@ -64,10 +70,14 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
|
|||
app.selectAll()
|
||||
}, [app])
|
||||
|
||||
const handleselectNone = React.useCallback(() => {
|
||||
const handleSelectNone = React.useCallback(() => {
|
||||
app.selectNone()
|
||||
}, [app])
|
||||
|
||||
const handleUploadMedia = React.useCallback(() => {
|
||||
app.openAsset()
|
||||
}, [app])
|
||||
|
||||
const showFileMenu =
|
||||
app.callbacks.onNewProject ||
|
||||
app.callbacks.onOpenProject ||
|
||||
|
@ -106,6 +116,14 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
|
|||
Save As...
|
||||
</DMItem>
|
||||
)}
|
||||
{!disableAssets && (
|
||||
<>
|
||||
<Divider />
|
||||
<DMItem onClick={handleUploadMedia} kbd="#U">
|
||||
Upload Media
|
||||
</DMItem>
|
||||
</>
|
||||
)}
|
||||
</DMSubMenu>
|
||||
)}
|
||||
{!readOnly && (
|
||||
|
@ -148,7 +166,7 @@ export const Menu = React.memo(function Menu({ showSponsorLink, readOnly }: Menu
|
|||
<DMItem onSelect={preventEvent} onClick={handleSelectAll} kbd="#A">
|
||||
Select All
|
||||
</DMItem>
|
||||
<DMItem onSelect={preventEvent} onClick={handleselectNone}>
|
||||
<DMItem onSelect={preventEvent} onClick={handleSelectNone}>
|
||||
Select None
|
||||
</DMItem>
|
||||
</DMSubMenu>
|
||||
|
|
|
@ -33,7 +33,6 @@ import {
|
|||
TextAlignLeftIcon,
|
||||
TextAlignRightIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { RowButton } from '~components/Primitives/RowButton'
|
||||
|
||||
const currentStyleSelector = (s: TDSnapshot) => s.appState.currentStyle
|
||||
const selectedIdsSelector = (s: TDSnapshot) =>
|
||||
|
@ -290,27 +289,6 @@ const ColorGrid = styled('div', {
|
|||
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', {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const GRID_SIZE = 8
|
||||
export const SVG_EXPORT_PADDING = 16
|
||||
export const BINDING_DISTANCE = 16
|
||||
export const CLONING_DISTANCE = 32
|
||||
export const FIT_TO_SCREEN_PADDING = 128
|
||||
|
@ -79,3 +80,8 @@ export const USER_COLORS = [
|
|||
'#55B467',
|
||||
'#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]
|
||||
)
|
||||
|
||||
const onOpenMedia = React.useCallback(async (app: TldrawApp) => {
|
||||
app.openAsset?.()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
onNewProject,
|
||||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onOpenMedia,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,10 +36,19 @@ export function useFileSystemHandlers() {
|
|||
[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 {
|
||||
onNewProject,
|
||||
onSaveProject,
|
||||
onSaveProjectAs,
|
||||
onOpenProject,
|
||||
onOpenMedia,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
const canHandleEvent = React.useCallback(
|
||||
(ignoreMenus = false) => {
|
||||
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))
|
||||
},
|
||||
[ref]
|
||||
|
@ -155,7 +155,8 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
|
||||
// File System
|
||||
|
||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs, onOpenMedia } =
|
||||
useFileSystemHandlers()
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+n,⌘+n',
|
||||
|
@ -198,6 +199,15 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
undefined,
|
||||
[app]
|
||||
)
|
||||
useHotkeys(
|
||||
'ctrl+u,⌘+u',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
onOpenMedia(e)
|
||||
},
|
||||
undefined,
|
||||
[app]
|
||||
)
|
||||
|
||||
// Undo Redo
|
||||
|
||||
|
|
|
@ -110,8 +110,6 @@ export class StateManager<T extends Record<string, any>> {
|
|||
this._status = 'ready'
|
||||
resolve(message)
|
||||
}
|
||||
|
||||
resolve(message)
|
||||
}).then((message) => {
|
||||
if (this.onReady) this.onReady(message)
|
||||
return message
|
||||
|
|
|
@ -6,7 +6,7 @@ import type { SelectTool } from './tools/SelectTool'
|
|||
describe('TldrawTestApp', () => {
|
||||
describe('When copying and pasting...', () => {
|
||||
it('copies a shape', () => {
|
||||
const app = new TldrawTestApp().loadDocument(mockDocument).selectNone().copy(['rect1'])
|
||||
new TldrawTestApp().loadDocument(mockDocument).selectNone().copy(['rect1'])
|
||||
})
|
||||
|
||||
it('pastes a shape', () => {
|
||||
|
@ -43,6 +43,8 @@ describe('TldrawTestApp', () => {
|
|||
expect(Object.keys(app.page.shapes).length).toBe(1)
|
||||
})
|
||||
|
||||
it.todo('Copies and pastes a shape with an asset')
|
||||
|
||||
it('Copies grouped shapes.', () => {
|
||||
const app = new TldrawTestApp()
|
||||
.loadDocument(mockDocument)
|
||||
|
@ -710,3 +712,13 @@ describe('TldrawTestApp', () => {
|
|||
.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,
|
||||
Utils,
|
||||
TLBounds,
|
||||
TLDropEventHandler,
|
||||
TLAssetType,
|
||||
TLAsset,
|
||||
} from '@tldraw/core'
|
||||
import {
|
||||
FlipType,
|
||||
|
@ -41,6 +44,9 @@ import {
|
|||
loadFileHandle,
|
||||
openFromFileSystem,
|
||||
saveToFileSystem,
|
||||
openAssetFromFileSystem,
|
||||
fileToBase64,
|
||||
getSizeFromDataurl,
|
||||
} from './data'
|
||||
import { TLDR } from './TLDR'
|
||||
import { shapeUtils } from '~state/shapes'
|
||||
|
@ -48,7 +54,14 @@ import { defaultStyle } from '~state/shapes/shared/shape-styles'
|
|||
import * as Commands from './commands'
|
||||
import { SessionArgsOfType, getSession, TldrawSession } from './sessions'
|
||||
import type { BaseTool } from './tools/BaseTool'
|
||||
import { USER_COLORS, FIT_TO_SCREEN_PADDING, 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 { EraseTool } from './tools/EraseTool'
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -128,6 +145,9 @@ export interface TDCallbacks {
|
|||
* (optional) A callback to run when the user creates a new project.
|
||||
*/
|
||||
onChangePresence?: (state: TldrawApp, user: TDUser) => void
|
||||
|
||||
onImageDelete?: (id: string) => void
|
||||
onImageCreate?: (file: File, id: string) => Promise<string>
|
||||
}
|
||||
|
||||
export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
|
@ -194,6 +214,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
clipboard?: {
|
||||
shapes: TDShape[]
|
||||
bindings: TDBinding[]
|
||||
assets: TLAsset[]
|
||||
}
|
||||
|
||||
rotationInfo = {
|
||||
|
@ -262,6 +283,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => {
|
||||
const next = { ...state }
|
||||
|
||||
const assetIdsInUse = new Set<string>([])
|
||||
|
||||
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
|
||||
if (next.document !== prev.document) {
|
||||
Object.entries(next.document.pages).forEach(([pageId, page]) => {
|
||||
|
@ -290,6 +313,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
parentId = prevPage?.shapes[id]?.parentId
|
||||
delete page.shapes[id]
|
||||
} else {
|
||||
if (shape.assetId) assetIdsInUse.add(shape.assetId)
|
||||
parentId = shape.parentId
|
||||
}
|
||||
|
||||
|
@ -407,6 +431,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
})
|
||||
}
|
||||
|
||||
// Cleanup assets
|
||||
|
||||
const currentPageId = next.appState.currentPageId
|
||||
|
||||
const currentPageState = next.document.pageStates[currentPageId]
|
||||
|
@ -973,7 +999,32 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
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.
|
||||
|
@ -1051,6 +1102,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
.clearSelectHistory()
|
||||
.loadDocument(migrate(TldrawApp.defaultDocument, TldrawApp.version))
|
||||
.persist()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1258,6 +1310,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
appState: {
|
||||
...TldrawApp.defaultState.appState,
|
||||
currentPageId: Object.keys(document.pages)[0],
|
||||
disableAssets: this.disableAssets,
|
||||
},
|
||||
},
|
||||
'loaded_document'
|
||||
|
@ -1282,7 +1335,10 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
saveProject = async () => {
|
||||
if (this.readOnly) return
|
||||
try {
|
||||
const fileHandle = await saveToFileSystem(this.document, this.fileSystemHandle)
|
||||
const fileHandle = await saveToFileSystem(
|
||||
migrate(this.document, TldrawApp.version),
|
||||
this.fileSystemHandle
|
||||
)
|
||||
this.fileSystemHandle = fileHandle
|
||||
this.persist()
|
||||
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.
|
||||
* Should move to the www layer.
|
||||
|
@ -1560,28 +1633,29 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
const copyingShapeIds = ids.flatMap((id) =>
|
||||
TLDR.getDocumentBranch(this.state, id, this.currentPageId)
|
||||
)
|
||||
|
||||
const copyingShapes = copyingShapeIds.map((id) =>
|
||||
Utils.deepClone(this.getShape(id, this.currentPageId))
|
||||
)
|
||||
|
||||
if (copyingShapes.length === 0) return this
|
||||
|
||||
const copyingBindings: TDBinding[] = Object.values(this.page.bindings).filter(
|
||||
(binding) =>
|
||||
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 = {
|
||||
shapes: copyingShapes,
|
||||
bindings: copyingBindings,
|
||||
assets: copyingAssets,
|
||||
}
|
||||
|
||||
try {
|
||||
const text = JSON.stringify({
|
||||
type: 'tldr/clipboard',
|
||||
shapes: copyingShapes,
|
||||
bindings: copyingBindings,
|
||||
...this.clipboard,
|
||||
})
|
||||
|
||||
navigator.clipboard.writeText(text).then(
|
||||
|
@ -1595,10 +1669,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
} catch (e) {
|
||||
// Browser does not support copying to clipboard
|
||||
}
|
||||
|
||||
this.pasteInfo.offset = [0, 0]
|
||||
this.pasteInfo.center = [0, 0]
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -1618,35 +1690,35 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
*/
|
||||
paste = (point?: number[]) => {
|
||||
if (this.readOnly) return
|
||||
const pasteInCurrentPage = (shapes: TDShape[], bindings: TDBinding[]) => {
|
||||
const pasteInCurrentPage = (shapes: TDShape[], bindings: TDBinding[], assets: TLAsset[]) => {
|
||||
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()))
|
||||
|
||||
bindings.forEach((binding) => (idsMap[binding.id] = Utils.uniqueId()))
|
||||
|
||||
let startIndex = TLDR.getTopChildIndex(this.state, this.currentPageId)
|
||||
|
||||
const shapesToPaste = shapes
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => {
|
||||
const parentShapeId = idsMap[shape.parentId]
|
||||
|
||||
const copy = {
|
||||
...shape,
|
||||
id: idsMap[shape.id],
|
||||
parentId: parentShapeId || this.currentPageId,
|
||||
}
|
||||
|
||||
if (shape.children) {
|
||||
copy.children = shape.children.map((id) => idsMap[id])
|
||||
}
|
||||
|
||||
if (!parentShapeId) {
|
||||
copy.childIndex = startIndex
|
||||
startIndex++
|
||||
}
|
||||
|
||||
if (copy.handles) {
|
||||
Object.values(copy.handles).forEach((handle) => {
|
||||
if (handle.bindingId) {
|
||||
|
@ -1654,21 +1726,16 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
return copy
|
||||
})
|
||||
|
||||
const bindingsToPaste = bindings.map((binding) => ({
|
||||
...binding,
|
||||
id: idsMap[binding.id],
|
||||
toId: idsMap[binding.toId],
|
||||
fromId: idsMap[binding.fromId],
|
||||
}))
|
||||
|
||||
const commonBounds = Utils.getCommonBounds(shapesToPaste.map(TLDR.getBounds))
|
||||
|
||||
let center = Vec.toFixed(this.getPagePoint(point || this.centerPoint))
|
||||
|
||||
if (
|
||||
Vec.dist(center, this.pasteInfo.center) < 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.offset = [0, 0]
|
||||
}
|
||||
|
||||
const centeredBounds = Utils.centerBounds(commonBounds, center)
|
||||
|
||||
const delta = Vec.sub(
|
||||
Utils.getBoundsCenter(centeredBounds),
|
||||
Utils.getBoundsCenter(commonBounds)
|
||||
)
|
||||
|
||||
this.create(
|
||||
shapesToPaste.map((shape) =>
|
||||
TLDR.getShapeUtil(shape.type).create({
|
||||
|
@ -1705,19 +1769,19 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
navigator.clipboard.readText().then((result) => {
|
||||
try {
|
||||
const data: { type: string; shapes: TDShape[]; bindings: TDBinding[] } =
|
||||
JSON.parse(result)
|
||||
|
||||
const data: {
|
||||
type: string
|
||||
shapes: TDShape[]
|
||||
bindings: TDBinding[]
|
||||
assets: TLAsset[]
|
||||
} = JSON.parse(result)
|
||||
if (data.type !== 'tldr/clipboard') {
|
||||
throw Error('The pasted string was not from the Tldraw clipboard.')
|
||||
}
|
||||
|
||||
pasteInCurrentPage(data.shapes, data.bindings)
|
||||
pasteInCurrentPage(data.shapes, data.bindings, data.assets)
|
||||
} catch (e) {
|
||||
TLDR.warn(e)
|
||||
|
||||
const shapeId = Utils.uniqueId()
|
||||
|
||||
this.createShapes({
|
||||
id: shapeId,
|
||||
type: TDShapeType.Text,
|
||||
|
@ -1726,7 +1790,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
point: this.getPagePoint(this.centerPoint, this.currentPageId),
|
||||
style: { ...this.appState.currentStyle },
|
||||
})
|
||||
|
||||
this.select(shapeId)
|
||||
}
|
||||
})
|
||||
|
@ -1734,7 +1797,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
// Navigator does not support clipboard. Note that this fallback will
|
||||
// not support pasting from one document to another.
|
||||
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) => {
|
||||
if (ids.length === 0) ids = Object.keys(this.page.shapes)
|
||||
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')
|
||||
// Embed our custom fonts
|
||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
|
||||
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');`
|
||||
defs.appendChild(style)
|
||||
svg.appendChild(defs)
|
||||
|
||||
function getSvgElementForShape(shape: TDShape) {
|
||||
// Get the shapes in order
|
||||
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 element = util.getSvgElement(shape)
|
||||
const bounds = util.getBounds(shape)
|
||||
|
||||
if (!element) return
|
||||
|
||||
element.setAttribute(
|
||||
const elm = util.getSvgElement(shape)
|
||||
if (!elm) return
|
||||
// If the element is an image, set the asset src as the xlinkhref
|
||||
if (shape.type === TDShapeType.Image) {
|
||||
elm.setAttribute('xlink:href', this.document.assets[shape.assetId].src)
|
||||
}
|
||||
// Put the element in the correct position relative to the common bounds
|
||||
elm.setAttribute(
|
||||
'transform',
|
||||
`translate(${padding + shape.point[0] - commonBounds.minX}, ${
|
||||
padding + shape.point[1] - commonBounds.minY
|
||||
`translate(${SVG_EXPORT_PADDING + shape.point[0] - commonBounds.minX}, ${
|
||||
SVG_EXPORT_PADDING + shape.point[1] - commonBounds.minY
|
||||
}) rotate(${((shape.rotation || 0) * 180) / Math.PI}, ${bounds.width / 2}, ${
|
||||
bounds.height / 2
|
||||
})`
|
||||
)
|
||||
|
||||
return element
|
||||
return elm
|
||||
}
|
||||
|
||||
// Assemble the final SVG by iterating through each shape and its children
|
||||
shapes.forEach((shape) => {
|
||||
// The shape is a group! Just add the children.
|
||||
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')
|
||||
|
||||
// Get the shape's children as elements
|
||||
shape.children
|
||||
.map((childId) => this.getShape(childId, pageId))
|
||||
.map(getSvgElementForShape)
|
||||
.filter(Boolean)
|
||||
.forEach((element) => g.appendChild(element!))
|
||||
|
||||
// Add the group element to the SVG
|
||||
// Get the shape's children as elms and add them to the group
|
||||
shape.children.forEach((childId) => {
|
||||
const shape = this.getShape(childId, pageId)
|
||||
const elm = getSvgElementForShape(shape)
|
||||
if (elm) g.appendChild(elm)
|
||||
})
|
||||
// Add the group elm to the SVG
|
||||
svg.appendChild(g)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const element = getSvgElementForShape(shape)
|
||||
|
||||
if (element) {
|
||||
svg.appendChild(element)
|
||||
}
|
||||
// Just add the shape's element to the
|
||||
const elm = getSvgElementForShape(shape)
|
||||
if (elm) svg.appendChild(elm)
|
||||
})
|
||||
|
||||
// Resize the element to the bounding box
|
||||
// Resize the elm to the bounding box
|
||||
svg.setAttribute(
|
||||
'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('height', String(commonBounds.height))
|
||||
svg.setAttribute('fill', 'transparent')
|
||||
// Clean up the SVG by removing any hidden elements
|
||||
svg
|
||||
.querySelectorAll('.tl-fill-hitarea, .tl-stroke-hitarea, .tl-binding-indicator')
|
||||
.forEach((element) => element.remove())
|
||||
|
||||
const s = new XMLSerializer()
|
||||
|
||||
const svgString = s
|
||||
.forEach((elm) => elm.remove())
|
||||
// Serialize the SVG to a string
|
||||
const svgString = new XMLSerializer()
|
||||
.serializeToString(svg)
|
||||
.replaceAll(' ', '')
|
||||
.replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1')
|
||||
|
||||
// Copy the string to the clipboard
|
||||
TLDR.copyStringToClipboard(svgString)
|
||||
|
||||
return svgString
|
||||
}
|
||||
|
||||
|
@ -1843,7 +1903,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
copyJson = (ids = this.selectedIds, pageId = this.currentPageId) => {
|
||||
if (ids.length === 0) ids = Object.keys(this.page.shapes)
|
||||
if (ids.length === 0) return
|
||||
|
||||
const shapes = ids.map((id) => this.getShape(id, pageId))
|
||||
const json = JSON.stringify(shapes, null, 2)
|
||||
TLDR.copyStringToClipboard(json)
|
||||
|
@ -1872,7 +1931,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
},
|
||||
reason
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -2405,6 +2463,44 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
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.
|
||||
* @param shapes An array of shapes.
|
||||
|
@ -2431,6 +2527,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
* @command
|
||||
*/
|
||||
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
|
||||
return this.setState(Commands.deleteShapes(this, ids))
|
||||
}
|
||||
|
@ -2701,6 +2805,52 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
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 */
|
||||
/* -------------------------------------------------- */
|
||||
|
@ -2821,6 +2971,20 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
/* ------------- 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)
|
||||
|
||||
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.updateInputs(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) => {
|
||||
|
@ -3181,12 +3360,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
getShapeUtil = TLDR.getShapeUtil
|
||||
|
||||
static version = 14
|
||||
static version = 15
|
||||
|
||||
static defaultDocument: TDDocument = {
|
||||
id: 'doc',
|
||||
name: 'New Document',
|
||||
version: 14,
|
||||
version: 15,
|
||||
pages: {
|
||||
page: {
|
||||
id: 'page',
|
||||
|
@ -3206,6 +3385,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
},
|
||||
},
|
||||
},
|
||||
assets: {},
|
||||
}
|
||||
|
||||
static defaultState: TDSnapshot = {
|
||||
|
@ -3215,6 +3395,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
isZoomSnap: false,
|
||||
isFocusMode: false,
|
||||
isSnapping: false,
|
||||
//@ts-ignore
|
||||
isDebugMode: process.env.NODE_ENV === 'development',
|
||||
isReadonlyMode: false,
|
||||
nudgeDistanceLarge: 16,
|
||||
|
@ -3234,6 +3415,8 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
isMenuOpen: false,
|
||||
isEmptyCanvas: false,
|
||||
snapLines: [],
|
||||
isLoading: false,
|
||||
disableAssets: false,
|
||||
},
|
||||
document: TldrawApp.defaultDocument,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { TDDocument, TDFile } from '~types'
|
||||
import { fileSave, fileOpen, FileSystemHandle } from './browser-fs-access'
|
||||
import { get as getFromIdb, set as setToIdb } from 'idb-keyval'
|
||||
import { IMAGE_EXTENSIONS, VIDEO_EXTENSIONS } from '~constants'
|
||||
|
||||
const options = { mode: 'readwrite' as const }
|
||||
|
||||
|
@ -96,3 +97,31 @@ export async function openFromFileSystem(): Promise<null | {
|
|||
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 {
|
||||
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 < 14) {
|
||||
|
@ -51,6 +68,10 @@ export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
|||
document.name = 'New Document'
|
||||
}
|
||||
|
||||
if (version < 15) {
|
||||
document.assets = {}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Object.values(document.pageStates).forEach((pageState) => {
|
||||
pageState.selectedIds = pageState.selectedIds.filter((id) => {
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
SessionType,
|
||||
ArrowBinding,
|
||||
TldrawPatch,
|
||||
TDShapeType,
|
||||
} from '~types'
|
||||
import { SLOW_SPEED, SNAP_DISTANCE } from '~constants'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
@ -632,6 +633,11 @@ export class TranslateSession extends BaseSession {
|
|||
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)
|
||||
})
|
||||
|
||||
|
|
|
@ -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', {
|
||||
position: 'absolute',
|
||||
top: 'var(--tl-padding)',
|
||||
left: 'var(--tl-padding)',
|
||||
width: 'calc(100% - (var(--tl-padding) * 2))',
|
||||
height: 'calc(100% - (var(--tl-padding) * 2))',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '4px',
|
||||
zIndex: 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 { TextUtil } from './TextUtil'
|
||||
import { DrawUtil } from './DrawUtil'
|
||||
import { ImageUtil } from './ImageUtil'
|
||||
import { TDShape, TDShapeType } from '~types'
|
||||
import { VideoUtil } from './VideoUtil'
|
||||
|
||||
export const Rectangle = new RectangleUtil()
|
||||
export const Triangle = new TriangleUtil()
|
||||
|
@ -17,6 +19,8 @@ export const Arrow = new ArrowUtil()
|
|||
export const Text = new TextUtil()
|
||||
export const Group = new GroupUtil()
|
||||
export const Sticky = new StickyUtil()
|
||||
export const Image = new ImageUtil()
|
||||
export const Video = new VideoUtil()
|
||||
|
||||
export const shapeUtils = {
|
||||
[TDShapeType.Rectangle]: Rectangle,
|
||||
|
@ -27,6 +31,8 @@ export const shapeUtils = {
|
|||
[TDShapeType.Text]: Text,
|
||||
[TDShapeType.Group]: Group,
|
||||
[TDShapeType.Sticky]: Sticky,
|
||||
[TDShapeType.Image]: Image,
|
||||
[TDShapeType.Video]: Video,
|
||||
}
|
||||
|
||||
export const getShapeUtil = <T extends TDShape>(shape: T | T['type']) => {
|
||||
|
|
|
@ -173,7 +173,11 @@ export class SelectTool extends BaseTool<Status> {
|
|||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
onCancel = () => {
|
||||
if (this.app.pageState.editingId) {
|
||||
this.app.setEditingId()
|
||||
} else {
|
||||
this.selectNone()
|
||||
}
|
||||
this.app.cancelSession()
|
||||
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) {
|
||||
this.setStatus(Status.Idle)
|
||||
return
|
||||
|
|
|
@ -64,4 +64,5 @@ export const mockDocument: TDDocument = {
|
|||
},
|
||||
},
|
||||
},
|
||||
assets: {},
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import type {
|
|||
TLBoundsHandleEventHandler,
|
||||
TLShapeBlurHandler,
|
||||
TLShapeCloneHandler,
|
||||
TLAssets,
|
||||
} from '@tldraw/core'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
|
@ -102,6 +103,8 @@ export interface TDSnapshot {
|
|||
isMenuOpen: boolean
|
||||
status: string
|
||||
snapLines: TLSnapLine[]
|
||||
isLoading: boolean
|
||||
disableAssets: boolean
|
||||
}
|
||||
document: TDDocument
|
||||
room?: {
|
||||
|
@ -130,6 +133,7 @@ export interface TDDocument {
|
|||
version: number
|
||||
pages: Record<string, TDPage>
|
||||
pageStates: Record<string, TLPageState>
|
||||
assets: TLAssets
|
||||
}
|
||||
|
||||
// The shape of a single page in the Tldraw document
|
||||
|
@ -277,6 +281,8 @@ export enum TDShapeType {
|
|||
Line = 'line',
|
||||
Text = 'text',
|
||||
Group = 'group',
|
||||
Image = 'image',
|
||||
Video = 'video',
|
||||
}
|
||||
|
||||
export enum Decoration {
|
||||
|
@ -338,6 +344,20 @@ export interface RectangleShape extends TDBaseShape {
|
|||
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
|
||||
export interface TriangleShape extends TDBaseShape {
|
||||
type: TDShapeType.Triangle
|
||||
|
@ -374,6 +394,8 @@ export type TDShape =
|
|||
| TextShape
|
||||
| GroupShape
|
||||
| StickyShape
|
||||
| ImageShape
|
||||
| VideoShape
|
||||
|
||||
/* ------------------ Shape Styles ------------------ */
|
||||
|
||||
|
|
615
yarn.lock
615
yarn.lock
|
@ -1077,6 +1077,385 @@
|
|||
unique-filename "^1.1.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":
|
||||
version "5.0.2"
|
||||
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"
|
||||
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":
|
||||
version "0.1.0"
|
||||
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"
|
||||
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":
|
||||
version "3.0.5"
|
||||
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"
|
||||
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":
|
||||
version "14.18.0"
|
||||
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"
|
||||
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:
|
||||
version "2.6.12"
|
||||
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"
|
||||
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:
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05"
|
||||
|
@ -7047,6 +7489,13 @@ fastq@^1.6.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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:
|
||||
version "3.0.4"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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:
|
||||
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:
|
||||
version "6.1.5"
|
||||
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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
|
||||
|
@ -9663,6 +10164,13 @@ lie@3.1.1:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.2.4"
|
||||
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"
|
||||
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:
|
||||
version "4.5.0"
|
||||
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"
|
||||
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:
|
||||
version "1.4.0"
|
||||
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"
|
||||
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:
|
||||
version "2.6.6"
|
||||
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"
|
||||
semver "^6.2.0"
|
||||
|
||||
pako@~1.0.5:
|
||||
pako@~1.0.2, pako@~1.0.5:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||
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"
|
||||
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:
|
||||
version "1.1.1"
|
||||
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"
|
||||
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:
|
||||
version "1.4.8"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
@ -12734,6 +13283,25 @@ seek-bzip@^1.0.5:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.0.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
@ -14535,6 +15108,20 @@ webpack-sources@^1.4.3:
|
|||
source-list-map "^2.0.0"
|
||||
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:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
|
||||
|
@ -14542,6 +15129,11 @@ whatwg-encoding@^1.0.5:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.3.0"
|
||||
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"
|
||||
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:
|
||||
version "7.5.6"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
|
||||
|
|
Loading…
Reference in a new issue