2021-11-03 16:46:33 +00:00
|
|
|
import * as React from 'react'
|
2021-11-06 11:16:30 +00:00
|
|
|
import { styled } from '~styles'
|
2021-11-03 16:46:33 +00:00
|
|
|
import * as RadixContextMenu from '@radix-ui/react-context-menu'
|
|
|
|
import { useTLDrawContext } from '~hooks'
|
2021-11-08 14:21:37 +00:00
|
|
|
import { TLDrawSnapshot, AlignType, DistributeType, StretchType } from '~types'
|
2021-11-03 16:46:33 +00:00
|
|
|
import {
|
|
|
|
AlignBottomIcon,
|
|
|
|
AlignCenterHorizontallyIcon,
|
|
|
|
AlignCenterVerticallyIcon,
|
|
|
|
AlignLeftIcon,
|
|
|
|
AlignRightIcon,
|
|
|
|
AlignTopIcon,
|
|
|
|
SpaceEvenlyHorizontallyIcon,
|
|
|
|
SpaceEvenlyVerticallyIcon,
|
|
|
|
StretchHorizontallyIcon,
|
|
|
|
StretchVerticallyIcon,
|
|
|
|
} from '@radix-ui/react-icons'
|
|
|
|
import { CMRowButton } from './CMRowButton'
|
|
|
|
import { CMIconButton } from './CMIconButton'
|
|
|
|
import { CMTriggerButton } from './CMTriggerButton'
|
|
|
|
import { Divider } from '~components/Divider'
|
|
|
|
import { MenuContent } from '~components/MenuContent'
|
|
|
|
|
2021-11-08 14:21:37 +00:00
|
|
|
const has1SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
2021-11-03 16:46:33 +00:00
|
|
|
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
|
|
|
|
}
|
2021-11-08 14:21:37 +00:00
|
|
|
const has2SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
2021-11-03 16:46:33 +00:00
|
|
|
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1
|
|
|
|
}
|
2021-11-08 14:21:37 +00:00
|
|
|
const has3SelectedIdsSelector = (s: TLDrawSnapshot) => {
|
2021-11-03 16:46:33 +00:00
|
|
|
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
|
|
|
|
}
|
|
|
|
|
2021-11-08 14:21:37 +00:00
|
|
|
const isDebugModeSelector = (s: TLDrawSnapshot) => {
|
2021-11-03 16:46:33 +00:00
|
|
|
return s.settings.isDebugMode
|
|
|
|
}
|
|
|
|
|
2021-11-08 14:21:37 +00:00
|
|
|
const hasGroupSelectedSelector = (s: TLDrawSnapshot) => {
|
2021-11-03 16:46:33 +00:00
|
|
|
return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
|
|
|
|
(id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2021-11-05 14:13:14 +00:00
|
|
|
const preventDefault = (e: Event) => e.stopPropagation()
|
|
|
|
|
2021-11-03 16:46:33 +00:00
|
|
|
interface ContextMenuProps {
|
2021-11-11 09:54:58 +00:00
|
|
|
onBlur: React.FocusEventHandler
|
2021-11-03 16:46:33 +00:00
|
|
|
children: React.ReactNode
|
|
|
|
}
|
|
|
|
|
2021-11-11 09:54:58 +00:00
|
|
|
export const ContextMenu = ({ onBlur, children }: ContextMenuProps): JSX.Element => {
|
2021-11-08 14:21:37 +00:00
|
|
|
const { state, useSelector } = useTLDrawContext()
|
2021-11-03 16:46:33 +00:00
|
|
|
const hasSelection = useSelector(has1SelectedIdsSelector)
|
|
|
|
const hasTwoOrMore = useSelector(has2SelectedIdsSelector)
|
|
|
|
const hasThreeOrMore = useSelector(has3SelectedIdsSelector)
|
|
|
|
const isDebugMode = useSelector(isDebugModeSelector)
|
|
|
|
const hasGroupSelected = useSelector(hasGroupSelectedSelector)
|
|
|
|
|
|
|
|
const rContent = React.useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
const handleFlipHorizontal = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.flipHorizontal()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleFlipVertical = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.flipVertical()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleDuplicate = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.duplicate()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleGroup = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.group()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleMoveToBack = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.moveToBack()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleMoveBackward = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.moveBackward()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleMoveForward = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.moveForward()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleMoveToFront = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.moveToFront()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleDelete = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.delete()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleCopyJson = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.copyJson()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleCopy = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.copy()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handlePaste = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.paste()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleCopySvg = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.copySvg()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleUndo = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.undo()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const handleRedo = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.redo()
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<RadixContextMenu.Root>
|
|
|
|
<RadixContextMenu.Trigger dir="ltr">{children}</RadixContextMenu.Trigger>
|
2021-11-11 09:54:58 +00:00
|
|
|
<RadixContextMenu.Content
|
|
|
|
dir="ltr"
|
|
|
|
ref={rContent}
|
|
|
|
onEscapeKeyDown={preventDefault}
|
|
|
|
asChild
|
|
|
|
tabIndex={-1}
|
|
|
|
onBlur={onBlur}
|
|
|
|
>
|
2021-11-03 16:46:33 +00:00
|
|
|
<MenuContent>
|
|
|
|
{hasSelection ? (
|
|
|
|
<>
|
|
|
|
<CMRowButton onSelect={handleFlipHorizontal} kbd="⇧H">
|
|
|
|
Flip Horizontal
|
|
|
|
</CMRowButton>
|
|
|
|
<CMRowButton onSelect={handleFlipVertical} kbd="⇧V">
|
|
|
|
Flip Vertical
|
|
|
|
</CMRowButton>
|
|
|
|
<CMRowButton onSelect={handleDuplicate} kbd="#D">
|
|
|
|
Duplicate
|
|
|
|
</CMRowButton>
|
2021-11-06 08:04:44 +00:00
|
|
|
{(hasTwoOrMore || hasGroupSelected) && <Divider />}
|
2021-11-03 16:46:33 +00:00
|
|
|
{hasTwoOrMore && (
|
|
|
|
<CMRowButton onSelect={handleGroup} kbd="#G">
|
|
|
|
Group
|
|
|
|
</CMRowButton>
|
|
|
|
)}
|
|
|
|
{hasGroupSelected && (
|
|
|
|
<CMRowButton onSelect={handleGroup} kbd="#⇧G">
|
|
|
|
Ungroup
|
|
|
|
</CMRowButton>
|
|
|
|
)}
|
2021-11-06 08:04:44 +00:00
|
|
|
<Divider />
|
2021-11-03 16:46:33 +00:00
|
|
|
<ContextMenuSubMenu label="Move">
|
|
|
|
<CMRowButton onSelect={handleMoveToFront} kbd="⇧]">
|
|
|
|
To Front
|
|
|
|
</CMRowButton>
|
|
|
|
<CMRowButton onSelect={handleMoveForward} kbd="]">
|
|
|
|
Forward
|
|
|
|
</CMRowButton>
|
|
|
|
<CMRowButton onSelect={handleMoveBackward} kbd="[">
|
|
|
|
Backward
|
|
|
|
</CMRowButton>
|
|
|
|
<CMRowButton onSelect={handleMoveToBack} kbd="⇧[">
|
|
|
|
To Back
|
|
|
|
</CMRowButton>
|
|
|
|
</ContextMenuSubMenu>
|
|
|
|
<MoveToPageMenu />
|
|
|
|
{hasTwoOrMore && (
|
|
|
|
<AlignDistributeSubMenu
|
|
|
|
hasTwoOrMore={hasTwoOrMore}
|
|
|
|
hasThreeOrMore={hasThreeOrMore}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<Divider />
|
|
|
|
<CMRowButton onSelect={handleCopy} kbd="#C">
|
|
|
|
Copy
|
|
|
|
</CMRowButton>
|
|
|
|
<CMRowButton onSelect={handleCopySvg} kbd="⇧#C">
|
|
|
|
Copy as SVG
|
|
|
|
</CMRowButton>
|
|
|
|
{isDebugMode && <CMRowButton onSelect={handleCopyJson}>Copy as JSON</CMRowButton>}
|
|
|
|
<CMRowButton onSelect={handlePaste} kbd="#V">
|
|
|
|
Paste
|
|
|
|
</CMRowButton>
|
|
|
|
<Divider />
|
|
|
|
<CMRowButton onSelect={handleDelete} kbd="⌫">
|
|
|
|
Delete
|
|
|
|
</CMRowButton>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<CMRowButton onSelect={handlePaste} kbd="#V">
|
|
|
|
Paste
|
|
|
|
</CMRowButton>
|
|
|
|
<CMRowButton onSelect={handleUndo} kbd="#Z">
|
|
|
|
Undo
|
|
|
|
</CMRowButton>
|
|
|
|
<CMRowButton onSelect={handleRedo} kbd="#⇧Z">
|
|
|
|
Redo
|
|
|
|
</CMRowButton>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</MenuContent>
|
|
|
|
</RadixContextMenu.Content>
|
|
|
|
</RadixContextMenu.Root>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function AlignDistributeSubMenu({
|
|
|
|
hasThreeOrMore,
|
|
|
|
}: {
|
|
|
|
hasTwoOrMore: boolean
|
|
|
|
hasThreeOrMore: boolean
|
|
|
|
}) {
|
2021-11-08 14:21:37 +00:00
|
|
|
const { state } = useTLDrawContext()
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const alignTop = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.align(AlignType.Top)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const alignCenterVertical = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.align(AlignType.CenterVertical)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const alignBottom = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.align(AlignType.Bottom)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const stretchVertically = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.stretch(StretchType.Vertical)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const distributeVertically = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.distribute(DistributeType.Vertical)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const alignLeft = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.align(AlignType.Left)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const alignCenterHorizontal = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.align(AlignType.CenterHorizontal)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const alignRight = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.align(AlignType.Right)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const stretchHorizontally = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.stretch(StretchType.Horizontal)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
const distributeHorizontally = React.useCallback(() => {
|
2021-11-08 14:21:37 +00:00
|
|
|
state.distribute(DistributeType.Horizontal)
|
|
|
|
}, [state])
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<RadixContextMenu.Root>
|
|
|
|
<CMTriggerButton isSubmenu>Align / Distribute</CMTriggerButton>
|
|
|
|
<RadixContextMenu.Content asChild sideOffset={2} alignOffset={-2}>
|
|
|
|
<StyledGridContent selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}>
|
|
|
|
<CMIconButton onSelect={alignLeft}>
|
|
|
|
<AlignLeftIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
<CMIconButton onSelect={alignCenterHorizontal}>
|
|
|
|
<AlignCenterHorizontallyIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
<CMIconButton onSelect={alignRight}>
|
|
|
|
<AlignRightIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
<CMIconButton onSelect={stretchHorizontally}>
|
|
|
|
<StretchHorizontallyIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
{hasThreeOrMore && (
|
|
|
|
<CMIconButton onSelect={distributeHorizontally}>
|
|
|
|
<SpaceEvenlyHorizontallyIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
)}
|
|
|
|
<CMIconButton onSelect={alignTop}>
|
|
|
|
<AlignTopIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
<CMIconButton onSelect={alignCenterVertical}>
|
|
|
|
<AlignCenterVerticallyIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
<CMIconButton onSelect={alignBottom}>
|
|
|
|
<AlignBottomIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
<CMIconButton onSelect={stretchVertically}>
|
|
|
|
<StretchVerticallyIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
{hasThreeOrMore && (
|
|
|
|
<CMIconButton onSelect={distributeVertically}>
|
|
|
|
<SpaceEvenlyVerticallyIcon />
|
|
|
|
</CMIconButton>
|
|
|
|
)}
|
|
|
|
<CMArrow offset={13} />
|
|
|
|
</StyledGridContent>
|
|
|
|
</RadixContextMenu.Content>
|
|
|
|
</RadixContextMenu.Root>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const StyledGridContent = styled(MenuContent, {
|
|
|
|
display: 'grid',
|
|
|
|
variants: {
|
|
|
|
selectedStyle: {
|
|
|
|
threeOrMore: {
|
|
|
|
gridTemplateColumns: 'repeat(5, auto)',
|
|
|
|
},
|
|
|
|
twoOrMore: {
|
|
|
|
gridTemplateColumns: 'repeat(4, auto)',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
/* ------------------ Move to Page ------------------ */
|
|
|
|
|
2021-11-08 14:21:37 +00:00
|
|
|
const currentPageIdSelector = (s: TLDrawSnapshot) => s.appState.currentPageId
|
|
|
|
const documentPagesSelector = (s: TLDrawSnapshot) => s.document.pages
|
2021-11-03 16:46:33 +00:00
|
|
|
|
|
|
|
function MoveToPageMenu(): JSX.Element | null {
|
2021-11-08 14:21:37 +00:00
|
|
|
const { state, useSelector } = useTLDrawContext()
|
2021-11-03 16:46:33 +00:00
|
|
|
const currentPageId = useSelector(currentPageIdSelector)
|
|
|
|
const documentPages = useSelector(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 (
|
|
|
|
<RadixContextMenu.Root dir="ltr">
|
|
|
|
<CMTriggerButton isSubmenu>Move To Page</CMTriggerButton>
|
|
|
|
<RadixContextMenu.Content dir="ltr" sideOffset={2} alignOffset={-2} asChild>
|
|
|
|
<MenuContent>
|
|
|
|
{sorted.map(({ id, name }, i) => (
|
|
|
|
<CMRowButton
|
|
|
|
key={id}
|
|
|
|
disabled={id === currentPageId}
|
2021-11-08 14:21:37 +00:00
|
|
|
onSelect={() => state.moveToPage(id)}
|
2021-11-03 16:46:33 +00:00
|
|
|
>
|
|
|
|
{name || `Page ${i}`}
|
|
|
|
</CMRowButton>
|
|
|
|
))}
|
|
|
|
<CMArrow offset={13} />
|
|
|
|
</MenuContent>
|
|
|
|
</RadixContextMenu.Content>
|
|
|
|
</RadixContextMenu.Root>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/* --------------------- Submenu -------------------- */
|
|
|
|
|
|
|
|
export interface ContextMenuSubMenuProps {
|
|
|
|
label: string
|
|
|
|
children: React.ReactNode
|
|
|
|
}
|
|
|
|
|
|
|
|
export function ContextMenuSubMenu({ children, label }: ContextMenuSubMenuProps): JSX.Element {
|
|
|
|
return (
|
|
|
|
<RadixContextMenu.Root dir="ltr">
|
|
|
|
<CMTriggerButton isSubmenu>{label}</CMTriggerButton>
|
|
|
|
<RadixContextMenu.Content dir="ltr" sideOffset={2} alignOffset={-2} asChild>
|
|
|
|
<MenuContent>
|
|
|
|
{children}
|
|
|
|
<CMArrow offset={13} />
|
|
|
|
</MenuContent>
|
|
|
|
</RadixContextMenu.Content>
|
|
|
|
</RadixContextMenu.Root>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/* ---------------------- Arrow --------------------- */
|
|
|
|
|
|
|
|
const CMArrow = styled(RadixContextMenu.ContextMenuArrow, {
|
|
|
|
fill: '$panel',
|
|
|
|
})
|