ContextMenu.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import { useApp } from '@tldraw/react'
  2. import { MOD_KEY, AlignType, DistributeType } from '@tldraw/core'
  3. import { observer } from 'mobx-react-lite'
  4. import { TablerIcon } from '../icons'
  5. import { Button } from '../Button'
  6. import * as React from 'react'
  7. import * as ReactContextMenu from '@radix-ui/react-context-menu'
  8. import * as Separator from '@radix-ui/react-separator'
  9. const preventDefault = (e: Event) => e.stopPropagation()
  10. interface ContextMenuProps {
  11. children: React.ReactNode
  12. collisionRef: React.RefObject<HTMLDivElement>
  13. }
  14. export const ContextMenu = observer(function ContextMenu({
  15. children,
  16. collisionRef,
  17. }: ContextMenuProps) {
  18. const app = useApp()
  19. const rContent = React.useRef<HTMLDivElement>(null)
  20. const runAndTransition = (f: Function) => {
  21. f()
  22. app.transition('select')
  23. }
  24. return (
  25. <ReactContextMenu.Root>
  26. <ReactContextMenu.Trigger>{children}</ReactContextMenu.Trigger>
  27. <ReactContextMenu.Content
  28. className="tl-menu tl-context-menu"
  29. ref={rContent}
  30. onEscapeKeyDown={() => app.transition('select')}
  31. collisionBoundary={collisionRef.current}
  32. asChild
  33. tabIndex={-1}
  34. >
  35. <div>
  36. {app.selectedShapes?.size > 1 && (
  37. <ReactContextMenu.Item>
  38. <div className="tl-menu-button-row pb-0">
  39. <Button
  40. title="Align left"
  41. onClick={() => runAndTransition(() => app.align(AlignType.Left))}
  42. >
  43. <TablerIcon name="layout-align-left" />
  44. </Button>
  45. <Button
  46. title="Align center horizontally"
  47. onClick={() => runAndTransition(() => app.align(AlignType.CenterHorizontal))}
  48. >
  49. <TablerIcon name="layout-align-center" />
  50. </Button>
  51. <Button
  52. title="Align right"
  53. onClick={() => runAndTransition(() => app.align(AlignType.Right))}
  54. >
  55. <TablerIcon name="layout-align-right" />
  56. </Button>
  57. <Separator.Root className="tl-toolbar-separator" orientation="vertical" />
  58. <Button
  59. title="Distribute horizontally"
  60. onClick={() => runAndTransition(() => app.distribute(DistributeType.Horizontal))}
  61. >
  62. <TablerIcon name="layout-distribute-vertical" />
  63. </Button>
  64. </div>
  65. <div className="tl-menu-button-row pt-0">
  66. <Button
  67. title="Align top"
  68. onClick={() => runAndTransition(() => app.align(AlignType.Top))}
  69. >
  70. <TablerIcon name="layout-align-top" />
  71. </Button>
  72. <Button
  73. title="Align center vertically"
  74. onClick={() => runAndTransition(() => app.align(AlignType.CenterVertical))}
  75. >
  76. <TablerIcon name="layout-align-middle" />
  77. </Button>
  78. <Button
  79. title="Align bottom"
  80. onClick={() => runAndTransition(() => app.align(AlignType.Bottom))}
  81. >
  82. <TablerIcon name="layout-align-bottom" />
  83. </Button>
  84. <Separator.Root className="tl-toolbar-separator" orientation="vertical" />
  85. <Button
  86. title="Distribute vertically"
  87. onClick={() => runAndTransition(() => app.distribute(DistributeType.Vertical))}
  88. >
  89. <TablerIcon name="layout-distribute-horizontal" />
  90. </Button>
  91. </div>
  92. <ReactContextMenu.Separator className="menu-separator" />
  93. </ReactContextMenu.Item>
  94. )}
  95. {app.selectedShapes?.size > 0 && (
  96. <>
  97. <ReactContextMenu.Item
  98. className="tl-menu-item"
  99. onClick={() => runAndTransition(app.cut)}
  100. >
  101. Cut
  102. <div className="tl-menu-right-slot">
  103. <span className="keyboard-shortcut">
  104. <code>{MOD_KEY}</code> <code>X</code>
  105. </span>
  106. </div>
  107. </ReactContextMenu.Item>
  108. <ReactContextMenu.Item
  109. className="tl-menu-item"
  110. onClick={() => runAndTransition(app.copy)}
  111. >
  112. Copy
  113. <div className="tl-menu-right-slot">
  114. <span className="keyboard-shortcut">
  115. <code>{MOD_KEY}</code> <code>C</code>
  116. </span>
  117. </div>
  118. </ReactContextMenu.Item>
  119. </>
  120. )}
  121. <ReactContextMenu.Item
  122. className="tl-menu-item"
  123. onClick={() => runAndTransition(app.paste)}
  124. >
  125. Paste
  126. <div className="tl-menu-right-slot">
  127. <span className="keyboard-shortcut">
  128. <code>{MOD_KEY}</code> <code>V</code>
  129. </span>
  130. </div>
  131. </ReactContextMenu.Item>
  132. <ReactContextMenu.Separator className="menu-separator" />
  133. <ReactContextMenu.Item
  134. className="tl-menu-item"
  135. onClick={() => runAndTransition(app.api.selectAll)}
  136. >
  137. Select all
  138. <div className="tl-menu-right-slot">
  139. <span className="keyboard-shortcut">
  140. <code>{MOD_KEY}</code> <code>A</code>
  141. </span>
  142. </div>
  143. </ReactContextMenu.Item>
  144. {app.selectedShapes?.size > 1 && (
  145. <ReactContextMenu.Item
  146. className="tl-menu-item"
  147. onClick={() => runAndTransition(app.api.deselectAll)}
  148. >
  149. Deselect all
  150. </ReactContextMenu.Item>
  151. )}
  152. {app.selectedShapes?.size > 0 && (
  153. <>
  154. <ReactContextMenu.Item
  155. className="tl-menu-item"
  156. onClick={() => runAndTransition(app.api.deleteShapes)}
  157. >
  158. Delete
  159. <div className="tl-menu-right-slot">
  160. <span className="keyboard-shortcut">
  161. <code>Del</code>
  162. </span>
  163. </div>
  164. </ReactContextMenu.Item>
  165. {app.selectedShapes?.size > 1 && (
  166. <>
  167. <ReactContextMenu.Separator className="menu-separator" />
  168. <ReactContextMenu.Item
  169. className="tl-menu-item"
  170. onClick={() => runAndTransition(app.flipHorizontal)}
  171. >
  172. Flip horizontally
  173. </ReactContextMenu.Item>
  174. <ReactContextMenu.Item
  175. className="tl-menu-item"
  176. onClick={() => runAndTransition(app.flipVertical)}
  177. >
  178. Flip vertically
  179. </ReactContextMenu.Item>
  180. </>
  181. )}
  182. <ReactContextMenu.Separator className="menu-separator" />
  183. <ReactContextMenu.Item
  184. className="tl-menu-item"
  185. onClick={() => runAndTransition(app.bringToFront)}
  186. >
  187. Move to front
  188. <div className="tl-menu-right-slot">
  189. <span className="keyboard-shortcut">
  190. <code>⇧</code> <code>]</code>
  191. </span>
  192. </div>
  193. </ReactContextMenu.Item>
  194. <ReactContextMenu.Item
  195. className="tl-menu-item"
  196. onClick={() => runAndTransition(app.sendToBack)}
  197. >
  198. Move to back
  199. <div className="tl-menu-right-slot">
  200. <span className="keyboard-shortcut">
  201. <code>⇧</code> <code>[</code>
  202. </span>
  203. </div>
  204. </ReactContextMenu.Item>
  205. </>
  206. )}
  207. </div>
  208. </ReactContextMenu.Content>
  209. </ReactContextMenu.Root>
  210. )
  211. })