codemirror-toolbar.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. export const CodeMirrorToolbar = () => {
  24. const view = useCodeMirrorViewContext()
  25. const panel = getPanel(view, createToolbarPanel)
  26. if (!panel) {
  27. return null
  28. }
  29. return createPortal(<Toolbar />, panel.dom)
  30. }
  31. const Toolbar = memo(function Toolbar() {
  32. const { t } = useTranslation()
  33. const state = useCodeMirrorStateContext()
  34. const view = useCodeMirrorViewContext()
  35. const [overflowed, setOverflowed] = useState(false)
  36. const overflowedItemsRef = useRef<Set<string>>(new Set())
  37. const languageName = state.facet(language)?.name
  38. const visual = isVisual(view)
  39. const listDepth = minimumListDepthForSelection(state)
  40. const {
  41. open: overflowOpen,
  42. onToggle: setOverflowOpen,
  43. ref: overflowRef,
  44. } = useDropdown()
  45. const buildOverflow = useCallback(
  46. (element: Element) => {
  47. debugConsole.log('recalculating toolbar overflow')
  48. setOverflowOpen(false)
  49. setOverflowed(true)
  50. overflowedItemsRef.current = new Set()
  51. const buttonGroups = [
  52. ...element.querySelectorAll<HTMLDivElement>('[data-overflow]'),
  53. ].reverse()
  54. // restore all the overflowed items
  55. for (const buttonGroup of buttonGroups) {
  56. buttonGroup.classList.remove('overflow-hidden')
  57. }
  58. // find all the available items
  59. for (const buttonGroup of buttonGroups) {
  60. if (element.scrollWidth <= element.clientWidth) {
  61. break
  62. }
  63. // add this item to the overflow
  64. overflowedItemsRef.current.add(buttonGroup.dataset.overflow!)
  65. buttonGroup.classList.add('overflow-hidden')
  66. }
  67. setOverflowed(overflowedItemsRef.current.size > 0)
  68. },
  69. [setOverflowOpen]
  70. )
  71. // calculate overflow when the container resizes
  72. const { elementRef, resizeRef } = useResizeObserver(buildOverflow)
  73. // calculate overflow when `languageName` or `visual` change
  74. useEffect(() => {
  75. if (resizeRef.current) {
  76. buildOverflow(resizeRef.current.element)
  77. }
  78. }, [buildOverflow, languageName, resizeRef, visual])
  79. // calculate overflow when buttons change
  80. const observerRef = useRef<MutationObserver | null>(null)
  81. const handleButtons = useCallback(
  82. node => {
  83. if (!('MutationObserver' in window)) {
  84. return
  85. }
  86. if (node) {
  87. observerRef.current = new MutationObserver(() => {
  88. if (resizeRef.current) {
  89. buildOverflow(resizeRef.current.element)
  90. }
  91. })
  92. observerRef.current.observe(node, { childList: true })
  93. } else if (observerRef.current) {
  94. observerRef.current.disconnect()
  95. }
  96. },
  97. [buildOverflow, resizeRef]
  98. )
  99. // calculate overflow when active element changes to/from inside a table
  100. const insideTable = document.activeElement?.closest(
  101. '.table-generator-help-modal,.table-generator,.table-generator-width-modal'
  102. )
  103. useEffect(() => {
  104. if (resizeRef.current) {
  105. buildOverflow(resizeRef.current.element)
  106. }
  107. }, [buildOverflow, insideTable, resizeRef])
  108. const showActions = !state.readOnly && !insideTable
  109. return (
  110. <div
  111. role="toolbar"
  112. aria-label={t('toolbar_editor')}
  113. className="ol-cm-toolbar toolbar-editor"
  114. ref={elementRef}
  115. >
  116. <EditorSwitch />
  117. {showActions && (
  118. <ToolbarItems
  119. state={state}
  120. languageName={languageName}
  121. visual={visual}
  122. listDepth={listDepth}
  123. />
  124. )}
  125. <div className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch">
  126. {showActions && (
  127. <ToolbarOverflow
  128. overflowed={overflowed}
  129. overflowOpen={overflowOpen}
  130. setOverflowOpen={setOverflowOpen}
  131. overflowRef={overflowRef}
  132. >
  133. <ToolbarItems
  134. state={state}
  135. overflowed={overflowedItemsRef.current}
  136. languageName={languageName}
  137. visual={visual}
  138. listDepth={listDepth}
  139. />
  140. </ToolbarOverflow>
  141. )}
  142. </div>
  143. <div
  144. className="ol-cm-toolbar-button-group ol-cm-toolbar-end"
  145. ref={handleButtons}
  146. >
  147. <ToggleSearchButton state={state} />
  148. <SwitchToPDFButton />
  149. <DetacherSynctexControl />
  150. <DetachCompileButtonWrapper />
  151. </div>
  152. </div>
  153. )
  154. })