diff --git a/packages/tldraw/src/Tldraw.tsx b/packages/tldraw/src/Tldraw.tsx index b2532cdee..89a095952 100644 --- a/packages/tldraw/src/Tldraw.tsx +++ b/packages/tldraw/src/Tldraw.tsx @@ -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 + 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 - 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 ( @@ -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} /> diff --git a/packages/tldraw/src/components/Primitives/AlertDialog/Alert.tsx b/packages/tldraw/src/components/Primitives/AlertDialog/Alert.tsx new file mode 100644 index 000000000..064bce86f --- /dev/null +++ b/packages/tldraw/src/components/Primitives/AlertDialog/Alert.tsx @@ -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 ( + + + {children} + + ) +} + +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 ( + + + {description} +
+ + + +
+
+
+ ) +} + +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, +}) diff --git a/packages/tldraw/src/components/Primitives/AlertDialog/index.ts b/packages/tldraw/src/components/Primitives/AlertDialog/index.ts index 67bb5e913..697b6b489 100644 --- a/packages/tldraw/src/components/Primitives/AlertDialog/index.ts +++ b/packages/tldraw/src/components/Primitives/AlertDialog/index.ts @@ -1 +1,2 @@ export * from './AlertDialog' +export * from './Alert' diff --git a/packages/tldraw/src/components/TopPanel/ShareMenu/ShareMenu.tsx b/packages/tldraw/src/components/TopPanel/ShareMenu/ShareMenu.tsx new file mode 100644 index 000000000..406df6527 --- /dev/null +++ b/packages/tldraw/src/components/TopPanel/ShareMenu/ShareMenu.tsx @@ -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(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 ( + <> + + + + + + + + + + + + + + + + + + + +
+ + + ) +} + +export default ShareMenu diff --git a/packages/tldraw/src/components/TopPanel/TopPanel.tsx b/packages/tldraw/src/components/TopPanel/TopPanel.tsx index e797af2b0..a1767986e 100644 --- a/packages/tldraw/src/components/TopPanel/TopPanel.tsx +++ b/packages/tldraw/src/components/TopPanel/TopPanel.tsx @@ -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({ {showMenu && } {showMultiplayerMenu && } + {showShareMenu && } {showPages && } )} {(showStyles || showZoom) && ( - {app.readOnly ? ( Read Only ) : ( @@ -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 ( - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/packages/tldraw/src/state/TldrawApp.ts b/packages/tldraw/src/state/TldrawApp.ts index c68bcc553..ec1bf94f1 100644 --- a/packages/tldraw/src/state/TldrawApp.ts +++ b/packages/tldraw/src/state/TldrawApp.ts @@ -1367,6 +1367,7 @@ export class TldrawApp extends StateManager { this.resetHistory() this.clearSelectHistory() this.session = undefined + // this set it and set it back to the default document const state = { ...TldrawApp.defaultState,