add alert dialog for alert in share button
This commit is contained in:
parent
e1861122f6
commit
c8b839b43e
6 changed files with 257 additions and 118 deletions
|
@ -59,6 +59,12 @@ export interface TldrawProps extends TDCallbacks {
|
|||
* (optional) Whether to show the multiplayer menu.
|
||||
*/
|
||||
showMultiplayerMenu?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Whether to show the share menu.
|
||||
*/
|
||||
showShareMenu?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Whether to show the pages UI.
|
||||
*/
|
||||
|
@ -114,6 +120,7 @@ export function Tldraw({
|
|||
autofocus = true,
|
||||
showMenu = true,
|
||||
showMultiplayerMenu = true,
|
||||
showShareMenu = true,
|
||||
showPages = true,
|
||||
showTools = true,
|
||||
showZoom = true,
|
||||
|
@ -217,6 +224,34 @@ export function Tldraw({
|
|||
setApp(newApp)
|
||||
}, [sId, id])
|
||||
|
||||
// In dev, we need to delete the prefixed
|
||||
const entry =
|
||||
window.location.port === '5420'
|
||||
? window.location.hash.replace('#/develop/', '')
|
||||
: window.location.search
|
||||
const urlSearchParams = new URLSearchParams(entry)
|
||||
|
||||
const encodedPage = urlSearchParams.get('d')
|
||||
|
||||
const decodedPage = JSONCrush.uncrush((encodedPage as string) ?? '')
|
||||
|
||||
React.useEffect(() => {
|
||||
if (decodedPage.length === 0) return
|
||||
const state = JSON.parse(decodedPage) as Record<string, any>
|
||||
if (Object.keys(state).length) {
|
||||
if ('page' in state) {
|
||||
app.loadDocumentFromURL(state.page, state.pageState)
|
||||
} else {
|
||||
const nextDocument = state as TDDocument
|
||||
if (nextDocument.id === app.document.id) {
|
||||
app.updateDocument(nextDocument)
|
||||
} else {
|
||||
app.loadDocument(nextDocument)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [app, decodedPage])
|
||||
|
||||
// Update the document if the `document` prop changes but the ids,
|
||||
// are the same, or else load a new document if the ids are different.
|
||||
React.useEffect(() => {
|
||||
|
@ -317,30 +352,6 @@ export function Tldraw({
|
|||
}
|
||||
}, [app])
|
||||
|
||||
// In dev, we need to delete the prefixed
|
||||
const entry =
|
||||
window.location.port === '5420'
|
||||
? window.location.hash.replace('#/develop/', '')
|
||||
: window.location.search
|
||||
const urlSearchParams = new URLSearchParams(entry)
|
||||
|
||||
const encodedPage = urlSearchParams.get('d')
|
||||
|
||||
const decodedPage = JSONCrush.uncrush((encodedPage as string) ?? '')
|
||||
|
||||
React.useEffect(() => {
|
||||
if (decodedPage.length) {
|
||||
const state = JSON.parse(decodedPage) as Record<string, any>
|
||||
if (Object.keys(state).length) {
|
||||
if ('page' in state) {
|
||||
app.loadDocumentFromURL(state.page, state.pageState)
|
||||
} else {
|
||||
app.loadDocument(state as TDDocument)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [app, decodedPage])
|
||||
|
||||
// Use the `key` to ensure that new selector hooks are made when the id changes
|
||||
return (
|
||||
<TldrawContext.Provider value={app}>
|
||||
|
@ -354,6 +365,7 @@ export function Tldraw({
|
|||
showPages={showPages}
|
||||
showMenu={showMenu}
|
||||
showMultiplayerMenu={showMultiplayerMenu}
|
||||
showShareMenu={showShareMenu}
|
||||
showStyles={showStyles}
|
||||
showZoom={showZoom}
|
||||
showTools={showTools}
|
||||
|
@ -376,6 +388,7 @@ interface InnerTldrawProps {
|
|||
showStyles: boolean
|
||||
showUI: boolean
|
||||
showTools: boolean
|
||||
showShareMenu: boolean
|
||||
}
|
||||
|
||||
const InnerTldraw = React.memo(function InnerTldraw({
|
||||
|
@ -384,6 +397,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
showPages,
|
||||
showMenu,
|
||||
showMultiplayerMenu,
|
||||
showShareMenu,
|
||||
showZoom,
|
||||
showStyles,
|
||||
showTools,
|
||||
|
@ -586,6 +600,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
showPages={showPages}
|
||||
showMenu={showMenu}
|
||||
showMultiplayerMenu={showMultiplayerMenu}
|
||||
showShareMenu={showShareMenu}
|
||||
showStyles={showStyles}
|
||||
showZoom={showZoom}
|
||||
/>
|
||||
|
|
113
packages/tldraw/src/components/Primitives/AlertDialog/Alert.tsx
Normal file
113
packages/tldraw/src/components/Primitives/AlertDialog/Alert.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||
import * as React from 'react'
|
||||
import { styled } from '~styles'
|
||||
|
||||
interface ContentProps {
|
||||
children: React.ReactNode
|
||||
onClose?: () => void
|
||||
container: any
|
||||
}
|
||||
|
||||
function Content({ children, onClose, container }: ContentProps) {
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
onClose?.()
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal container={container}>
|
||||
<StyledOverlay />
|
||||
<StyledContent onKeyDown={handleKeyDown}>{children}</StyledContent>
|
||||
</AlertDialogPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledDescription = styled(AlertDialogPrimitive.Description, {
|
||||
marginBottom: 20,
|
||||
color: '$text',
|
||||
fontSize: '$2',
|
||||
lineHeight: 1.5,
|
||||
textAlign: 'center',
|
||||
minWidth: 0,
|
||||
alignSelf: 'center',
|
||||
})
|
||||
|
||||
const AlertDialogRoot = AlertDialogPrimitive.Root
|
||||
const AlertDialogContent = Content
|
||||
const AlertDialogDescription = StyledDescription
|
||||
const AlertDialogAction = AlertDialogPrimitive.Action
|
||||
|
||||
interface AlertProps {
|
||||
container: any
|
||||
description: string
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const Alert = ({ container, description, open, onClose }: AlertProps) => {
|
||||
return (
|
||||
<AlertDialogRoot open={open}>
|
||||
<AlertDialogContent onClose={onClose} container={container}>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
gap: '$6',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<AlertDialogAction asChild>
|
||||
<Button css={{ backgroundColor: '#2F80ED', color: 'White' }} onClick={onClose}>
|
||||
Ok
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogRoot>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, .15)',
|
||||
pointerEvents: 'all',
|
||||
})
|
||||
|
||||
const StyledContent = styled(AlertDialogPrimitive.Content, {
|
||||
position: 'fixed',
|
||||
font: '$ui',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 'max-content',
|
||||
padding: '$3',
|
||||
pointerEvents: 'all',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '$3',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
fontFamily: '$ui',
|
||||
border: '1px solid $panelContrast',
|
||||
boxShadow: '$panel',
|
||||
})
|
||||
|
||||
const Button = styled('button', {
|
||||
all: 'unset',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '$2',
|
||||
padding: '0 15px',
|
||||
fontSize: '$1',
|
||||
lineHeight: 1,
|
||||
fontWeight: 'normal',
|
||||
height: 36,
|
||||
color: '$text',
|
||||
cursor: 'pointer',
|
||||
minWidth: 48,
|
||||
})
|
|
@ -1 +1,2 @@
|
|||
export * from './AlertDialog'
|
||||
export * from './Alert'
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { ClipboardIcon, Share1Icon } from '@radix-ui/react-icons'
|
||||
import JSONCrush from 'jsoncrush'
|
||||
import * as React from 'react'
|
||||
import { FormattedMessage, useIntl } from 'react-intl'
|
||||
import { Alert } from '~components/Primitives/AlertDialog'
|
||||
import { DMContent, DMItem, DMTriggerIcon } from '~components/Primitives/DropdownMenu'
|
||||
import { SmallIcon } from '~components/Primitives/SmallIcon'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
|
||||
const ShareMenu = () => {
|
||||
const app = useTldrawApp()
|
||||
const intl = useIntl()
|
||||
const currentPageId = app.appState.currentPageId
|
||||
const pageDocument = app.document.pages[currentPageId]
|
||||
const pageState = app.document.pageStates[currentPageId]
|
||||
const [container, setContainer] = React.useState<any>(null)
|
||||
const [openDialog, setOpenDialog] = React.useState(false)
|
||||
|
||||
const toggleOpenDialog = () => setOpenDialog(!openDialog)
|
||||
|
||||
const copyCurrentPageLink = () => {
|
||||
const hasAsset = Object.entries(pageDocument.shapes).filter(
|
||||
([_, value]) => value.assetId
|
||||
).length
|
||||
if (hasAsset) {
|
||||
toggleOpenDialog()
|
||||
} else {
|
||||
try {
|
||||
const state = {
|
||||
page: {
|
||||
...pageDocument,
|
||||
},
|
||||
pageState: {
|
||||
...pageState,
|
||||
},
|
||||
}
|
||||
const crushed = JSONCrush.crush(JSON.stringify(state))
|
||||
const link = `${window.location.href}/?d=${encodeURIComponent(crushed)}`
|
||||
navigator.clipboard.writeText(link)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const copyProjectLink = () => {
|
||||
if (Object.keys(app.document.assets).length) {
|
||||
toggleOpenDialog()
|
||||
} else {
|
||||
try {
|
||||
const crushed = JSONCrush.crush(JSON.stringify(app.document))
|
||||
const link = `${window.location.href}/?d=${encodeURIComponent(crushed)}`
|
||||
navigator.clipboard.writeText(link)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon id="TD-MultiplayerMenuIcon">
|
||||
<Share1Icon />
|
||||
</DMTriggerIcon>
|
||||
<DMContent
|
||||
variant="menu"
|
||||
id="TD-MultiplayerMenu"
|
||||
side="bottom"
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DMItem id="TD-Multiplayer-CopyInviteLink" onClick={copyCurrentPageLink}>
|
||||
<FormattedMessage id="copy.current.page.link" />
|
||||
<SmallIcon>
|
||||
<ClipboardIcon />
|
||||
</SmallIcon>
|
||||
</DMItem>
|
||||
<DMItem id="TD-Multiplayer-CopyReadOnlyLink" onClick={copyProjectLink}>
|
||||
<FormattedMessage id="copy.project.link" />
|
||||
<SmallIcon>
|
||||
<ClipboardIcon />
|
||||
</SmallIcon>
|
||||
</DMItem>
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
<div ref={setContainer} />
|
||||
<Alert
|
||||
container={container}
|
||||
description="Data is too big to be encoded into an URL"
|
||||
open={openDialog}
|
||||
onClose={toggleOpenDialog}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShareMenu
|
|
@ -1,11 +1,5 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { ClipboardIcon } from '@radix-ui/react-icons'
|
||||
import JSONCrush from 'jsoncrush'
|
||||
import * as React from 'react'
|
||||
import { FormattedMessage, useIntl } from 'react-intl'
|
||||
import { DMContent, DMItem } from '~components/Primitives/DropdownMenu'
|
||||
import { Panel } from '~components/Primitives/Panel'
|
||||
import { SmallIcon } from '~components/Primitives/SmallIcon'
|
||||
import { ToolButton } from '~components/Primitives/ToolButton'
|
||||
import { UndoIcon } from '~components/Primitives/icons'
|
||||
import { useTldrawApp } from '~hooks'
|
||||
|
@ -13,6 +7,7 @@ import { styled } from '~styles'
|
|||
import { Menu } from './Menu/Menu'
|
||||
import { MultiplayerMenu } from './MultiplayerMenu'
|
||||
import { PageMenu } from './PageMenu'
|
||||
import ShareMenu from './ShareMenu/ShareMenu'
|
||||
import { StyleMenu } from './StyleMenu'
|
||||
import { ZoomMenu } from './ZoomMenu'
|
||||
|
||||
|
@ -23,6 +18,7 @@ interface TopPanelProps {
|
|||
showStyles: boolean
|
||||
showZoom: boolean
|
||||
showMultiplayerMenu: boolean
|
||||
showShareMenu: boolean
|
||||
}
|
||||
|
||||
export function TopPanel({
|
||||
|
@ -32,6 +28,7 @@ export function TopPanel({
|
|||
showStyles,
|
||||
showZoom,
|
||||
showMultiplayerMenu,
|
||||
showShareMenu,
|
||||
}: TopPanelProps) {
|
||||
const app = useTldrawApp()
|
||||
|
||||
|
@ -41,13 +38,13 @@ export function TopPanel({
|
|||
<Panel side="left" id="TD-MenuPanel">
|
||||
{showMenu && <Menu readOnly={readOnly} />}
|
||||
{showMultiplayerMenu && <MultiplayerMenu />}
|
||||
{showShareMenu && <ShareMenu />}
|
||||
{showPages && <PageMenu />}
|
||||
</Panel>
|
||||
)}
|
||||
<StyledSpacer />
|
||||
{(showStyles || showZoom) && (
|
||||
<Panel side="right">
|
||||
<ShareMenu />
|
||||
{app.readOnly ? (
|
||||
<ReadOnlyLabel>Read Only</ReadOnlyLabel>
|
||||
) : (
|
||||
|
@ -98,90 +95,3 @@ const ReadOnlyLabel = styled('div', {
|
|||
paddingRight: '$1',
|
||||
userSelect: 'none',
|
||||
})
|
||||
|
||||
const ShareButton = styled(DropdownMenu.Trigger, {
|
||||
all: 'unset',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '$2',
|
||||
padding: '0 15px',
|
||||
fontSize: '$1',
|
||||
lineHeight: 1,
|
||||
fontWeight: 'normal',
|
||||
height: 36,
|
||||
cursor: 'pointer',
|
||||
minWidth: 48,
|
||||
backgroundColor: '#2F80ED',
|
||||
color: 'White',
|
||||
marginTop: 2,
|
||||
})
|
||||
|
||||
const ShareMenu = () => {
|
||||
const app = useTldrawApp()
|
||||
const intl = useIntl()
|
||||
const currentPageId = app.appState.currentPageId
|
||||
const pageDocument = app.document.pages[currentPageId]
|
||||
const pageState = app.document.pageStates[currentPageId]
|
||||
|
||||
const copyCurrentPageLink = () => {
|
||||
const hasAsset = Object.entries(pageDocument.shapes).filter(
|
||||
([_, value]) => value.assetId
|
||||
).length
|
||||
if (hasAsset) {
|
||||
alert(intl.formatMessage({ id: 'data.too.big.encoded' }))
|
||||
} else {
|
||||
try {
|
||||
const state = {
|
||||
page: {
|
||||
...pageDocument,
|
||||
},
|
||||
pageState: {
|
||||
...pageState,
|
||||
},
|
||||
}
|
||||
const crushed = JSONCrush.crush(JSON.stringify(state))
|
||||
const link = `${window.location.href}/?d=${encodeURIComponent(crushed)}`
|
||||
navigator.clipboard.writeText(link)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const copyProjectLink = () => {
|
||||
if (Object.keys(app.document.assets).length) {
|
||||
alert(intl.formatMessage({ id: 'data.too.big.encoded' }))
|
||||
} else {
|
||||
try {
|
||||
const crushed = JSONCrush.crush(JSON.stringify(app.document))
|
||||
const link = `${window.location.href}/?d=${encodeURIComponent(crushed)}`
|
||||
navigator.clipboard.writeText(link)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<ShareButton id="TD-MultiplayerMenuIcon">
|
||||
<FormattedMessage id="share" />
|
||||
</ShareButton>
|
||||
<DMContent variant="menu" id="TD-MultiplayerMenu" side="bottom" align="start" sideOffset={4}>
|
||||
<DMItem id="TD-Multiplayer-CopyInviteLink" onClick={copyCurrentPageLink}>
|
||||
<FormattedMessage id="copy.current.page.link" />
|
||||
<SmallIcon>
|
||||
<ClipboardIcon />
|
||||
</SmallIcon>
|
||||
</DMItem>
|
||||
<DMItem id="TD-Multiplayer-CopyReadOnlyLink" onClick={copyProjectLink}>
|
||||
<FormattedMessage id="copy.project.link" />
|
||||
<SmallIcon>
|
||||
<ClipboardIcon />
|
||||
</SmallIcon>
|
||||
</DMItem>
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1367,6 +1367,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
this.resetHistory()
|
||||
this.clearSelectHistory()
|
||||
this.session = undefined
|
||||
// this set it and set it back to the default document
|
||||
|
||||
const state = {
|
||||
...TldrawApp.defaultState,
|
||||
|
|
Loading…
Reference in a new issue