tldraw/components/canvas/context-menu/context-menu.tsx

378 lines
11 KiB
TypeScript
Raw Normal View History

2021-06-10 09:49:16 +00:00
import * as _ContextMenu from '@radix-ui/react-context-menu'
import styled from 'styles'
2021-06-17 12:03:08 +00:00
import {
IconWrapper,
2021-06-29 12:00:59 +00:00
breakpoints,
2021-07-10 16:14:49 +00:00
RowButton,
ContextMenuArrow,
ContextMenuDivider,
ContextMenuButton,
ContextMenuSubMenu,
ContextMenuIconButton,
ContextMenuRoot,
MenuContent,
2021-06-17 12:03:08 +00:00
} from 'components/shared'
2021-06-29 12:00:59 +00:00
import { commandKey, deepCompareArrays, isMobile } from 'utils'
2021-06-10 09:49:16 +00:00
import state, { useSelector } from 'state'
2021-06-17 12:03:08 +00:00
import {
AlignType,
DistributeType,
MoveType,
ShapeType,
StretchType,
} from 'types'
2021-06-29 12:00:59 +00:00
import tld from 'utils/tld'
2021-06-10 09:49:16 +00:00
import React, { useRef } from 'react'
2021-06-17 12:03:08 +00:00
import {
ChevronRightIcon,
AlignBottomIcon,
AlignCenterHorizontallyIcon,
AlignCenterVerticallyIcon,
AlignLeftIcon,
AlignRightIcon,
AlignTopIcon,
SpaceEvenlyHorizontallyIcon,
SpaceEvenlyVerticallyIcon,
StretchHorizontallyIcon,
StretchVerticallyIcon,
} from '@radix-ui/react-icons'
function alignTop() {
state.send('ALIGNED', { type: AlignType.Top })
}
function alignCenterVertical() {
state.send('ALIGNED', { type: AlignType.CenterVertical })
}
function alignBottom() {
state.send('ALIGNED', { type: AlignType.Bottom })
}
function stretchVertically() {
state.send('STRETCHED', { type: StretchType.Vertical })
}
function distributeVertically() {
state.send('DISTRIBUTED', { type: DistributeType.Vertical })
}
function alignLeft() {
state.send('ALIGNED', { type: AlignType.Left })
}
function alignCenterHorizontal() {
state.send('ALIGNED', { type: AlignType.CenterHorizontal })
}
function alignRight() {
state.send('ALIGNED', { type: AlignType.Right })
}
function stretchHorizontally() {
state.send('STRETCHED', { type: StretchType.Horizontal })
}
function distributeHorizontally() {
state.send('DISTRIBUTED', { type: DistributeType.Horizontal })
}
2021-06-10 09:49:16 +00:00
export default function ContextMenu({
children,
}: {
children: React.ReactNode
2021-06-21 21:35:28 +00:00
}): JSX.Element {
const selectedShapeIds = useSelector(
(s) => s.values.selectedIds,
2021-06-10 09:49:16 +00:00
deepCompareArrays
)
const rContent = useRef<HTMLDivElement>(null)
const hasGroupSelected = useSelector((s) =>
2021-06-29 12:00:59 +00:00
selectedShapeIds.some(
2021-06-30 22:44:25 +00:00
(id) => tld.getShape(s.data, id)?.type === ShapeType.Group
2021-06-29 12:00:59 +00:00
)
)
const hasTwoOrMore = selectedShapeIds.length > 1
const hasThreeOrMore = selectedShapeIds.length > 2
2021-06-10 09:49:16 +00:00
return (
2021-07-10 16:14:49 +00:00
<ContextMenuRoot>
2021-06-10 09:49:16 +00:00
<_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
2021-07-10 16:14:49 +00:00
<MenuContent
as={_ContextMenu.Content}
ref={rContent}
isMobile={isMobile()}
>
{selectedShapeIds.length ? (
2021-06-10 09:49:16 +00:00
<>
2021-07-10 16:14:49 +00:00
{/* <ContextMenuButton onSelect={() => state.send('COPIED')}>
2021-06-10 09:49:16 +00:00
<span>Copy</span>
<kbd>
<span>{commandKey()}</span>
<span>C</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
<ContextMenuButton onSelect={() => state.send('CUT')}>
2021-06-10 09:49:16 +00:00
<span>Cut</span>
<kbd>
<span>{commandKey()}</span>
<span>X</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
2021-06-10 09:49:16 +00:00
*/}
2021-07-10 16:14:49 +00:00
<ContextMenuButton onSelect={() => state.send('DUPLICATED')}>
2021-06-10 09:49:16 +00:00
<span>Duplicate</span>
<kbd>
<span>{commandKey()}</span>
<span>D</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
<ContextMenuDivider />
{hasGroupSelected ||
2021-06-17 12:03:08 +00:00
(hasTwoOrMore && (
2021-06-10 09:49:16 +00:00
<>
{hasGroupSelected && (
2021-07-10 16:14:49 +00:00
<ContextMenuButton onSelect={() => state.send('UNGROUPED')}>
2021-06-10 09:49:16 +00:00
<span>Ungroup</span>
<kbd>
<span>{commandKey()}</span>
<span></span>
<span>G</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
2021-06-10 09:49:16 +00:00
)}
2021-06-17 12:03:08 +00:00
{hasTwoOrMore && (
2021-07-10 16:14:49 +00:00
<ContextMenuButton onSelect={() => state.send('GROUPED')}>
2021-06-10 09:49:16 +00:00
<span>Group</span>
<kbd>
<span>{commandKey()}</span>
<span>G</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
2021-06-10 09:49:16 +00:00
)}
</>
))}
2021-07-10 16:14:49 +00:00
<ContextMenuSubMenu label="Move">
<ContextMenuButton
onSelect={() =>
state.send('MOVED', {
type: MoveType.ToFront,
})
}
>
<span>To Front</span>
<kbd>
<span>{commandKey()}</span>
<span></span>
<span>]</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
2021-06-10 09:49:16 +00:00
2021-07-10 16:14:49 +00:00
<ContextMenuButton
onSelect={() =>
state.send('MOVED', {
type: MoveType.Forward,
})
}
>
<span>Forward</span>
<kbd>
<span>{commandKey()}</span>
<span>]</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
<ContextMenuButton
onSelect={() =>
state.send('MOVED', {
type: MoveType.Backward,
})
}
>
<span>Backward</span>
<kbd>
<span>{commandKey()}</span>
<span>[</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
<ContextMenuButton
onSelect={() =>
state.send('MOVED', {
type: MoveType.ToBack,
})
}
>
<span>To Back</span>
<kbd>
<span>{commandKey()}</span>
<span></span>
<span>[</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
</ContextMenuSubMenu>
2021-06-17 12:03:08 +00:00
{hasTwoOrMore && (
<AlignDistributeSubMenu
hasTwoOrMore={hasTwoOrMore}
hasThreeOrMore={hasThreeOrMore}
/>
)}
<MoveToPageMenu />
2021-07-10 16:14:49 +00:00
<ContextMenuButton onSelect={() => state.send('COPIED_TO_SVG')}>
2021-06-20 22:01:40 +00:00
<span>Copy to SVG</span>
<kbd>
<span>{commandKey()}</span>
<span></span>
<span>C</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
<ContextMenuDivider />
<ContextMenuButton onSelect={() => state.send('DELETED')}>
2021-06-10 09:49:16 +00:00
<span>Delete</span>
<kbd>
<span></span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
2021-06-10 09:49:16 +00:00
</>
) : (
<>
2021-07-10 16:14:49 +00:00
<ContextMenuButton onSelect={() => state.send('UNDO')}>
2021-06-10 09:49:16 +00:00
<span>Undo</span>
<kbd>
<span>{commandKey()}</span>
<span>Z</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
<ContextMenuButton onSelect={() => state.send('REDO')}>
2021-06-10 09:49:16 +00:00
<span>Redo</span>
<kbd>
<span>{commandKey()}</span>
<span></span>
<span>Z</span>
</kbd>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
2021-06-10 09:49:16 +00:00
</>
)}
2021-07-10 16:14:49 +00:00
</MenuContent>
</ContextMenuRoot>
)
}
2021-06-17 12:03:08 +00:00
function AlignDistributeSubMenu({
hasThreeOrMore,
}: {
hasTwoOrMore: boolean
hasThreeOrMore: boolean
}) {
return (
2021-07-10 16:14:49 +00:00
<ContextMenuRoot>
2021-06-29 12:00:59 +00:00
<_ContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
2021-06-17 12:03:08 +00:00
<span>Align / Distribute</span>
<IconWrapper size="small">
<ChevronRightIcon />
</IconWrapper>
</_ContextMenu.TriggerItem>
<StyledGrid
2021-07-10 16:14:49 +00:00
as={_ContextMenu.Content}
2021-06-17 12:03:08 +00:00
sideOffset={2}
alignOffset={-2}
isMobile={isMobile()}
selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}
>
2021-07-10 16:14:49 +00:00
<ContextMenuIconButton onSelect={alignLeft}>
2021-06-17 12:03:08 +00:00
<AlignLeftIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
<ContextMenuIconButton onSelect={alignCenterHorizontal}>
2021-06-17 12:03:08 +00:00
<AlignCenterHorizontallyIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
<ContextMenuIconButton onSelect={alignRight}>
2021-06-17 12:03:08 +00:00
<AlignRightIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
<ContextMenuIconButton onSelect={stretchHorizontally}>
2021-06-17 12:03:08 +00:00
<StretchHorizontallyIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
2021-06-17 12:03:08 +00:00
{hasThreeOrMore && (
2021-07-10 16:14:49 +00:00
<ContextMenuIconButton onSelect={distributeHorizontally}>
2021-06-17 12:03:08 +00:00
<SpaceEvenlyHorizontallyIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
2021-06-17 12:03:08 +00:00
)}
2021-07-10 16:14:49 +00:00
<ContextMenuIconButton onSelect={alignTop}>
2021-06-17 12:03:08 +00:00
<AlignTopIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
<ContextMenuIconButton onSelect={alignCenterVertical}>
2021-06-17 12:03:08 +00:00
<AlignCenterVerticallyIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
<ContextMenuIconButton onSelect={alignBottom}>
2021-06-17 12:03:08 +00:00
<AlignBottomIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
<ContextMenuIconButton onSelect={stretchVertically}>
2021-06-17 12:03:08 +00:00
<StretchVerticallyIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
2021-06-17 12:03:08 +00:00
{hasThreeOrMore && (
2021-07-10 16:14:49 +00:00
<ContextMenuIconButton onSelect={distributeVertically}>
2021-06-17 12:03:08 +00:00
<SpaceEvenlyVerticallyIcon />
2021-07-10 16:14:49 +00:00
</ContextMenuIconButton>
2021-06-17 12:03:08 +00:00
)}
2021-07-10 16:14:49 +00:00
<ContextMenuArrow offset={13} />
2021-06-17 12:03:08 +00:00
</StyledGrid>
2021-07-10 16:14:49 +00:00
</ContextMenuRoot>
2021-06-17 12:03:08 +00:00
)
}
2021-07-10 16:14:49 +00:00
const StyledGrid = styled(MenuContent, {
2021-06-17 12:03:08 +00:00
display: 'grid',
variants: {
selectedStyle: {
threeOrMore: {
gridTemplateColumns: 'repeat(5, auto)',
},
twoOrMore: {
gridTemplateColumns: 'repeat(4, auto)',
},
},
},
})
function MoveToPageMenu() {
2021-06-10 09:49:16 +00:00
const documentPages = useSelector((s) => s.data.document.pages)
const currentPageId = useSelector((s) => s.data.currentPageId)
if (!documentPages[currentPageId]) return null
const sorted = Object.values(documentPages)
.sort((a, b) => a.childIndex - b.childIndex)
.filter((a) => a.id !== currentPageId)
2021-06-17 12:03:08 +00:00
if (sorted.length === 0) return null
2021-06-10 09:49:16 +00:00
return (
2021-07-10 16:14:49 +00:00
<ContextMenuRoot>
<ContextMenuButton>
<span>Move To Page</span>
<IconWrapper size="small">
<ChevronRightIcon />
</IconWrapper>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
<MenuContent
as={_ContextMenu.Content}
sideOffset={2}
alignOffset={-2}
isMobile={isMobile()}
>
2021-06-10 09:49:16 +00:00
{sorted.map(({ id, name }) => (
2021-07-10 16:14:49 +00:00
<ContextMenuButton
2021-06-10 09:49:16 +00:00
key={id}
disabled={id === currentPageId}
onSelect={() => state.send('MOVED_TO_PAGE', { id })}
>
<span>{name}</span>
2021-07-10 16:14:49 +00:00
</ContextMenuButton>
2021-06-10 09:49:16 +00:00
))}
2021-07-10 16:14:49 +00:00
<ContextMenuArrow offset={13} />
</MenuContent>
</ContextMenuRoot>
2021-06-10 09:49:16 +00:00
)
}