layout-dropdown-button.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import { memo, ReactNode, useCallback } from 'react'
  2. import { Dropdown, MenuItem, MenuItemProps } from 'react-bootstrap'
  3. import { Trans, useTranslation } from 'react-i18next'
  4. import Tooltip from '../../../shared/components/tooltip'
  5. import Icon from '../../../shared/components/icon'
  6. import IconChecked from '../../../shared/components/icon-checked'
  7. import ControlledDropdown from '../../../shared/components/controlled-dropdown'
  8. import {
  9. IdeLayout,
  10. IdeView,
  11. useLayoutContext,
  12. } from '../../../shared/context/layout-context'
  13. import * as eventTracking from '../../../infrastructure/event-tracking'
  14. import useEventListener from '../../../shared/hooks/use-event-listener'
  15. import { DetachRole } from '@/shared/context/detach-context'
  16. function IconPlaceholder() {
  17. return <Icon type="" fw />
  18. }
  19. function IconRefresh() {
  20. return <Icon type="refresh" fw spin />
  21. }
  22. function IconLayout() {
  23. return <Icon type="columns" fw />
  24. }
  25. function IconSplit() {
  26. return <Icon type="columns" fw />
  27. }
  28. function IconDetach() {
  29. return <Icon type="window-restore" fw />
  30. }
  31. function IconEditorOnly() {
  32. return <Icon type="code" fw />
  33. }
  34. function IconPdfOnly() {
  35. return <Icon type="file-pdf-o" fw />
  36. }
  37. function IconCheckmark({
  38. iconFor,
  39. pdfLayout,
  40. view,
  41. detachRole,
  42. }: {
  43. iconFor: string
  44. pdfLayout: IdeLayout
  45. view: IdeView | null
  46. detachRole?: DetachRole
  47. }) {
  48. if (detachRole === 'detacher' || view === 'history') {
  49. return <IconPlaceholder />
  50. }
  51. if (
  52. iconFor === 'editorOnly' &&
  53. pdfLayout === 'flat' &&
  54. (view === 'editor' || view === 'file')
  55. ) {
  56. return <IconChecked />
  57. } else if (iconFor === 'pdfOnly' && pdfLayout === 'flat' && view === 'pdf') {
  58. return <IconChecked />
  59. } else if (iconFor === 'sideBySide' && pdfLayout === 'sideBySide') {
  60. return <IconChecked />
  61. }
  62. // return empty icon for placeholder
  63. return <IconPlaceholder />
  64. }
  65. function LayoutMenuItem({
  66. checkmark,
  67. icon,
  68. text,
  69. ...props
  70. }: {
  71. checkmark: ReactNode
  72. icon: ReactNode
  73. text: string | ReactNode
  74. } & MenuItemProps) {
  75. return (
  76. <MenuItem {...props}>
  77. <div className="layout-menu-item">
  78. <div className="layout-menu-item-start">
  79. <div>{checkmark}</div>
  80. <div>{icon}</div>
  81. <div>{text}</div>
  82. </div>
  83. </div>
  84. </MenuItem>
  85. )
  86. }
  87. function DetachDisabled() {
  88. const { t } = useTranslation()
  89. return (
  90. <Tooltip
  91. id="detach-disabled"
  92. description={t('your_browser_does_not_support_this_feature')}
  93. overlayProps={{ placement: 'left' }}
  94. >
  95. <LayoutMenuItem
  96. disabled
  97. checkmark={<IconPlaceholder />}
  98. icon={<IconDetach />}
  99. text={t('pdf_in_separate_tab')}
  100. />
  101. </Tooltip>
  102. )
  103. }
  104. function LayoutDropdownButton() {
  105. const { t } = useTranslation()
  106. const {
  107. reattach,
  108. detach,
  109. detachIsLinked,
  110. detachRole,
  111. changeLayout,
  112. view,
  113. pdfLayout,
  114. } = useLayoutContext()
  115. const handleDetach = useCallback(() => {
  116. detach()
  117. eventTracking.sendMB('project-layout-detach')
  118. }, [detach])
  119. const handleReattach = useCallback(() => {
  120. if (detachRole !== 'detacher') {
  121. return
  122. }
  123. reattach()
  124. eventTracking.sendMB('project-layout-reattach')
  125. }, [detachRole, reattach])
  126. // reattach when the PDF pane opens
  127. useEventListener('ui:pdf-open', handleReattach)
  128. const handleChangeLayout = useCallback(
  129. (newLayout: IdeLayout, newView?: IdeView) => {
  130. handleReattach()
  131. changeLayout(newLayout, newView)
  132. eventTracking.sendMB('project-layout-change', {
  133. layout: newLayout,
  134. view: newView,
  135. })
  136. },
  137. [changeLayout, handleReattach]
  138. )
  139. const processing = !detachIsLinked && detachRole === 'detacher'
  140. // bsStyle is required for Dropdown.Toggle, but we will override style
  141. return (
  142. <>
  143. {processing && (
  144. <div aria-live="assertive" className="sr-only">
  145. {t('layout_processing')}
  146. </div>
  147. )}
  148. <ControlledDropdown
  149. id="layout-dropdown"
  150. onMainButtonClick={() => {
  151. eventTracking.sendMB('navigation-clicked-layout')
  152. }}
  153. className="toolbar-item layout-dropdown"
  154. pullRight
  155. >
  156. <Dropdown.Toggle className="btn-full-height" bsStyle="link">
  157. {processing ? <IconRefresh /> : <IconLayout />}
  158. <span className="toolbar-label">{t('layout')}</span>
  159. </Dropdown.Toggle>
  160. <Dropdown.Menu className="layout-dropdown-list">
  161. <LayoutMenuItem
  162. onSelect={() => handleChangeLayout('sideBySide')}
  163. checkmark={
  164. <IconCheckmark
  165. iconFor="sideBySide"
  166. pdfLayout={pdfLayout}
  167. view={view}
  168. detachRole={detachRole}
  169. />
  170. }
  171. icon={<IconSplit />}
  172. text={t('editor_and_pdf')}
  173. />
  174. <LayoutMenuItem
  175. onSelect={() => handleChangeLayout('flat', 'editor')}
  176. checkmark={
  177. <IconCheckmark
  178. iconFor="editorOnly"
  179. pdfLayout={pdfLayout}
  180. view={view}
  181. detachRole={detachRole}
  182. />
  183. }
  184. icon={<IconEditorOnly />}
  185. text={
  186. <Trans
  187. i18nKey="editor_only_hide_pdf"
  188. components={[
  189. <span key="editor_only_hide_pdf" className="subdued" />,
  190. ]}
  191. />
  192. }
  193. />
  194. <LayoutMenuItem
  195. onSelect={() => handleChangeLayout('flat', 'pdf')}
  196. checkmark={
  197. <IconCheckmark
  198. iconFor="pdfOnly"
  199. pdfLayout={pdfLayout}
  200. view={view}
  201. detachRole={detachRole}
  202. />
  203. }
  204. icon={<IconPdfOnly />}
  205. text={
  206. <Trans
  207. i18nKey="pdf_only_hide_editor"
  208. components={[
  209. <span key="pdf_only_hide_editor" className="subdued" />,
  210. ]}
  211. />
  212. }
  213. />
  214. {'BroadcastChannel' in window ? (
  215. <LayoutMenuItem
  216. onSelect={() => handleDetach()}
  217. checkmark={
  218. detachRole === 'detacher' ? (
  219. detachIsLinked ? (
  220. <IconChecked />
  221. ) : (
  222. <IconRefresh />
  223. )
  224. ) : (
  225. <IconPlaceholder />
  226. )
  227. }
  228. icon={<IconDetach />}
  229. text={t('pdf_in_separate_tab')}
  230. />
  231. ) : (
  232. <DetachDisabled />
  233. )}
  234. </Dropdown.Menu>
  235. </ControlledDropdown>
  236. </>
  237. )
  238. }
  239. export default memo(LayoutDropdownButton)