layout-dropdown-button.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import { memo, useCallback, forwardRef } from 'react'
  2. import {
  3. Dropdown,
  4. DropdownItem,
  5. DropdownMenu,
  6. DropdownToggle,
  7. DropdownToggleCustom,
  8. } from '@/shared/components/dropdown/dropdown-menu'
  9. import { Trans, useTranslation } from 'react-i18next'
  10. import {
  11. IdeLayout,
  12. IdeView,
  13. useLayoutContext,
  14. } from '../../../shared/context/layout-context'
  15. import * as eventTracking from '../../../infrastructure/event-tracking'
  16. import useEventListener from '../../../shared/hooks/use-event-listener'
  17. import { DetachRole } from '@/shared/context/detach-context'
  18. import MaterialIcon from '@/shared/components/material-icon'
  19. import OLTooltip from '@/shared/components/ol/ol-tooltip'
  20. import OLSpinner from '@/shared/components/ol/ol-spinner'
  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. <div aria-live="assertive" className="visually-hidden" id="layout-status">
  161. {processing ? t('layout_processing') : ''}
  162. </div>
  163. <Dropdown className="toolbar-item layout-dropdown" align="end">
  164. <DropdownToggle
  165. aria-describedby={processing ? 'layout-status' : undefined}
  166. id="layout-dropdown-btn"
  167. className="btn-full-height"
  168. as={LayoutDropdownToggleButton}
  169. >
  170. {processing ? (
  171. <OLSpinner size="sm" />
  172. ) : (
  173. <MaterialIcon type="dock_to_right" className="align-middle" />
  174. )}
  175. <span className="toolbar-label">{t('layout')}</span>
  176. </DropdownToggle>
  177. <DropdownMenu>
  178. <EnhancedDropdownItem
  179. onClick={() => handleChangeLayout('sideBySide')}
  180. active={isActiveDropdownItem({
  181. iconFor: 'sideBySide',
  182. pdfLayout,
  183. view,
  184. detachRole,
  185. })}
  186. leadingIcon="dock_to_right"
  187. >
  188. {t('editor_and_pdf')}
  189. </EnhancedDropdownItem>
  190. <EnhancedDropdownItem
  191. onClick={() => handleChangeLayout('flat', 'editor')}
  192. active={isActiveDropdownItem({
  193. iconFor: 'editorOnly',
  194. pdfLayout,
  195. view,
  196. detachRole,
  197. })}
  198. leadingIcon="code"
  199. >
  200. <div className="d-flex flex-column">
  201. <Trans
  202. i18nKey="editor_only_hide_pdf"
  203. components={[
  204. <span key="editor_only_hide_pdf" className="subdued" />,
  205. ]}
  206. />
  207. </div>
  208. </EnhancedDropdownItem>
  209. <EnhancedDropdownItem
  210. onClick={() => handleChangeLayout('flat', 'pdf')}
  211. active={isActiveDropdownItem({
  212. iconFor: 'pdfOnly',
  213. pdfLayout,
  214. view,
  215. detachRole,
  216. })}
  217. leadingIcon="picture_as_pdf"
  218. >
  219. <div className="d-flex flex-column">
  220. <Trans
  221. i18nKey="pdf_only_hide_editor"
  222. components={[
  223. <span key="pdf_only_hide_editor" className="subdued" />,
  224. ]}
  225. />
  226. </div>
  227. </EnhancedDropdownItem>
  228. {detachable ? (
  229. <EnhancedDropdownItem
  230. onClick={() => handleDetach()}
  231. active={detachRole === 'detacher' && detachIsLinked}
  232. trailingIcon={
  233. detachRole === 'detacher' ? (
  234. detachIsLinked ? (
  235. 'check'
  236. ) : (
  237. <OLSpinner size="sm" />
  238. )
  239. ) : null
  240. }
  241. leadingIcon="select_window"
  242. >
  243. {t('pdf_in_separate_tab')}
  244. </EnhancedDropdownItem>
  245. ) : (
  246. <BS5DetachDisabled />
  247. )}
  248. </DropdownMenu>
  249. </Dropdown>
  250. </>
  251. )
  252. }
  253. export default memo(LayoutDropdownButton)