import { TLPageId, useEditor, useValue } from '@tldraw/editor' import { supportsDownloadingOriginal } from '../context/actions' import { useUiEvents } from '../context/events' import { useToasts } from '../context/toasts' import { showMenuPaste, useAllowGroup, useAllowUngroup, useAnySelectedShapesCount, useCanApplySelectionAction, useHasLinkShapeSelected, useHasShapesOnPage, useOnlyFlippableShape, useShowAutoSizeToggle, useThreeStackableItems, useUnlockedSelectedShapesCount, } from '../hooks/menu-hooks' import { useGetEmbedDefinition } from '../hooks/useGetEmbedDefinition' import { useReadonly } from '../hooks/useReadonly' import { TldrawUiMenuActionCheckboxItem } from './primitives/menus/TldrawUiMenuActionCheckboxItem' import { TldrawUiMenuActionItem } from './primitives/menus/TldrawUiMenuActionItem' import { TldrawUiMenuGroup } from './primitives/menus/TldrawUiMenuGroup' import { TldrawUiMenuItem } from './primitives/menus/TldrawUiMenuItem' import { TldrawUiMenuSubmenu } from './primitives/menus/TldrawUiMenuSubmenu' /* -------------------- Selection ------------------- */ /** @public @react */ export function ToggleAutoSizeMenuItem() { const shouldDisplay = useShowAutoSizeToggle() if (!shouldDisplay) return null return } /** @public @react */ export function EditLinkMenuItem() { const shouldDisplay = useHasLinkShapeSelected() if (!shouldDisplay) return null return } /** @public @react */ export function DuplicateMenuItem() { const shouldDisplay = useUnlockedSelectedShapesCount(1) if (!shouldDisplay) return null return } /** @public @react */ export function FlattenMenuItem() { const editor = useEditor() const shouldDisplay = useValue( 'should display flatten option', () => { const selectedShapeIds = editor.getSelectedShapeIds() if (selectedShapeIds.length === 0) return false const onlySelectedShape = editor.getOnlySelectedShape() if (onlySelectedShape && editor.isShapeOfType(onlySelectedShape, 'image')) { return false } return true }, [editor] ) if (!shouldDisplay) return null return } /** @public @react */ export function DownloadOriginalMenuItem() { const editor = useEditor() const shouldDisplay = useValue( 'should display download original option', () => { const selectedShapes = editor.getSelectedShapes() if (selectedShapes.length === 0) return false return selectedShapes.some((shape) => supportsDownloadingOriginal(shape, editor)) }, [editor] ) if (!shouldDisplay) return null return } /** @public @react */ export function GroupMenuItem() { const shouldDisplay = useAllowGroup() if (!shouldDisplay) return null return } /** @public @react */ export function UngroupMenuItem() { const shouldDisplay = useAllowUngroup() if (!shouldDisplay) return null return } /** @public @react */ export function RemoveFrameMenuItem() { const editor = useEditor() const shouldDisplay = useValue( 'allow unframe', () => { const selectedShapes = editor.getSelectedShapes() if (selectedShapes.length === 0) return false return selectedShapes.every((shape) => editor.isShapeFrameLike(shape)) }, [editor] ) if (!shouldDisplay) return null return } /** @public @react */ export function FitFrameToContentMenuItem() { const editor = useEditor() const shouldDisplay = useValue( 'allow fit frame to content', () => { const onlySelectedShape = editor.getOnlySelectedShape() if (!onlySelectedShape) return false return ( editor.isShapeFrameLike(onlySelectedShape) && editor.getSortedChildIdsForParent(onlySelectedShape).length > 0 ) }, [editor] ) if (!shouldDisplay) return null return } /** @public @react */ export function ToggleLockMenuItem() { const shouldDisplay = useAnySelectedShapesCount(1) if (!shouldDisplay) return null return } /** @public @react */ export function ToggleTransparentBgMenuItem() { const editor = useEditor() const isTransparentBg = useValue( 'isTransparentBg', () => !editor.getInstanceState().exportBackground, [editor] ) return ( ) } /** @public @react */ export function UnlockAllMenuItem() { const hasShapes = useHasShapesOnPage() return } /* ---------------------- Zoom ---------------------- */ /** @public @react */ export function ZoomTo100MenuItem() { const editor = useEditor() const isZoomedTo100 = useValue('zoomed to 100', () => editor.getEfficientZoomLevel() === 1, [ editor, ]) return } /** @public @react */ export function ZoomToFitMenuItem() { const hasShapes = useHasShapesOnPage() return ( ) } /** @public @react */ export function ZoomToSelectionMenuItem() { const canApplySelectionAction = useCanApplySelectionAction() return ( ) } /* -------------------- Clipboard ------------------- */ /** @public @react */ export function ClipboardMenuGroup() { return ( ) } /** @public @react */ export function CopyAsMenuGroup() { const editor = useEditor() const atLeastOneShapeOnPage = useHasShapesOnPage() const isDebugMode = useValue('isDebugMode', () => editor.getInstanceState().isDebugMode, [editor]) return ( {Boolean(editor.getContainerWindow().navigator.clipboard?.write) && ( )} {isDebugMode && } ) } /** @public @react */ export function CutMenuItem() { const canApplySelectionAction = useCanApplySelectionAction() const hasUnlockedShapes = useUnlockedSelectedShapesCount(1) return ( ) } /** @public @react */ export function CopyMenuItem() { const canApplySelectionAction = useCanApplySelectionAction() return } /** @public @react */ export function PasteMenuItem() { const shouldDisplay = showMenuPaste return } /* ------------------- Conversions ------------------ */ /** @public @react */ export function ConversionsMenuGroup() { const atLeastOneShapeOnPage = useHasShapesOnPage() if (!atLeastOneShapeOnPage) return null return ( ) } /* ------------------ Set Selection ----------------- */ /** @public @react */ export function SelectAllMenuItem() { const atLeastOneShapeOnPage = useHasShapesOnPage() return } /* ------------------ Delete Group ------------------ */ /** @public @react */ export function DeleteMenuItem() { const canApplySelectionAction = useCanApplySelectionAction() const hasUnlockedShapes = useUnlockedSelectedShapesCount(1) return ( ) } /* --------------------- Modify --------------------- */ /** @public @react */ export function EditMenuSubmenu() { const isReadonlyMode = useReadonly() if (!useAnySelectedShapesCount(1)) return null if (isReadonlyMode) return null return ( ) } /** @public @react */ export function ArrangeMenuSubmenu() { const twoSelected = useUnlockedSelectedShapesCount(2) const onlyFlippableShapeSelected = useOnlyFlippableShape() const isReadonlyMode = useReadonly() if (isReadonlyMode) return null if (!(twoSelected || onlyFlippableShapeSelected)) return null return ( {twoSelected && ( )} {twoSelected && ( )} {(twoSelected || onlyFlippableShapeSelected) && ( )} ) } function DistributeMenuGroup() { const threeSelected = useUnlockedSelectedShapesCount(3) if (!threeSelected) return null return ( ) } function OrderMenuGroup() { const twoSelected = useUnlockedSelectedShapesCount(2) const threeStackableItems = useThreeStackableItems() if (!twoSelected) return null return ( {threeStackableItems && } {threeStackableItems && } ) } /** @public @react */ export function ReorderMenuSubmenu() { const isReadonlyMode = useReadonly() const oneSelected = useUnlockedSelectedShapesCount(1) if (isReadonlyMode) return null if (!oneSelected) return null return ( ) } /** @public @react */ export function MoveToPageMenu() { const editor = useEditor() const pages = useValue('pages', () => editor.getPages(), [editor]) const currentPageId = useValue('current page id', () => editor.getCurrentPageId(), [editor]) const { addToast } = useToasts() const trackEvent = useUiEvents() const isReadonlyMode = useReadonly() const oneSelected = useUnlockedSelectedShapesCount(1) if (!oneSelected) return null if (isReadonlyMode) return null return ( {pages.map((page) => ( 30 ? `${page.name.slice(0, 30)}…` : page.name} onSelect={() => { editor.markHistoryStoppingPoint('move_shapes_to_page') editor.moveShapesToPage(editor.getSelectedShapeIds(), page.id as TLPageId) const toPage = editor.getPage(page.id) if (toPage) { addToast({ title: 'Changed page', description: `Moved to ${toPage.name}.`, actions: [ { label: 'Go back', type: 'primary', onClick: () => { editor.markHistoryStoppingPoint('change-page') editor.setCurrentPage(currentPageId) }, }, ], }) } trackEvent('move-to-page', { source: 'context-menu' }) }} /> ))} ) } /** @public @react */ export function ConvertToBookmarkMenuItem() { const editor = useEditor() const oneEmbedSelected = useValue( 'oneEmbedSelected', () => { const onlySelectedShape = editor.getOnlySelectedShape() if (!onlySelectedShape) return false return !!( editor.isShapeOfType(onlySelectedShape, 'embed') && onlySelectedShape.props.url && !editor.isShapeOrAncestorLocked(onlySelectedShape) ) }, [editor] ) if (!oneEmbedSelected) return null return } /** @public @react */ export function ConvertToEmbedMenuItem() { const editor = useEditor() const getEmbedDefinition = useGetEmbedDefinition() const oneEmbeddableBookmarkSelected = useValue( 'oneEmbeddableBookmarkSelected', () => { const onlySelectedShape = editor.getOnlySelectedShape() if (!onlySelectedShape) return false return !!( editor.isShapeOfType(onlySelectedShape, 'bookmark') && onlySelectedShape.props.url && getEmbedDefinition(onlySelectedShape.props.url) && !editor.isShapeOrAncestorLocked(onlySelectedShape) ) }, [editor] ) if (!oneEmbeddableBookmarkSelected) return null return } /* ------------------- Preferences ------------------ */ /** @public @react */ export function ToggleSnapModeItem() { const editor = useEditor() const isSnapMode = useValue('isSnapMode', () => editor.user.getIsSnapMode(), [editor]) return } /** @public @react */ export function ToggleToolLockItem() { const editor = useEditor() const isToolLock = useValue('isToolLock', () => editor.getInstanceState().isToolLocked, [editor]) return } /** @public @react */ export function ToggleGridItem() { const editor = useEditor() const isGridMode = useValue('isGridMode', () => editor.getInstanceState().isGridMode, [editor]) return } /** @public @react */ export function ToggleWrapModeItem() { const editor = useEditor() const isWrapMode = useValue('isWrapMode', () => editor.user.getIsWrapMode(), [editor]) return } /** @public @react */ export function ToggleDarkModeItem() { const editor = useEditor() const isDarkMode = useValue('isDarkMode', () => editor.user.getIsDarkMode(), [editor]) return } /** @public @react */ export function ToggleFocusModeItem() { const editor = useEditor() const isFocusMode = useValue('isFocusMode', () => editor.getInstanceState().isFocusMode, [editor]) return } /** @public @react */ export function ToggleEdgeScrollingItem() { const editor = useEditor() const edgeScrollSpeed = useValue('edgeScrollSpeed', () => editor.user.getEdgeScrollSpeed(), [ editor, ]) return ( ) } /** @public @react */ export function ToggleInvertZoomItem() { const editor = useEditor() const isMouseInputMode = useValue( 'inputMode', () => editor.user.getUserPreferences().inputMode === 'mouse', [editor] ) const isZoomDirectionInverted = useValue( 'isZoomDirectionInverted', () => editor.user.getIsZoomDirectionInverted(), [editor] ) return ( ) } /** @public @react */ export function ToggleReduceMotionItem() { const editor = useEditor() const animationSpeed = useValue('animationSpeed', () => editor.user.getAnimationSpeed(), [editor]) return ( ) } /** @public @react */ export function ToggleKeyboardShortcutsItem() { const editor = useEditor() const keyboardShortcuts = useValue( 'keyboardShortcuts', () => editor.user.getAreKeyboardShortcutsEnabled(), [editor] ) return ( ) } /** @public @react */ export function ToggleEnhancedA11yModeItem() { const editor = useEditor() const enhancedA11yMode = useValue('enhancedA11yMode', () => editor.user.getEnhancedA11yMode(), [ editor, ]) return } /** @public @react */ export function ToggleDebugModeItem() { const editor = useEditor() const isDebugMode = useValue('isDebugMode', () => editor.getInstanceState().isDebugMode, [editor]) return } /** @public @react */ export function ToggleDynamicSizeModeItem() { const editor = useEditor() const isDynamicResizeMode = useValue( 'dynamic resize', () => editor.user.getIsDynamicResizeMode(), [editor] ) return ( ) } /** @public @react */ export function TogglePasteAtCursorItem() { const editor = useEditor() const pasteAtCursor = useValue('paste at cursor', () => editor.user.getIsPasteAtCursorMode(), [ editor, ]) return ( ) } /* ---------------------- Print --------------------- */ /** @public @react */ export function PrintItem() { const hasShapes = useHasShapesOnPage() return } /* ---------------------- Multiplayer --------------------- */ /** @public @react */ export function CursorChatItem() { const editor = useEditor() const shouldShow = useValue( 'show cursor chat', () => editor.getCurrentToolId() === 'select' && !editor.getInstanceState().isCoarsePointer, [editor] ) if (!shouldShow) return null return }