Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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 { 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. export default function ContextMenu({
  15. children,
  16. }: {
  17. children: React.ReactNode
  18. }) {
  19. const selectedShapes = useSelector(
  20. (s) => getSelectedShapes(s.data),
  21. deepCompareArrays
  22. )
  23. const rContent = useRef<HTMLDivElement>(null)
  24. const hasGroupSelectd = selectedShapes.some((s) => s.type === ShapeType.Group)
  25. const hasMultipleSelected = selectedShapes.length > 1
  26. return (
  27. <_ContextMenu.Root>
  28. <_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
  29. <StyledContent ref={rContent} isMobile={isMobile()}>
  30. {selectedShapes.length ? (
  31. <>
  32. {/* <Button onSelect={() => state.send('COPIED')}>
  33. <span>Copy</span>
  34. <kbd>
  35. <span>{commandKey()}</span>
  36. <span>C</span>
  37. </kbd>
  38. </Button>
  39. <Button onSelect={() => state.send('CUT')}>
  40. <span>Cut</span>
  41. <kbd>
  42. <span>{commandKey()}</span>
  43. <span>X</span>
  44. </kbd>
  45. </Button>
  46. */}
  47. <Button onSelect={() => state.send('DUPLICATED')}>
  48. <span>Duplicate</span>
  49. <kbd>
  50. <span>{commandKey()}</span>
  51. <span>D</span>
  52. </kbd>
  53. </Button>
  54. <StyledDivider />
  55. <Button
  56. onSelect={() =>
  57. state.send('MOVED', {
  58. type: MoveType.ToFront,
  59. })
  60. }
  61. >
  62. <span>Move To Front</span>
  63. <kbd>
  64. <span>{commandKey()}</span>
  65. <span>⇧</span>
  66. <span>]</span>
  67. </kbd>
  68. </Button>
  69. <Button
  70. onSelect={() =>
  71. state.send('MOVED', {
  72. type: MoveType.Forward,
  73. })
  74. }
  75. >
  76. <span>Move Forward</span>
  77. <kbd>
  78. <span>{commandKey()}</span>
  79. <span>]</span>
  80. </kbd>
  81. </Button>
  82. <Button
  83. onSelect={() =>
  84. state.send('MOVED', {
  85. type: MoveType.Backward,
  86. })
  87. }
  88. >
  89. <span>Move Backward</span>
  90. <kbd>
  91. <span>{commandKey()}</span>
  92. <span>[</span>
  93. </kbd>
  94. </Button>
  95. <Button
  96. onSelect={() =>
  97. state.send('MOVED', {
  98. type: MoveType.ToBack,
  99. })
  100. }
  101. >
  102. <span>Move to Back</span>
  103. <kbd>
  104. <span>{commandKey()}</span>
  105. <span>⇧</span>
  106. <span>[</span>
  107. </kbd>
  108. </Button>
  109. {hasGroupSelectd ||
  110. (hasMultipleSelected && (
  111. <>
  112. <StyledDivider />
  113. {hasGroupSelectd && (
  114. <Button onSelect={() => state.send('UNGROUPED')}>
  115. <span>Ungroup</span>
  116. <kbd>
  117. <span>{commandKey()}</span>
  118. <span>⇧</span>
  119. <span>G</span>
  120. </kbd>
  121. </Button>
  122. )}
  123. {hasMultipleSelected && (
  124. <Button onSelect={() => state.send('GROUPED')}>
  125. <span>Group</span>
  126. <kbd>
  127. <span>{commandKey()}</span>
  128. <span>G</span>
  129. </kbd>
  130. </Button>
  131. )}
  132. </>
  133. ))}
  134. <StyledDivider />
  135. {/* <Button onSelect={() => state.send('MOVED_TO_PAGE')}> */}
  136. <MoveToPageDropDown>Move to Page</MoveToPageDropDown>
  137. {/* </Button> */}
  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: 2,
  177. border: '1px solid $panel',
  178. boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
  179. minWidth: 128,
  180. '& kbd': {
  181. marginLeft: '32px',
  182. fontSize: '$1',
  183. fontFamily: '$ui',
  184. },
  185. '& kbd > span': {
  186. display: 'inline-block',
  187. width: '12px',
  188. },
  189. variants: {
  190. isMobile: {
  191. true: {
  192. '& kbd': {
  193. display: 'none',
  194. },
  195. },
  196. },
  197. },
  198. })
  199. const StyledDivider = styled(_ContextMenu.Separator, {
  200. backgroundColor: '$hover',
  201. height: 1,
  202. margin: '2px -2px',
  203. })
  204. function Button({
  205. onSelect,
  206. children,
  207. disabled = false,
  208. }: {
  209. onSelect: () => void
  210. disabled?: boolean
  211. children: React.ReactNode
  212. }) {
  213. return (
  214. <_ContextMenu.Item
  215. as={RowButton}
  216. disabled={disabled}
  217. bp={{ '@initial': 'mobile', '@sm': 'small' }}
  218. onSelect={onSelect}
  219. >
  220. {children}
  221. </_ContextMenu.Item>
  222. )
  223. }
  224. function MoveToPageDropDown({ children }: { children: React.ReactNode }) {
  225. const documentPages = useSelector((s) => s.data.document.pages)
  226. const currentPageId = useSelector((s) => s.data.currentPageId)
  227. if (!documentPages[currentPageId]) return null
  228. const sorted = Object.values(documentPages)
  229. .sort((a, b) => a.childIndex - b.childIndex)
  230. .filter((a) => a.id !== currentPageId)
  231. return (
  232. <_Dropdown.Root>
  233. <_Dropdown.Trigger
  234. as={RowButton}
  235. bp={{ '@initial': 'mobile', '@sm': 'small' }}
  236. >
  237. {children}
  238. </_Dropdown.Trigger>
  239. <StyledDialogContent side="right" sideOffset={8}>
  240. {sorted.map(({ id, name }) => (
  241. <_Dropdown.Item
  242. as={RowButton}
  243. key={id}
  244. bp={{ '@initial': 'mobile', '@sm': 'small' }}
  245. disabled={id === currentPageId}
  246. onSelect={() => state.send('MOVED_TO_PAGE', { id })}
  247. >
  248. <span>{name}</span>
  249. </_Dropdown.Item>
  250. ))}
  251. </StyledDialogContent>
  252. </_Dropdown.Root>
  253. )
  254. }
  255. const StyledDialogContent = styled(_Dropdown.Content, {
  256. // position: 'fixed',
  257. // top: '50%',
  258. // left: '50%',
  259. // transform: 'translate(-50%, -50%)',
  260. // minWidth: 200,
  261. // maxWidth: 'fit-content',
  262. // maxHeight: '85vh',
  263. // marginTop: '-5vh',
  264. minWidth: 128,
  265. backgroundColor: '$panel',
  266. borderRadius: '4px',
  267. overflow: 'hidden',
  268. pointerEvents: 'all',
  269. userSelect: 'none',
  270. zIndex: 200,
  271. padding: 2,
  272. border: '1px solid $panel',
  273. boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
  274. '&:focus': {
  275. outline: 'none',
  276. },
  277. })