kopia lustrzana https://github.com/Tldraw/Tldraw
559 wiersze
17 KiB
TypeScript
559 wiersze
17 KiB
TypeScript
import {
|
|
TLBookmarkShape,
|
|
TLEmbedShape,
|
|
TLFrameShape,
|
|
TLPageId,
|
|
useEditor,
|
|
useValue,
|
|
} from '@tldraw/editor'
|
|
import { getEmbedInfo } from '../../utils/embeds/embeds'
|
|
import { useActions } from '../context/actions'
|
|
import { useUiEvents } from '../context/events'
|
|
import { useToasts } from '../context/toasts'
|
|
import {
|
|
showMenuPaste,
|
|
useAllowGroup,
|
|
useAllowUngroup,
|
|
useAnySelectedShapesCount,
|
|
useHasLinkShapeSelected,
|
|
useOnlyFlippableShape,
|
|
useShowAutoSizeToggle,
|
|
useThreeStackableItems,
|
|
useUnlockedSelectedShapesCount,
|
|
} from '../hooks/menu-hooks'
|
|
import { TldrawUiMenuCheckboxItem } from './primitives/menus/TldrawUiMenuCheckboxItem'
|
|
import { TldrawUiMenuGroup } from './primitives/menus/TldrawUiMenuGroup'
|
|
import { TldrawUiMenuItem } from './primitives/menus/TldrawUiMenuItem'
|
|
import { TldrawUiMenuSubmenu } from './primitives/menus/TldrawUiMenuSubmenu'
|
|
|
|
/* -------------------- Selection ------------------- */
|
|
/** @public */
|
|
export function ToggleAutoSizeMenuItem() {
|
|
const actions = useActions()
|
|
const shouldDisplay = useShowAutoSizeToggle()
|
|
|
|
return <TldrawUiMenuItem {...actions['toggle-auto-size']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function EditLinkMenuItem() {
|
|
const actions = useActions()
|
|
const shouldDisplay = useHasLinkShapeSelected()
|
|
|
|
return <TldrawUiMenuItem {...actions['edit-link']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function DuplicateMenuItem() {
|
|
const actions = useActions()
|
|
const shouldDisplay = useUnlockedSelectedShapesCount(1)
|
|
|
|
return <TldrawUiMenuItem {...actions['duplicate']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function GroupMenuItem() {
|
|
const actions = useActions()
|
|
const shouldDisplay = useAllowGroup()
|
|
|
|
return <TldrawUiMenuItem {...actions['group']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function UngroupMenuItem() {
|
|
const actions = useActions()
|
|
const shouldDisplay = useAllowUngroup()
|
|
|
|
return <TldrawUiMenuItem {...actions['ungroup']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function RemoveFrameMenuItem() {
|
|
const editor = useEditor()
|
|
const actions = useActions()
|
|
const shouldDisplay = useValue(
|
|
'allow unframe',
|
|
() => {
|
|
const selectedShapes = editor.getSelectedShapes()
|
|
if (selectedShapes.length === 0) return false
|
|
return selectedShapes.every((shape) => editor.isShapeOfType<TLFrameShape>(shape, 'frame'))
|
|
},
|
|
[editor]
|
|
)
|
|
|
|
return <TldrawUiMenuItem {...actions['remove-frame']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function FitFrameToContentMenuItem() {
|
|
const editor = useEditor()
|
|
const actions = useActions()
|
|
const shouldDisplay = useValue(
|
|
'allow fit frame to content',
|
|
() => {
|
|
const onlySelectedShape = editor.getOnlySelectedShape()
|
|
if (!onlySelectedShape) return false
|
|
return (
|
|
editor.isShapeOfType<TLFrameShape>(onlySelectedShape, 'frame') &&
|
|
editor.getSortedChildIdsForParent(onlySelectedShape).length > 0
|
|
)
|
|
},
|
|
[editor]
|
|
)
|
|
|
|
return <TldrawUiMenuItem {...actions['fit-frame-to-content']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function ToggleLockMenuItem() {
|
|
const editor = useEditor()
|
|
const actions = useActions()
|
|
const shouldDisplay = useValue('selected shapes', () => editor.getSelectedShapes().length > 0, [
|
|
editor,
|
|
])
|
|
|
|
return <TldrawUiMenuItem {...actions['toggle-lock']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function ToggleTransparentBgMenuItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const isTransparentBg = useValue(
|
|
'isTransparentBg',
|
|
() => !editor.getInstanceState().exportBackground,
|
|
[editor]
|
|
)
|
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-transparent']} checked={isTransparentBg} />
|
|
}
|
|
/** @public */
|
|
export function UnlockAllMenuItem() {
|
|
const editor = useEditor()
|
|
const actions = useActions()
|
|
const shouldDisplay = useValue('any shapes', () => editor.getCurrentPageShapeIds().size > 0, [
|
|
editor,
|
|
])
|
|
|
|
return <TldrawUiMenuItem {...actions['unlock-all']} disabled={!shouldDisplay} />
|
|
}
|
|
|
|
/* ---------------------- Zoom ---------------------- */
|
|
/** @public */
|
|
export function ZoomTo100MenuItem() {
|
|
const editor = useEditor()
|
|
const isZoomedTo100 = useValue('zoomed to 100', () => editor.getZoomLevel() === 1, [editor])
|
|
const actions = useActions()
|
|
|
|
return <TldrawUiMenuItem {...actions['zoom-to-100']} noClose disabled={isZoomedTo100} />
|
|
}
|
|
/** @public */
|
|
export function ZoomToFitMenuItem() {
|
|
const editor = useEditor()
|
|
const hasShapes = useValue('has shapes', () => editor.getCurrentPageShapeIds().size > 0, [editor])
|
|
const actions = useActions()
|
|
|
|
return (
|
|
<TldrawUiMenuItem
|
|
{...actions['zoom-to-fit']}
|
|
disabled={!hasShapes}
|
|
data-testid="minimap.zoom-menu.zoom-to-fit"
|
|
noClose
|
|
/>
|
|
)
|
|
}
|
|
/** @public */
|
|
export function ZoomToSelectionMenuItem() {
|
|
const editor = useEditor()
|
|
const hasSelected = useValue('has shapes', () => editor.getSelectedShapeIds().length > 0, [
|
|
editor,
|
|
])
|
|
const actions = useActions()
|
|
|
|
return (
|
|
<TldrawUiMenuItem
|
|
{...actions['zoom-to-selection']}
|
|
disabled={!hasSelected}
|
|
data-testid="minimap.zoom-menu.zoom-to-selection"
|
|
noClose
|
|
/>
|
|
)
|
|
}
|
|
|
|
/* -------------------- Clipboard ------------------- */
|
|
/** @public */
|
|
export function ClipboardMenuGroup() {
|
|
const editor = useEditor()
|
|
const actions = useActions()
|
|
const atLeastOneShapeOnPage = useValue(
|
|
'atLeastOneShapeOnPage',
|
|
() => editor.getCurrentPageShapeIds().size > 0,
|
|
[]
|
|
)
|
|
|
|
return (
|
|
<TldrawUiMenuGroup id="clipboard">
|
|
<CutMenuItem />
|
|
<CopyMenuItem />
|
|
<TldrawUiMenuSubmenu
|
|
id="copy-as"
|
|
label="context-menu.copy-as"
|
|
size="small"
|
|
disabled={!atLeastOneShapeOnPage}
|
|
>
|
|
<TldrawUiMenuGroup id="copy-as-group">
|
|
<TldrawUiMenuItem {...actions['copy-as-svg']} />
|
|
{Boolean(window.navigator.clipboard?.write) && (
|
|
<TldrawUiMenuItem {...actions['copy-as-png']} />
|
|
)}
|
|
<TldrawUiMenuItem {...actions['copy-as-json']} />
|
|
</TldrawUiMenuGroup>
|
|
<TldrawUiMenuGroup id="copy-as-bg">
|
|
<ToggleTransparentBgMenuItem />
|
|
</TldrawUiMenuGroup>
|
|
</TldrawUiMenuSubmenu>
|
|
<DuplicateMenuItem />
|
|
<PasteMenuItem />
|
|
<DeleteMenuItem />
|
|
</TldrawUiMenuGroup>
|
|
)
|
|
}
|
|
/** @public */
|
|
export function CutMenuItem() {
|
|
const actions = useActions()
|
|
const shouldDisplay = useUnlockedSelectedShapesCount(1)
|
|
|
|
return <TldrawUiMenuItem {...actions['cut']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function CopyMenuItem() {
|
|
const actions = useActions()
|
|
const shouldDisplay = useAnySelectedShapesCount(1)
|
|
|
|
return <TldrawUiMenuItem {...actions['copy']} disabled={!shouldDisplay} />
|
|
}
|
|
/** @public */
|
|
export function PasteMenuItem() {
|
|
const actions = useActions()
|
|
const shouldDisplay = showMenuPaste
|
|
|
|
return <TldrawUiMenuItem {...actions['paste']} disabled={!shouldDisplay} />
|
|
}
|
|
|
|
/* ------------------- Conversions ------------------ */
|
|
/** @public */
|
|
export function ConversionsMenuGroup() {
|
|
const actions = useActions()
|
|
const shouldDisplay = useUnlockedSelectedShapesCount(1)
|
|
|
|
return (
|
|
<TldrawUiMenuGroup id="conversions">
|
|
<TldrawUiMenuSubmenu
|
|
id="export-as"
|
|
label="context-menu.export-as"
|
|
size="small"
|
|
disabled={!shouldDisplay}
|
|
>
|
|
<TldrawUiMenuGroup id="export-as-group">
|
|
<TldrawUiMenuItem {...actions['export-as-svg']} />
|
|
<TldrawUiMenuItem {...actions['export-as-png']} />
|
|
<TldrawUiMenuItem {...actions['export-as-json']} />
|
|
</TldrawUiMenuGroup>
|
|
<TldrawUiMenuGroup id="export-as-bg">
|
|
<ToggleTransparentBgMenuItem />
|
|
</TldrawUiMenuGroup>
|
|
</TldrawUiMenuSubmenu>
|
|
</TldrawUiMenuGroup>
|
|
)
|
|
}
|
|
|
|
/* ------------------ Set Selection ----------------- */
|
|
/** @public */
|
|
export function SetSelectionGroup() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const atLeastOneShapeOnPage = useValue(
|
|
'atLeastOneShapeOnPage',
|
|
() => editor.getCurrentPageShapeIds().size > 0,
|
|
[editor]
|
|
)
|
|
|
|
return (
|
|
<TldrawUiMenuGroup id="set-selection-group">
|
|
<TldrawUiMenuItem {...actions['select-all']} disabled={!atLeastOneShapeOnPage} />
|
|
</TldrawUiMenuGroup>
|
|
)
|
|
}
|
|
|
|
/* ------------------ Delete Group ------------------ */
|
|
/** @public */
|
|
export function DeleteMenuItem() {
|
|
const actions = useActions()
|
|
const oneSelected = useUnlockedSelectedShapesCount(1)
|
|
|
|
return <TldrawUiMenuItem {...actions['delete']} disabled={!oneSelected} />
|
|
}
|
|
|
|
/* --------------------- Modify --------------------- */
|
|
/** @public */
|
|
export function ArrangeMenuSubmenu() {
|
|
const twoSelected = useUnlockedSelectedShapesCount(2)
|
|
const onlyFlippableShapeSelected = useOnlyFlippableShape()
|
|
const actions = useActions()
|
|
|
|
if (!(twoSelected || onlyFlippableShapeSelected)) return null
|
|
|
|
return (
|
|
<TldrawUiMenuSubmenu id="arrange" label="context-menu.arrange" size="small">
|
|
{twoSelected && (
|
|
<TldrawUiMenuGroup id="align">
|
|
<TldrawUiMenuItem {...actions['align-left']} />
|
|
<TldrawUiMenuItem {...actions['align-center-horizontal']} />
|
|
<TldrawUiMenuItem {...actions['align-right']} />
|
|
<TldrawUiMenuItem {...actions['align-top']} />
|
|
<TldrawUiMenuItem {...actions['align-center-vertical']} />
|
|
<TldrawUiMenuItem {...actions['align-bottom']} />
|
|
</TldrawUiMenuGroup>
|
|
)}
|
|
<DistributeMenuGroup />
|
|
{twoSelected && (
|
|
<TldrawUiMenuGroup id="stretch">
|
|
<TldrawUiMenuItem {...actions['stretch-horizontal']} />
|
|
<TldrawUiMenuItem {...actions['stretch-vertical']} />
|
|
</TldrawUiMenuGroup>
|
|
)}
|
|
{onlyFlippableShapeSelected && (
|
|
<TldrawUiMenuGroup id="flip">
|
|
<TldrawUiMenuItem {...actions['flip-horizontal']} />
|
|
<TldrawUiMenuItem {...actions['flip-vertical']} />
|
|
</TldrawUiMenuGroup>
|
|
)}
|
|
<OrderMenuGroup />
|
|
</TldrawUiMenuSubmenu>
|
|
)
|
|
}
|
|
|
|
function DistributeMenuGroup() {
|
|
const actions = useActions()
|
|
const threeSelected = useUnlockedSelectedShapesCount(3)
|
|
if (!threeSelected) return null
|
|
|
|
return (
|
|
<TldrawUiMenuGroup id="distribute">
|
|
<TldrawUiMenuItem {...actions['distribute-horizontal']} />
|
|
<TldrawUiMenuItem {...actions['distribute-vertical']} />
|
|
</TldrawUiMenuGroup>
|
|
)
|
|
}
|
|
|
|
function OrderMenuGroup() {
|
|
const actions = useActions()
|
|
const twoSelected = useUnlockedSelectedShapesCount(2)
|
|
const threeStackableItems = useThreeStackableItems()
|
|
if (!twoSelected) return null
|
|
|
|
return (
|
|
<TldrawUiMenuGroup id="order">
|
|
<TldrawUiMenuItem {...actions['pack']} />
|
|
{threeStackableItems && <TldrawUiMenuItem {...actions['stack-horizontal']} />}
|
|
{threeStackableItems && <TldrawUiMenuItem {...actions['stack-vertical']} />}
|
|
</TldrawUiMenuGroup>
|
|
)
|
|
}
|
|
/** @public */
|
|
export function ReorderMenuSubmenu() {
|
|
const actions = useActions()
|
|
const oneSelected = useUnlockedSelectedShapesCount(1)
|
|
if (!oneSelected) return null
|
|
|
|
return (
|
|
<TldrawUiMenuSubmenu id="reorder" label="context-menu.reorder" size="small">
|
|
<TldrawUiMenuGroup id="reorder">
|
|
<TldrawUiMenuItem {...actions['bring-to-front']} />
|
|
<TldrawUiMenuItem {...actions['bring-forward']} />
|
|
<TldrawUiMenuItem {...actions['send-backward']} />
|
|
<TldrawUiMenuItem {...actions['send-to-back']} />
|
|
</TldrawUiMenuGroup>
|
|
</TldrawUiMenuSubmenu>
|
|
)
|
|
}
|
|
/** @public */
|
|
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 actions = useActions()
|
|
const trackEvent = useUiEvents()
|
|
|
|
const oneSelected = useUnlockedSelectedShapesCount(1)
|
|
if (!oneSelected) return null
|
|
|
|
return (
|
|
<TldrawUiMenuSubmenu id="move-to-page" label="context-menu.move-to-page" size="small">
|
|
<TldrawUiMenuGroup id="pages">
|
|
{pages.map((page) => (
|
|
<TldrawUiMenuItem
|
|
id={page.id}
|
|
key={page.id}
|
|
disabled={currentPageId === page.id}
|
|
label={page.name}
|
|
onSelect={() => {
|
|
editor.mark('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.mark('change-page')
|
|
editor.setCurrentPage(currentPageId)
|
|
},
|
|
},
|
|
],
|
|
})
|
|
}
|
|
trackEvent('move-to-page', { source: 'context-menu' })
|
|
}}
|
|
title={page.name}
|
|
/>
|
|
))}
|
|
</TldrawUiMenuGroup>
|
|
<TldrawUiMenuGroup id="new-page">
|
|
<TldrawUiMenuItem {...actions['new-page']} />
|
|
</TldrawUiMenuGroup>
|
|
</TldrawUiMenuSubmenu>
|
|
)
|
|
}
|
|
/** @public */
|
|
export function EmbedsGroup() {
|
|
const editor = useEditor()
|
|
const actions = useActions()
|
|
|
|
const oneEmbedSelected = useValue(
|
|
'oneEmbedSelected',
|
|
() => {
|
|
const onlySelectedShape = editor.getOnlySelectedShape()
|
|
if (!onlySelectedShape) return false
|
|
return !!(
|
|
editor.isShapeOfType<TLEmbedShape>(onlySelectedShape, 'embed') &&
|
|
onlySelectedShape.props.url &&
|
|
!editor.isShapeOrAncestorLocked(onlySelectedShape)
|
|
)
|
|
},
|
|
[editor]
|
|
)
|
|
|
|
const oneEmbeddableBookmarkSelected = useValue(
|
|
'oneEmbeddableBookmarkSelected',
|
|
() => {
|
|
const onlySelectedShape = editor.getOnlySelectedShape()
|
|
if (!onlySelectedShape) return false
|
|
return !!(
|
|
editor.isShapeOfType<TLBookmarkShape>(onlySelectedShape, 'bookmark') &&
|
|
onlySelectedShape.props.url &&
|
|
getEmbedInfo(onlySelectedShape.props.url) &&
|
|
!editor.isShapeOrAncestorLocked(onlySelectedShape)
|
|
)
|
|
},
|
|
[editor]
|
|
)
|
|
|
|
return (
|
|
<TldrawUiMenuGroup id="embeds">
|
|
{/* XXX this doesn't exist?? */}
|
|
{/* <TldrawUiMenuItem {...actions['edit-embed']} disabled={!oneEmbedSelected} /> */}
|
|
<TldrawUiMenuItem {...actions['convert-to-bookmark']} disabled={!oneEmbedSelected} />
|
|
<TldrawUiMenuItem
|
|
{...actions['convert-to-embed']}
|
|
disabled={!oneEmbeddableBookmarkSelected}
|
|
/>
|
|
</TldrawUiMenuGroup>
|
|
)
|
|
}
|
|
|
|
/* ------------------- Preferences ------------------ */
|
|
/** @public */
|
|
export function ToggleSnapModeItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const isSnapMode = useValue('isSnapMode', () => editor.user.getIsSnapMode(), [editor])
|
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-snap-mode']} checked={isSnapMode} />
|
|
}
|
|
/** @public */
|
|
export function ToggleToolLockItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const isToolLock = useValue('isToolLock', () => editor.getInstanceState().isToolLocked, [editor])
|
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-tool-lock']} checked={isToolLock} />
|
|
}
|
|
/** @public */
|
|
export function ToggleGridItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const isGridMode = useValue('isGridMode', () => editor.getInstanceState().isGridMode, [editor])
|
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-grid']} checked={isGridMode} />
|
|
}
|
|
|
|
/** @public */
|
|
export function ToggleWrapModeItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const isWrapMode = useValue('isWrapMode', () => editor.user.getIsWrapMode(), [editor])
|
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-wrap-mode']} checked={isWrapMode} />
|
|
}
|
|
|
|
/** @public */
|
|
export function ToggleDarkModeItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const isDarkMode = useValue('isDarkMode', () => editor.user.getIsDarkMode(), [editor])
|
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-dark-mode']} checked={isDarkMode} />
|
|
}
|
|
/** @public */
|
|
export function ToggleFocusModeItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const isFocusMode = useValue('isFocusMode', () => editor.getInstanceState().isFocusMode, [editor])
|
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-focus-mode']} checked={isFocusMode} />
|
|
}
|
|
/** @public */
|
|
export function ToggleEdgeScrollingItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const edgeScrollSpeed = useValue('edgeScrollSpeed', () => editor.user.getEdgeScrollSpeed(), [
|
|
editor,
|
|
])
|
|
return (
|
|
<TldrawUiMenuCheckboxItem
|
|
{...actions['toggle-edge-scrolling']}
|
|
checked={edgeScrollSpeed === 1}
|
|
/>
|
|
)
|
|
}
|
|
/** @public */
|
|
export function ToggleReduceMotionItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const animationSpeed = useValue('animationSpeed', () => editor.user.getAnimationSpeed(), [editor])
|
|
return (
|
|
<TldrawUiMenuCheckboxItem {...actions['toggle-reduce-motion']} checked={animationSpeed === 0} />
|
|
)
|
|
}
|
|
/** @public */
|
|
export function ToggleDebugModeItem() {
|
|
const actions = useActions()
|
|
const editor = useEditor()
|
|
const isDebugMode = useValue('isDebugMode', () => editor.getInstanceState().isDebugMode, [editor])
|
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-debug-mode']} checked={isDebugMode} />
|
|
}
|
|
|
|
/* ---------------------- Print --------------------- */
|
|
/** @public */
|
|
export function PrintItem() {
|
|
const editor = useEditor()
|
|
const actions = useActions()
|
|
const emptyPage = useValue('emptyPage', () => editor.getCurrentPageShapeIds().size === 0, [
|
|
editor,
|
|
])
|
|
return <TldrawUiMenuItem {...actions['print']} disabled={emptyPage} />
|
|
}
|