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
}