layout-dropdown-button.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import { memo, useCallback, forwardRef } from 'react'
  2. import { Spinner } from 'react-bootstrap'
  3. import {
  4. Dropdown,
  5. DropdownItem,
  6. DropdownMenu,
  7. DropdownToggle,
  8. DropdownToggleCustom,
  9. } from '@/features/ui/components/bootstrap-5/dropdown-menu'
  10. import { Trans, useTranslation } from 'react-i18next'
  11. import {
  12. IdeLayout,
  13. IdeView,
  14. useLayoutContext,
  15. } from '../../../shared/context/layout-context'
  16. import * as eventTracking from '../../../infrastructure/event-tracking'
  17. import useEventListener from '../../../shared/hooks/use-event-listener'
  18. import { DetachRole } from '@/shared/context/detach-context'
  19. import MaterialIcon from '@/shared/components/material-icon'
  20. import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
  21. const isActiveDropdownItem = ({
  22. iconFor,
  23. pdfLayout,
  24. view,
  25. detachRole,
  26. }: {
  27. iconFor: string
  28. pdfLayout: IdeLayout
  29. view: IdeView | null
  30. detachRole?: DetachRole
  31. }) => {
  32. if (detachRole === 'detacher' || view === 'history') {
  33. return false
  34. }
  35. if (
  36. iconFor === 'editorOnly' &&
  37. pdfLayout === 'flat' &&
  38. (view === 'editor' || view === 'file')
  39. ) {
  40. return true
  41. } else if (iconFor === 'pdfOnly' && pdfLayout === 'flat' && view === 'pdf') {
  42. return true
  43. } else if (iconFor === 'sideBySide' && pdfLayout === 'sideBySide') {
  44. return true
  45. }
  46. return false
  47. }
  48. function EnhancedDropdownItem({
  49. active,
  50. ...props
  51. }: React.ComponentProps<typeof DropdownItem>) {
  52. return (
  53. <DropdownItem
  54. active={active}
  55. aria-current={active}
  56. trailingIcon={active ? 'check' : null}
  57. {...props}
  58. />
  59. )
  60. }
  61. const LayoutDropdownToggleButton = forwardRef<
  62. HTMLButtonElement,
  63. {
  64. onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
  65. }
  66. >(({ onClick, ...props }, ref) => {
  67. const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  68. eventTracking.sendMB('navigation-clicked-layout')
  69. onClick(e)
  70. }
  71. return <DropdownToggleCustom {...props} ref={ref} onClick={handleClick} />
  72. })
  73. LayoutDropdownToggleButton.displayName = 'LayoutDropdownToggleButton'
  74. function BS5DetachDisabled() {
  75. const { t } = useTranslation()
  76. return (
  77. <OLTooltip
  78. id="detach-disabled"
  79. description={t('your_browser_does_not_support_this_feature')}
  80. overlayProps={{ placement: 'left' }}
  81. >
  82. <span>
  83. <EnhancedDropdownItem disabled leadingIcon="select_window">
  84. {t('pdf_in_separate_tab')}
  85. </EnhancedDropdownItem>
  86. </span>
  87. </OLTooltip>
  88. )
  89. }
  90. function LayoutDropdownButton() {
  91. const {
  92. reattach,
  93. detach,
  94. detachIsLinked,
  95. detachRole,
  96. changeLayout,
  97. view,
  98. pdfLayout,
  99. } = useLayoutContext()
  100. const handleDetach = useCallback(() => {
  101. detach()
  102. eventTracking.sendMB('project-layout-detach')
  103. }, [detach])
  104. const handleReattach = useCallback(() => {
  105. if (detachRole !== 'detacher') {
  106. return
  107. }
  108. reattach()
  109. eventTracking.sendMB('project-layout-reattach')
  110. }, [detachRole, reattach])
  111. // reattach when the PDF pane opens
  112. useEventListener('ui:pdf-open', handleReattach)
  113. const handleChangeLayout = useCallback(
  114. (newLayout: IdeLayout, newView?: IdeView) => {
  115. handleReattach()
  116. changeLayout(newLayout, newView)
  117. eventTracking.sendMB('project-layout-change', {
  118. layout: newLayout,
  119. view: newView,
  120. })
  121. },
  122. [changeLayout, handleReattach]
  123. )
  124. return (
  125. <LayoutDropdownButtonUi
  126. processing={!detachIsLinked && detachRole === 'detacher'}
  127. handleChangeLayout={handleChangeLayout}
  128. handleDetach={handleDetach}
  129. detachIsLinked={detachIsLinked}
  130. detachRole={detachRole}
  131. pdfLayout={pdfLayout}
  132. view={view}
  133. detachable={'BroadcastChannel' in window}
  134. />
  135. )
  136. }
  137. type LayoutDropdownButtonUiProps = {
  138. processing: boolean
  139. handleChangeLayout: (newLayout: IdeLayout, newView?: IdeView) => void
  140. handleDetach: () => void
  141. detachIsLinked: boolean
  142. detachRole: DetachRole
  143. pdfLayout: IdeLayout
  144. view: IdeView | null
  145. detachable: boolean
  146. }
  147. export const LayoutDropdownButtonUi = ({
  148. processing,
  149. handleChangeLayout,
  150. handleDetach,
  151. detachIsLinked,
  152. detachRole,
  153. view,
  154. pdfLayout,
  155. detachable,
  156. }: LayoutDropdownButtonUiProps) => {
  157. const { t } = useTranslation()
  158. return (
  159. <>
  160. {processing && (
  161. <div aria-live="assertive" className="visually-hidden">
  162. {t('layout_processing')}
  163. </div>
  164. )}
  165. <Dropdown className="toolbar-item layout-dropdown" align="end">
  166. <DropdownToggle
  167. id="layout-dropdown-btn"
  168. className="btn-full-height"
  169. as={LayoutDropdownToggleButton}
  170. >
  171. {processing ? (
  172. <Spinner
  173. animation="border"
  174. aria-hidden="true"
  175. size="sm"
  176. role="status"
  177. />
  178. ) : (
  179. <MaterialIcon type="dock_to_right" className="align-middle" />
  180. )}
  181. <span className="toolbar-label">{t('layout')}</span>
  182. </DropdownToggle>
  183. <DropdownMenu>
  184. <EnhancedDropdownItem
  185. onClick={() => handleChangeLayout('sideBySide')}
  186. active={isActiveDropdownItem({
  187. iconFor: 'sideBySide',
  188. pdfLayout,
  189. view,
  190. detachRole,
  191. })}
  192. leadingIcon="dock_to_right"
  193. >
  194. {t('editor_and_pdf')}
  195. </EnhancedDropdownItem>
  196. <EnhancedDropdownItem
  197. onClick={() => handleChangeLayout('flat', 'editor')}
  198. active={isActiveDropdownItem({
  199. iconFor: 'editorOnly',
  200. pdfLayout,
  201. view,
  202. detachRole,
  203. })}
  204. leadingIcon="code"
  205. >
  206. <div className="d-flex flex-column">
  207. <Trans
  208. i18nKey="editor_only_hide_pdf"
  209. components={[
  210. <span key="editor_only_hide_pdf" className="subdued" />,
  211. ]}
  212. />
  213. </div>
  214. </EnhancedDropdownItem>
  215. <EnhancedDropdownItem
  216. onClick={() => handleChangeLayout('flat', 'pdf')}
  217. active={isActiveDropdownItem({
  218. iconFor: 'pdfOnly',
  219. pdfLayout,
  220. view,
  221. detachRole,
  222. })}
  223. leadingIcon="picture_as_pdf"
  224. >
  225. <div className="d-flex flex-column">
  226. <Trans
  227. i18nKey="pdf_only_hide_editor"
  228. components={[
  229. <span key="pdf_only_hide_editor" className="subdued" />,
  230. ]}
  231. />
  232. </div>
  233. </EnhancedDropdownItem>
  234. {detachable ? (
  235. <EnhancedDropdownItem
  236. onClick={() => handleDetach()}
  237. active={detachRole === 'detacher' && detachIsLinked}
  238. trailingIcon={
  239. detachRole === 'detacher' ? (
  240. detachIsLinked ? (
  241. 'check'
  242. ) : (
  243. <span className="spinner-container">
  244. <Spinner
  245. animation="border"
  246. aria-hidden="true"
  247. size="sm"
  248. role="status"
  249. />
  250. <span className="visually-hidden">{t('loading')}</span>
  251. </span>
  252. )
  253. ) : null
  254. }
  255. leadingIcon="select_window"
  256. >
  257. {t('pdf_in_separate_tab')}
  258. </EnhancedDropdownItem>
  259. ) : (
  260. <BS5DetachDisabled />
  261. )}
  262. </DropdownMenu>
  263. </Dropdown>
  264. </>
  265. )
  266. }
  267. export default memo(LayoutDropdownButton)