codemirror-toolbar.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import { memo, useCallback, useEffect, useRef, useState } from 'react'
  2. import { createPortal } from 'react-dom'
  3. import {
  4. useCodeMirrorStateContext,
  5. useCodeMirrorViewContext,
  6. } from './codemirror-context'
  7. import { useResizeObserver } from '@/shared/hooks/use-resize-observer'
  8. import { ToolbarItems } from './toolbar/toolbar-items'
  9. import { ToolbarOverflow } from './toolbar/overflow'
  10. import useDropdown from '../../../shared/hooks/use-dropdown'
  11. import { getPanel } from '@codemirror/view'
  12. import { createToolbarPanel } from '../extensions/toolbar/toolbar-panel'
  13. import EditorSwitch from './editor-switch'
  14. import SwitchToPDFButton from './switch-to-pdf-button'
  15. import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control'
  16. import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper'
  17. import { isVisual } from '../extensions/visual/visual'
  18. import { language } from '@codemirror/language'
  19. import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors'
  20. import { debugConsole } from '@/utils/debugging'
  21. import { useTranslation } from 'react-i18next'
  22. import { ToggleSearchButton } from '@/features/source-editor/components/toolbar/toggle-search-button'
  23. import ReviewPanelHeader from '@/features/review-panel-new/components/review-panel-header'
  24. import useReviewPanelLayout from '@/features/review-panel-new/hooks/use-review-panel-layout'
  25. import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
  26. import Breadcrumbs from '@/features/ide-redesign/components/breadcrumbs'
  27. import classNames from 'classnames'
  28. import { useUserSettingsContext } from '@/shared/context/user-settings-context'
  29. import { useFeatureFlag } from '@/shared/context/split-test-context'
  30. export const CodeMirrorToolbar = () => {
  31. const view = useCodeMirrorViewContext()
  32. const panel = getPanel(view, createToolbarPanel)
  33. if (!panel) {
  34. return null
  35. }
  36. return createPortal(<Toolbar />, panel.dom)
  37. }
  38. const Toolbar = memo(function Toolbar() {
  39. const { t } = useTranslation()
  40. const state = useCodeMirrorStateContext()
  41. const view = useCodeMirrorViewContext()
  42. const {
  43. userSettings: { breadcrumbs },
  44. } = useUserSettingsContext()
  45. const visualPreviewEnabled = useFeatureFlag('visual-preview')
  46. const [overflowed, setOverflowed] = useState(false)
  47. const overflowedItemsRef = useRef<Set<string>>(new Set())
  48. const languageName = state.facet(language)?.name
  49. const visual = isVisual(view)
  50. const listDepth = minimumListDepthForSelection(state)
  51. const newEditor = useIsNewEditorEnabled()
  52. const { showHeader: showReviewPanelHeader } = useReviewPanelLayout()
  53. const {
  54. open: overflowOpen,
  55. onToggle: setOverflowOpen,
  56. ref: overflowRef,
  57. } = useDropdown()
  58. const buildOverflow = useCallback(
  59. (element: Element) => {
  60. debugConsole.log('recalculating toolbar overflow')
  61. setOverflowOpen(false)
  62. setOverflowed(true)
  63. overflowedItemsRef.current = new Set()
  64. const buttonGroups = [
  65. ...element.querySelectorAll<HTMLDivElement>('[data-overflow]'),
  66. ].reverse()
  67. // restore all the overflowed items
  68. for (const buttonGroup of buttonGroups) {
  69. buttonGroup.classList.remove('overflow-hidden')
  70. }
  71. // find all the available items
  72. for (const buttonGroup of buttonGroups) {
  73. if (element.scrollWidth <= element.clientWidth) {
  74. break
  75. }
  76. // add this item to the overflow
  77. overflowedItemsRef.current.add(buttonGroup.dataset.overflow!)
  78. buttonGroup.classList.add('overflow-hidden')
  79. }
  80. setOverflowed(overflowedItemsRef.current.size > 0)
  81. },
  82. [setOverflowOpen]
  83. )
  84. // calculate overflow when the container resizes
  85. const { elementRef, resizeRef } = useResizeObserver(buildOverflow)
  86. // calculate overflow when `languageName` or `visual` change
  87. useEffect(() => {
  88. if (resizeRef.current) {
  89. buildOverflow(resizeRef.current.element)
  90. }
  91. }, [buildOverflow, languageName, resizeRef, visual])
  92. // calculate overflow when buttons change
  93. const observerRef = useRef<MutationObserver | null>(null)
  94. const handleButtons = useCallback(
  95. (node: HTMLDivElement) => {
  96. if (!('MutationObserver' in window)) {
  97. return
  98. }
  99. if (node) {
  100. observerRef.current = new MutationObserver(() => {
  101. if (resizeRef.current) {
  102. buildOverflow(resizeRef.current.element)
  103. }
  104. })
  105. observerRef.current.observe(node, { childList: true })
  106. } else if (observerRef.current) {
  107. observerRef.current.disconnect()
  108. }
  109. },
  110. [buildOverflow, resizeRef]
  111. )
  112. // calculate overflow when active element changes to/from inside a table
  113. const insideTable = document.activeElement?.closest(
  114. '.table-generator-help-modal,.table-generator,.table-generator-width-modal'
  115. )
  116. useEffect(() => {
  117. if (resizeRef.current) {
  118. buildOverflow(resizeRef.current.element)
  119. }
  120. }, [buildOverflow, insideTable, resizeRef])
  121. const showActions = !state.readOnly && !insideTable
  122. return (
  123. <>
  124. {newEditor && showReviewPanelHeader && <ReviewPanelHeader />}
  125. <div
  126. id="ol-cm-toolbar-wrapper"
  127. className={classNames('ol-cm-toolbar-wrapper', {
  128. 'ol-cm-toolbar-wrapper-indented': newEditor && showReviewPanelHeader,
  129. })}
  130. >
  131. <div
  132. role="toolbar"
  133. aria-label={t('toolbar_editor')}
  134. className="ol-cm-toolbar toolbar-editor"
  135. ref={elementRef}
  136. >
  137. {!visualPreviewEnabled && <EditorSwitch />}
  138. {showActions && (
  139. <ToolbarItems
  140. state={state}
  141. languageName={languageName}
  142. visual={visual}
  143. listDepth={listDepth}
  144. />
  145. )}
  146. <div className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch">
  147. {showActions && (
  148. <ToolbarOverflow
  149. overflowed={overflowed}
  150. overflowOpen={overflowOpen}
  151. setOverflowOpen={setOverflowOpen}
  152. overflowRef={overflowRef}
  153. >
  154. <ToolbarItems
  155. state={state}
  156. overflowed={overflowedItemsRef.current}
  157. languageName={languageName}
  158. visual={visual}
  159. listDepth={listDepth}
  160. />
  161. </ToolbarOverflow>
  162. )}
  163. </div>
  164. <div
  165. className="ol-cm-toolbar-button-group ol-cm-toolbar-end"
  166. ref={handleButtons}
  167. >
  168. <ToggleSearchButton state={state} />
  169. <SwitchToPDFButton />
  170. <DetacherSynctexControl />
  171. <DetachCompileButtonWrapper />
  172. </div>
  173. </div>
  174. {newEditor && breadcrumbs && <Breadcrumbs />}
  175. </div>
  176. </>
  177. )
  178. })