您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

context-menu.tsx 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import * as _ContextMenu from '@radix-ui/react-context-menu'
  2. import * as _Dropdown from '@radix-ui/react-dropdown-menu'
  3. import styled from 'styles'
  4. import { IconWrapper, RowButton } from './shared'
  5. import {
  6. commandKey,
  7. deepCompareArrays,
  8. getSelectedShapes,
  9. isMobile,
  10. } from 'utils/utils'
  11. import state, { useSelector } from 'state'
  12. import { MoveType, ShapeType } from 'types'
  13. import React, { useRef } from 'react'
  14. import { ChevronRightIcon } from '@radix-ui/react-icons'
  15. export default function ContextMenu({
  16. children,
  17. }: {
  18. children: React.ReactNode
  19. }) {
  20. const selectedShapes = useSelector(
  21. (s) => getSelectedShapes(s.data),
  22. deepCompareArrays
  23. )
  24. const rContent = useRef<HTMLDivElement>(null)
  25. const hasGroupSelectd = selectedShapes.some((s) => s.type === ShapeType.Group)
  26. const hasMultipleSelected = selectedShapes.length > 1
  27. return (
  28. <_ContextMenu.Root>
  29. <_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
  30. <StyledContent ref={rContent} isMobile={isMobile()}>
  31. {selectedShapes.length ? (
  32. <>
  33. {/* <Button onSelect={() => state.send('COPIED')}>
  34. <span>Copy</span>
  35. <kbd>
  36. <span>{commandKey()}</span>
  37. <span>C</span>
  38. </kbd>
  39. </Button>
  40. <Button onSelect={() => state.send('CUT')}>
  41. <span>Cut</span>
  42. <kbd>
  43. <span>{commandKey()}</span>
  44. <span>X</span>
  45. </kbd>
  46. </Button>
  47. */}
  48. <Button onSelect={() => state.send('DUPLICATED')}>
  49. <span>Duplicate</span>
  50. <kbd>
  51. <span>{commandKey()}</span>
  52. <span>D</span>
  53. </kbd>
  54. </Button>
  55. <StyledDivider />
  56. {hasGroupSelectd ||
  57. (hasMultipleSelected && (
  58. <>
  59. {hasGroupSelectd && (
  60. <Button onSelect={() => state.send('UNGROUPED')}>
  61. <span>Ungroup</span>
  62. <kbd>
  63. <span>{commandKey()}</span>
  64. <span>⇧</span>
  65. <span>G</span>
  66. </kbd>
  67. </Button>
  68. )}
  69. {hasMultipleSelected && (
  70. <Button onSelect={() => state.send('GROUPED')}>
  71. <span>Group</span>
  72. <kbd>
  73. <span>{commandKey()}</span>
  74. <span>G</span>
  75. </kbd>
  76. </Button>
  77. )}
  78. </>
  79. ))}
  80. <SubMenu label="Move">
  81. <Button
  82. onSelect={() =>
  83. state.send('MOVED', {
  84. type: MoveType.ToFront,
  85. })
  86. }
  87. >
  88. <span>To Front</span>
  89. <kbd>
  90. <span>{commandKey()}</span>
  91. <span>⇧</span>
  92. <span>]</span>
  93. </kbd>
  94. </Button>
  95. <Button
  96. onSelect={() =>
  97. state.send('MOVED', {
  98. type: MoveType.Forward,
  99. })
  100. }
  101. >
  102. <span>Forward</span>
  103. <kbd>
  104. <span>{commandKey()}</span>
  105. <span>]</span>
  106. </kbd>
  107. </Button>
  108. <Button
  109. onSelect={() =>
  110. state.send('MOVED', {
  111. type: MoveType.Backward,
  112. })
  113. }
  114. >
  115. <span>Backward</span>
  116. <kbd>
  117. <span>{commandKey()}</span>
  118. <span>[</span>
  119. </kbd>
  120. </Button>
  121. <Button
  122. onSelect={() =>
  123. state.send('MOVED', {
  124. type: MoveType.ToBack,
  125. })
  126. }
  127. >
  128. <span>To Back</span>
  129. <kbd>
  130. <span>{commandKey()}</span>
  131. <span>⇧</span>
  132. <span>[</span>
  133. </kbd>
  134. </Button>
  135. </SubMenu>
  136. <MoveToPageMenu />
  137. <StyledDivider />
  138. <Button onSelect={() => state.send('DELETED')}>
  139. <span>Delete</span>
  140. <kbd>
  141. <span>⌫</span>
  142. </kbd>
  143. </Button>
  144. </>
  145. ) : (
  146. <>
  147. <Button onSelect={() => state.send('UNDO')}>
  148. <span>Undo</span>
  149. <kbd>
  150. <span>{commandKey()}</span>
  151. <span>Z</span>
  152. </kbd>
  153. </Button>
  154. <Button onSelect={() => state.send('REDO')}>
  155. <span>Redo</span>
  156. <kbd>
  157. <span>{commandKey()}</span>
  158. <span>⇧</span>
  159. <span>Z</span>
  160. </kbd>
  161. </Button>
  162. </>
  163. )}
  164. </StyledContent>
  165. </_ContextMenu.Root>
  166. )
  167. }
  168. const StyledContent = styled(_ContextMenu.Content, {
  169. position: 'relative',
  170. backgroundColor: '$panel',
  171. borderRadius: '4px',
  172. overflow: 'hidden',
  173. pointerEvents: 'all',
  174. userSelect: 'none',
  175. zIndex: 200,
  176. padding: 3,
  177. boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
  178. minWidth: 128,
  179. '& kbd': {
  180. marginLeft: '32px',
  181. fontSize: '$1',
  182. fontFamily: '$ui',
  183. },
  184. '& kbd > span': {
  185. display: 'inline-block',
  186. width: '12px',
  187. },
  188. variants: {
  189. isMobile: {
  190. true: {
  191. '& kbd': {
  192. display: 'none',
  193. },
  194. },
  195. },
  196. },
  197. })
  198. const StyledDivider = styled(_ContextMenu.Separator, {
  199. backgroundColor: '$hover',
  200. height: 1,
  201. margin: '3px -3px',
  202. })
  203. function Button({
  204. onSelect,
  205. children,
  206. disabled = false,
  207. }: {
  208. onSelect: () => void
  209. disabled?: boolean
  210. children: React.ReactNode
  211. }) {
  212. return (
  213. <_ContextMenu.Item
  214. as={RowButton}
  215. disabled={disabled}
  216. bp={{ '@initial': 'mobile', '@sm': 'small' }}
  217. onSelect={onSelect}
  218. >
  219. {children}
  220. </_ContextMenu.Item>
  221. )
  222. }
  223. function SubMenu({
  224. children,
  225. label,
  226. }: {
  227. label: string
  228. children: React.ReactNode
  229. }) {
  230. return (
  231. <_ContextMenu.Root>
  232. <_ContextMenu.TriggerItem
  233. as={RowButton}
  234. bp={{ '@initial': 'mobile', '@sm': 'small' }}
  235. >
  236. <span>{label}</span>
  237. <IconWrapper size="small">
  238. <ChevronRightIcon />
  239. </IconWrapper>
  240. </_ContextMenu.TriggerItem>
  241. <StyledContent sideOffset={2} alignOffset={-2} isMobile={isMobile()}>
  242. {children}
  243. <StyledArrow offset={13} />
  244. </StyledContent>
  245. </_ContextMenu.Root>
  246. )
  247. }
  248. function MoveToPageMenu() {
  249. const documentPages = useSelector((s) => s.data.document.pages)
  250. const currentPageId = useSelector((s) => s.data.currentPageId)
  251. if (!documentPages[currentPageId]) return null
  252. const sorted = Object.values(documentPages)
  253. .sort((a, b) => a.childIndex - b.childIndex)
  254. .filter((a) => a.id !== currentPageId)
  255. return (
  256. <_ContextMenu.Root>
  257. <_ContextMenu.TriggerItem
  258. as={RowButton}
  259. bp={{ '@initial': 'mobile', '@sm': 'small' }}
  260. >
  261. <span>Move To Page</span>
  262. <IconWrapper size="small">
  263. <ChevronRightIcon />
  264. </IconWrapper>
  265. </_ContextMenu.TriggerItem>
  266. <StyledContent sideOffset={2} alignOffset={-2} isMobile={isMobile()}>
  267. {sorted.map(({ id, name }) => (
  268. <Button
  269. key={id}
  270. disabled={id === currentPageId}
  271. onSelect={() => state.send('MOVED_TO_PAGE', { id })}
  272. >
  273. <span>{name}</span>
  274. </Button>
  275. ))}
  276. <StyledArrow offset={13} />
  277. </StyledContent>
  278. </_ContextMenu.Root>
  279. )
  280. }
  281. const StyledDialogContent = styled(_Dropdown.Content, {
  282. // position: 'fixed',
  283. // top: '50%',
  284. // left: '50%',
  285. // transform: 'translate(-50%, -50%)',
  286. // minWidth: 200,
  287. // maxWidth: 'fit-content',
  288. // maxHeight: '85vh',
  289. // marginTop: '-5vh',
  290. minWidth: 128,
  291. backgroundColor: '$panel',
  292. borderRadius: '4px',
  293. overflow: 'hidden',
  294. pointerEvents: 'all',
  295. userSelect: 'none',
  296. zIndex: 200,
  297. padding: 2,
  298. border: '1px solid $panel',
  299. boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
  300. '&:focus': {
  301. outline: 'none',
  302. },
  303. })
  304. const StyledArrow = styled(_ContextMenu.Arrow, {
  305. fill: 'white',
  306. })