You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

context-menu.tsx 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import * as _ContextMenu from '@radix-ui/react-context-menu'
  2. import styled from 'styles'
  3. import {
  4. IconWrapper,
  5. breakpoints,
  6. RowButton,
  7. ContextMenuArrow,
  8. ContextMenuDivider,
  9. ContextMenuButton,
  10. ContextMenuSubMenu,
  11. ContextMenuIconButton,
  12. ContextMenuRoot,
  13. MenuContent,
  14. } from 'components/shared'
  15. import { commandKey, deepCompareArrays } from 'utils'
  16. import state, { useSelector } from 'state'
  17. import {
  18. AlignType,
  19. DistributeType,
  20. MoveType,
  21. ShapeType,
  22. StretchType,
  23. } from 'types'
  24. import tld from 'utils/tld'
  25. import React, { useRef } from 'react'
  26. import {
  27. ChevronRightIcon,
  28. AlignBottomIcon,
  29. AlignCenterHorizontallyIcon,
  30. AlignCenterVerticallyIcon,
  31. AlignLeftIcon,
  32. AlignRightIcon,
  33. AlignTopIcon,
  34. SpaceEvenlyHorizontallyIcon,
  35. SpaceEvenlyVerticallyIcon,
  36. StretchHorizontallyIcon,
  37. StretchVerticallyIcon,
  38. } from '@radix-ui/react-icons'
  39. import { Kbd } from 'components/shared'
  40. function alignTop() {
  41. state.send('ALIGNED', { type: AlignType.Top })
  42. }
  43. function alignCenterVertical() {
  44. state.send('ALIGNED', { type: AlignType.CenterVertical })
  45. }
  46. function alignBottom() {
  47. state.send('ALIGNED', { type: AlignType.Bottom })
  48. }
  49. function stretchVertically() {
  50. state.send('STRETCHED', { type: StretchType.Vertical })
  51. }
  52. function distributeVertically() {
  53. state.send('DISTRIBUTED', { type: DistributeType.Vertical })
  54. }
  55. function alignLeft() {
  56. state.send('ALIGNED', { type: AlignType.Left })
  57. }
  58. function alignCenterHorizontal() {
  59. state.send('ALIGNED', { type: AlignType.CenterHorizontal })
  60. }
  61. function alignRight() {
  62. state.send('ALIGNED', { type: AlignType.Right })
  63. }
  64. function stretchHorizontally() {
  65. state.send('STRETCHED', { type: StretchType.Horizontal })
  66. }
  67. function distributeHorizontally() {
  68. state.send('DISTRIBUTED', { type: DistributeType.Horizontal })
  69. }
  70. export default function ContextMenu({
  71. children,
  72. }: {
  73. children: React.ReactNode
  74. }): JSX.Element {
  75. const selectedShapeIds = useSelector(
  76. (s) => s.values.selectedIds,
  77. deepCompareArrays
  78. )
  79. const rContent = useRef<HTMLDivElement>(null)
  80. const hasGroupSelected = useSelector((s) =>
  81. selectedShapeIds.some(
  82. (id) => tld.getShape(s.data, id)?.type === ShapeType.Group
  83. )
  84. )
  85. const hasTwoOrMore = selectedShapeIds.length > 1
  86. const hasThreeOrMore = selectedShapeIds.length > 2
  87. return (
  88. <ContextMenuRoot>
  89. <_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
  90. <MenuContent as={_ContextMenu.Content} ref={rContent}>
  91. {selectedShapeIds.length ? (
  92. <>
  93. {/* <ContextMenuButton onSelect={() => state.send('COPIED')}>
  94. <span>Copy</span>
  95. <Kbd>
  96. <span>{commandKey()}</span>
  97. <span>C</span>
  98. </Kbd>
  99. </ContextMenuButton>
  100. <ContextMenuButton onSelect={() => state.send('CUT')}>
  101. <span>Cut</span>
  102. <Kbd>
  103. <span>{commandKey()}</span>
  104. <span>X</span>
  105. </Kbd>
  106. </ContextMenuButton>
  107. */}
  108. <ContextMenuButton onSelect={() => state.send('DUPLICATED')}>
  109. <span>Duplicate</span>
  110. <Kbd>
  111. <span>{commandKey()}</span>
  112. <span>D</span>
  113. </Kbd>
  114. </ContextMenuButton>
  115. <ContextMenuDivider />
  116. {hasGroupSelected ||
  117. (hasTwoOrMore && (
  118. <>
  119. {hasGroupSelected && (
  120. <ContextMenuButton onSelect={() => state.send('UNGROUPED')}>
  121. <span>Ungroup</span>
  122. <Kbd>
  123. <span>{commandKey()}</span>
  124. <span>⇧</span>
  125. <span>G</span>
  126. </Kbd>
  127. </ContextMenuButton>
  128. )}
  129. {hasTwoOrMore && (
  130. <ContextMenuButton onSelect={() => state.send('GROUPED')}>
  131. <span>Group</span>
  132. <Kbd>
  133. <span>{commandKey()}</span>
  134. <span>G</span>
  135. </Kbd>
  136. </ContextMenuButton>
  137. )}
  138. </>
  139. ))}
  140. <ContextMenuSubMenu label="Move">
  141. <ContextMenuButton
  142. onSelect={() =>
  143. state.send('MOVED', {
  144. type: MoveType.ToFront,
  145. })
  146. }
  147. >
  148. <span>To Front</span>
  149. <Kbd>
  150. <span>{commandKey()}</span>
  151. <span>⇧</span>
  152. <span>]</span>
  153. </Kbd>
  154. </ContextMenuButton>
  155. <ContextMenuButton
  156. onSelect={() =>
  157. state.send('MOVED', {
  158. type: MoveType.Forward,
  159. })
  160. }
  161. >
  162. <span>Forward</span>
  163. <Kbd>
  164. <span>{commandKey()}</span>
  165. <span>]</span>
  166. </Kbd>
  167. </ContextMenuButton>
  168. <ContextMenuButton
  169. onSelect={() =>
  170. state.send('MOVED', {
  171. type: MoveType.Backward,
  172. })
  173. }
  174. >
  175. <span>Backward</span>
  176. <Kbd>
  177. <span>{commandKey()}</span>
  178. <span>[</span>
  179. </Kbd>
  180. </ContextMenuButton>
  181. <ContextMenuButton
  182. onSelect={() =>
  183. state.send('MOVED', {
  184. type: MoveType.ToBack,
  185. })
  186. }
  187. >
  188. <span>To Back</span>
  189. <Kbd>
  190. <span>{commandKey()}</span>
  191. <span>⇧</span>
  192. <span>[</span>
  193. </Kbd>
  194. </ContextMenuButton>
  195. </ContextMenuSubMenu>
  196. {hasTwoOrMore && (
  197. <AlignDistributeSubMenu
  198. hasTwoOrMore={hasTwoOrMore}
  199. hasThreeOrMore={hasThreeOrMore}
  200. />
  201. )}
  202. <MoveToPageMenu />
  203. <ContextMenuButton onSelect={() => state.send('COPIED_TO_SVG')}>
  204. <span>Copy to SVG</span>
  205. <Kbd>
  206. <span>{commandKey()}</span>
  207. <span>⇧</span>
  208. <span>C</span>
  209. </Kbd>
  210. </ContextMenuButton>
  211. <ContextMenuDivider />
  212. <ContextMenuButton onSelect={() => state.send('DELETED')}>
  213. <span>Delete</span>
  214. <Kbd>
  215. <span>⌫</span>
  216. </Kbd>
  217. </ContextMenuButton>
  218. </>
  219. ) : (
  220. <>
  221. <ContextMenuButton onSelect={() => state.send('UNDO')}>
  222. <span>Undo</span>
  223. <Kbd>
  224. <span>{commandKey()}</span>
  225. <span>Z</span>
  226. </Kbd>
  227. </ContextMenuButton>
  228. <ContextMenuButton onSelect={() => state.send('REDO')}>
  229. <span>Redo</span>
  230. <Kbd>
  231. <span>{commandKey()}</span>
  232. <span>⇧</span>
  233. <span>Z</span>
  234. </Kbd>
  235. </ContextMenuButton>
  236. </>
  237. )}
  238. </MenuContent>
  239. </ContextMenuRoot>
  240. )
  241. }
  242. function AlignDistributeSubMenu({
  243. hasThreeOrMore,
  244. }: {
  245. hasTwoOrMore: boolean
  246. hasThreeOrMore: boolean
  247. }) {
  248. return (
  249. <ContextMenuRoot>
  250. <_ContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
  251. <span>Align / Distribute</span>
  252. <IconWrapper size="small">
  253. <ChevronRightIcon />
  254. </IconWrapper>
  255. </_ContextMenu.TriggerItem>
  256. <StyledGrid
  257. as={_ContextMenu.Content}
  258. sideOffset={2}
  259. alignOffset={-2}
  260. selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}
  261. >
  262. <ContextMenuIconButton onSelect={alignLeft}>
  263. <AlignLeftIcon />
  264. </ContextMenuIconButton>
  265. <ContextMenuIconButton onSelect={alignCenterHorizontal}>
  266. <AlignCenterHorizontallyIcon />
  267. </ContextMenuIconButton>
  268. <ContextMenuIconButton onSelect={alignRight}>
  269. <AlignRightIcon />
  270. </ContextMenuIconButton>
  271. <ContextMenuIconButton onSelect={stretchHorizontally}>
  272. <StretchHorizontallyIcon />
  273. </ContextMenuIconButton>
  274. {hasThreeOrMore && (
  275. <ContextMenuIconButton onSelect={distributeHorizontally}>
  276. <SpaceEvenlyHorizontallyIcon />
  277. </ContextMenuIconButton>
  278. )}
  279. <ContextMenuIconButton onSelect={alignTop}>
  280. <AlignTopIcon />
  281. </ContextMenuIconButton>
  282. <ContextMenuIconButton onSelect={alignCenterVertical}>
  283. <AlignCenterVerticallyIcon />
  284. </ContextMenuIconButton>
  285. <ContextMenuIconButton onSelect={alignBottom}>
  286. <AlignBottomIcon />
  287. </ContextMenuIconButton>
  288. <ContextMenuIconButton onSelect={stretchVertically}>
  289. <StretchVerticallyIcon />
  290. </ContextMenuIconButton>
  291. {hasThreeOrMore && (
  292. <ContextMenuIconButton onSelect={distributeVertically}>
  293. <SpaceEvenlyVerticallyIcon />
  294. </ContextMenuIconButton>
  295. )}
  296. <ContextMenuArrow offset={13} />
  297. </StyledGrid>
  298. </ContextMenuRoot>
  299. )
  300. }
  301. const StyledGrid = styled(MenuContent, {
  302. display: 'grid',
  303. variants: {
  304. selectedStyle: {
  305. threeOrMore: {
  306. gridTemplateColumns: 'repeat(5, auto)',
  307. },
  308. twoOrMore: {
  309. gridTemplateColumns: 'repeat(4, auto)',
  310. },
  311. },
  312. },
  313. })
  314. function MoveToPageMenu() {
  315. const documentPages = useSelector((s) => s.data.document.pages)
  316. const currentPageId = useSelector((s) => s.data.currentPageId)
  317. if (!documentPages[currentPageId]) return null
  318. const sorted = Object.values(documentPages)
  319. .sort((a, b) => a.childIndex - b.childIndex)
  320. .filter((a) => a.id !== currentPageId)
  321. if (sorted.length === 0) return null
  322. return (
  323. <ContextMenuRoot>
  324. <ContextMenuButton>
  325. <span>Move To Page</span>
  326. <IconWrapper size="small">
  327. <ChevronRightIcon />
  328. </IconWrapper>
  329. </ContextMenuButton>
  330. <MenuContent as={_ContextMenu.Content} sideOffset={2} alignOffset={-2}>
  331. {sorted.map(({ id, name }) => (
  332. <ContextMenuButton
  333. key={id}
  334. disabled={id === currentPageId}
  335. onSelect={() => state.send('MOVED_TO_PAGE', { id })}
  336. >
  337. <span>{name}</span>
  338. </ContextMenuButton>
  339. ))}
  340. <ContextMenuArrow offset={13} />
  341. </MenuContent>
  342. </ContextMenuRoot>
  343. )
  344. }