import * as React from 'react' import { styled } from '~styles' import * as RadixContextMenu from '@radix-ui/react-context-menu' import { useTldrawApp } from '~hooks' import { TDSnapshot, AlignType, DistributeType, StretchType, TDExportType } from '~types' import { AlignBottomIcon, AlignCenterHorizontallyIcon, AlignCenterVerticallyIcon, AlignLeftIcon, AlignRightIcon, AlignTopIcon, SpaceEvenlyHorizontallyIcon, SpaceEvenlyVerticallyIcon, StretchHorizontallyIcon, StretchVerticallyIcon, } from '@radix-ui/react-icons' import { Divider } from '~components/Primitives/Divider' import { MenuContent } from '~components/Primitives/MenuContent' import { RowButton, RowButtonProps } from '~components/Primitives/RowButton' import { ToolButton, ToolButtonProps } from '~components/Primitives/ToolButton' import { FormattedMessage, useIntl } from 'react-intl' const numberOfSelectedIdsSelector = (s: TDSnapshot) => { return s.document.pageStates[s.appState.currentPageId].selectedIds.length } const isDebugModeSelector = (s: TDSnapshot) => { return s.settings.isDebugMode } const hasGroupSelectedSelector = (s: TDSnapshot) => { return s.document.pageStates[s.appState.currentPageId].selectedIds.some( (id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined ) } const preventDefault = (e: Event) => e.stopPropagation() interface ContextMenuProps { onBlur?: React.FocusEventHandler children: React.ReactNode } export const ContextMenu = ({ onBlur, children }: ContextMenuProps) => { return ( {children} ) } interface InnerContextMenuProps { onBlur?: React.FocusEventHandler } const InnerMenu = React.memo(function InnerMenu({ onBlur }: InnerContextMenuProps) { const app = useTldrawApp() const intl = useIntl() const numberOfSelectedIds = app.useStore(numberOfSelectedIdsSelector) const isDebugMode = app.useStore(isDebugModeSelector) const hasGroupSelected = app.useStore(hasGroupSelectedSelector) const rContent = React.useRef(null) const handleFlipHorizontal = React.useCallback(() => { app.flipHorizontal() }, [app]) const handleFlipVertical = React.useCallback(() => { app.flipVertical() }, [app]) const handleDuplicate = React.useCallback(() => { app.duplicate() }, [app]) const handleLock = React.useCallback(() => { app.toggleLocked() }, [app]) const handleGroup = React.useCallback(() => { app.group() }, [app]) const handleMoveToBack = React.useCallback(() => { app.moveToBack() }, [app]) const handleMoveBackward = React.useCallback(() => { app.moveBackward() }, [app]) const handleMoveForward = React.useCallback(() => { app.moveForward() }, [app]) const handleMoveToFront = React.useCallback(() => { app.moveToFront() }, [app]) const handleDelete = React.useCallback(() => { app.delete() }, [app]) const handleCut = React.useCallback(() => { app.cut() }, [app]) const handleCopy = React.useCallback(() => { app.copy() }, [app]) const handlePaste = React.useCallback(() => { app.paste() }, [app]) const handleCopySVG = React.useCallback(() => { app.copyImage(TDExportType.SVG, { scale: 1, quality: 1, transparentBackground: false }) }, [app]) const handleCopyPNG = React.useCallback(() => { app.copyImage(TDExportType.PNG, { scale: 2, quality: 1, transparentBackground: true }) }, [app]) const handleUndo = React.useCallback(() => { app.undo() }, [app]) const handleRedo = React.useCallback(() => { app.redo() }, [app]) const handleExportPNG = React.useCallback(async () => { app.exportImage(TDExportType.PNG, { scale: 2, quality: 1, transparentBackground: true }) }, [app]) const handleExportJPG = React.useCallback(async () => { app.exportImage(TDExportType.JPG, { scale: 2, quality: 1, transparentBackground: false }) }, [app]) const handleExportWEBP = React.useCallback(async () => { app.exportImage(TDExportType.WEBP, { scale: 2, quality: 1, transparentBackground: false }) }, [app]) const handleExportSVG = React.useCallback(async () => { app.exportImage(TDExportType.SVG, { scale: 1, quality: 1, transparentBackground: false }) }, [app]) const handleCopyJSON = React.useCallback(async () => { app.copyJson() }, [app]) const handleExportJSON = React.useCallback(async () => { app.exportJson() }, [app]) const hasSelection = numberOfSelectedIds > 0 const hasTwoOrMore = numberOfSelectedIds > 1 const hasThreeOrMore = numberOfSelectedIds > 2 return ( {hasSelection ? ( <> / {(hasTwoOrMore || hasGroupSelected) && } {hasTwoOrMore && ( )} {hasGroupSelected && ( )} {hasTwoOrMore && ( )} SVG PNG {isDebugMode && ( JSON )} SVG PNG JPG WEBP {isDebugMode && ( JSON )} ) : ( <> )} ) }) /* ---------- Align and Distribute Sub Menu --------- */ function AlignDistributeSubMenu({ hasThreeOrMore, }: { hasTwoOrMore: boolean hasThreeOrMore: boolean }) { const app = useTldrawApp() const alignTop = React.useCallback(() => { app.align(AlignType.Top) }, [app]) const alignCenterVertical = React.useCallback(() => { app.align(AlignType.CenterVertical) }, [app]) const alignBottom = React.useCallback(() => { app.align(AlignType.Bottom) }, [app]) const stretchVertically = React.useCallback(() => { app.stretch(StretchType.Vertical) }, [app]) const distributeVertically = React.useCallback(() => { app.distribute(DistributeType.Vertical) }, [app]) const alignLeft = React.useCallback(() => { app.align(AlignType.Left) }, [app]) const alignCenterHorizontal = React.useCallback(() => { app.align(AlignType.CenterHorizontal) }, [app]) const alignRight = React.useCallback(() => { app.align(AlignType.Right) }, [app]) const stretchHorizontally = React.useCallback(() => { app.stretch(StretchType.Horizontal) }, [app]) const distributeHorizontally = React.useCallback(() => { app.distribute(DistributeType.Horizontal) }, [app]) return ( Align / Distribute {hasThreeOrMore && ( )} {hasThreeOrMore && ( )} ) } const StyledGridContent = styled(MenuContent, { display: 'grid', variants: { numberOfSelected: { threeOrMore: { gridTemplateColumns: 'repeat(5, auto)', }, twoOrMore: { gridTemplateColumns: 'repeat(4, auto)', }, }, }, }) /* -------------- Move to Page Sub Menu ------------- */ const currentPageIdSelector = (s: TDSnapshot) => s.appState.currentPageId const documentPagesSelector = (s: TDSnapshot) => s.document.pages function MoveToPageMenu() { const app = useTldrawApp() const currentPageId = app.useStore(currentPageIdSelector) const documentPages = app.useStore(documentPagesSelector) const sorted = Object.values(documentPages) .sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0)) .filter((a) => a.id !== currentPageId) if (sorted.length === 0) return null return ( {sorted.map(({ id, name }, i) => ( app.moveToPage(id)} > {name || `Page ${i}`} ))} ) } /* --------------------- Submenu -------------------- */ export interface ContextMenuSubMenuProps { label: string size?: 'small' children: React.ReactNode id?: string } export function ContextMenuSubMenu({ children, label, size, id }: ContextMenuSubMenuProps) { return ( {label} {children} ) } /* ---------------------- Arrow --------------------- */ const CMArrow = styled(RadixContextMenu.ContextMenuArrow, { fill: '$panel', }) /* ------------------- IconButton ------------------- */ function CMIconButton({ onSelect, ...rest }: ToolButtonProps) { return ( ) } /* -------------------- RowButton ------------------- */ const CMRowButton = ({ id, ...rest }: RowButtonProps) => { return ( ) } /* ----------------- Trigger Button ----------------- */ interface CMTriggerButtonProps extends RowButtonProps { isSubmenu?: boolean } export const CMTriggerButton = ({ isSubmenu, ...rest }: CMTriggerButtonProps) => { return ( ) }