codemirror-toolbar.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { memo, useCallback, useEffect, useRef, useState } from 'react'
  2. import { createPortal } from 'react-dom'
  3. import {
  4. useCodeMirrorStateContext,
  5. useCodeMirrorViewContext,
  6. } from './codemirror-editor'
  7. import { searchPanelOpen } from '@codemirror/search'
  8. import { useResizeObserver } from '../../../shared/hooks/use-resize-observer'
  9. import { ToolbarButton } from './toolbar/toolbar-button'
  10. import { ToolbarItems } from './toolbar/toolbar-items'
  11. import * as commands from '../extensions/toolbar/commands'
  12. import { ToolbarOverflow } from './toolbar/overflow'
  13. import useDropdown from '../../../shared/hooks/use-dropdown'
  14. import { getPanel } from '@codemirror/view'
  15. import { createToolbarPanel } from '../extensions/toolbar/toolbar-panel'
  16. import EditorSwitch from './editor-switch'
  17. import SwitchToPDFButton from './switch-to-pdf-button'
  18. import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control'
  19. import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper'
  20. import getMeta from '../../../utils/meta'
  21. import { isVisual } from '../extensions/visual/visual'
  22. import SplitTestBadge from '../../../shared/components/split-test-badge'
  23. import { language } from '@codemirror/language'
  24. import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors'
  25. export const CodeMirrorToolbar = () => {
  26. const view = useCodeMirrorViewContext()
  27. const panel = getPanel(view, createToolbarPanel)
  28. if (!panel) {
  29. return null
  30. }
  31. return createPortal(<Toolbar />, panel.dom)
  32. }
  33. const Toolbar = memo(function Toolbar() {
  34. const showSourceToolbar: boolean = getMeta('ol-showSourceToolbar')
  35. const state = useCodeMirrorStateContext()
  36. const view = useCodeMirrorViewContext()
  37. const [overflowed, setOverflowed] = useState(false)
  38. const [collapsed, setCollapsed] = useState(false)
  39. const overflowedItemsRef = useRef<Set<string>>(new Set())
  40. const languageName = state.facet(language)?.name
  41. const visual = isVisual(view)
  42. const listDepth = minimumListDepthForSelection(state)
  43. const {
  44. open: overflowOpen,
  45. onToggle: setOverflowOpen,
  46. ref: overflowRef,
  47. } = useDropdown()
  48. const buildOverflow = useCallback(
  49. (element: Element) => {
  50. setOverflowOpen(false)
  51. setOverflowed(true)
  52. overflowedItemsRef.current = new Set()
  53. const buttonGroups = [
  54. ...element.querySelectorAll<HTMLDivElement>('[data-overflow]'),
  55. ].reverse()
  56. // restore all the overflowed items
  57. for (const buttonGroup of buttonGroups) {
  58. buttonGroup.classList.remove('overflow-hidden')
  59. }
  60. // find all the available items
  61. for (const buttonGroup of buttonGroups) {
  62. if (element.scrollWidth <= element.clientWidth) {
  63. break
  64. }
  65. // add this item to the overflow
  66. overflowedItemsRef.current.add(buttonGroup.dataset.overflow!)
  67. buttonGroup.classList.add('overflow-hidden')
  68. }
  69. setOverflowed(overflowedItemsRef.current.size > 0)
  70. },
  71. [setOverflowOpen]
  72. )
  73. // calculate overflow when the container resizes
  74. const { elementRef, resizeRef } = useResizeObserver(buildOverflow)
  75. // calculate overflow when `languageName` or `visual` change
  76. useEffect(() => {
  77. if (resizeRef.current) {
  78. buildOverflow(resizeRef.current.element)
  79. }
  80. }, [buildOverflow, languageName, resizeRef, visual])
  81. const toggleToolbar = useCallback(() => {
  82. setCollapsed(value => !value)
  83. }, [])
  84. if (collapsed) {
  85. return null
  86. }
  87. return (
  88. <div className="ol-cm-toolbar toolbar-editor" ref={elementRef}>
  89. {showSourceToolbar && <EditorSwitch />}
  90. <ToolbarItems
  91. state={state}
  92. languageName={languageName}
  93. visual={visual}
  94. listDepth={listDepth}
  95. />
  96. <div className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch">
  97. <ToolbarOverflow
  98. overflowed={overflowed}
  99. overflowOpen={overflowOpen}
  100. setOverflowOpen={setOverflowOpen}
  101. overflowRef={overflowRef}
  102. >
  103. <ToolbarItems
  104. state={state}
  105. overflowed={overflowedItemsRef.current}
  106. languageName={languageName}
  107. visual={visual}
  108. listDepth={listDepth}
  109. />
  110. </ToolbarOverflow>
  111. <div className="formatting-buttons-wrapper" />
  112. </div>
  113. <div className="ol-cm-toolbar-button-group ol-cm-toolbar-end">
  114. <ToolbarButton
  115. id="toolbar-toggle-search"
  116. label="Toggle Search"
  117. command={commands.toggleSearch}
  118. active={searchPanelOpen(state)}
  119. icon="search"
  120. />
  121. {languageName === 'latex' && !visual && (
  122. <SplitTestBadge
  123. splitTestName="source-editor-toolbar"
  124. displayOnVariants={['enabled']}
  125. />
  126. )}
  127. {showSourceToolbar && (
  128. <>
  129. <SwitchToPDFButton />
  130. <DetacherSynctexControl />
  131. <DetachCompileButtonWrapper />
  132. </>
  133. )}
  134. </div>
  135. <div className="ol-cm-toolbar-button-group hidden">
  136. <ToolbarButton
  137. id="toolbar-expand-less"
  138. label="Hide Toolbar"
  139. command={toggleToolbar}
  140. icon="caret-up"
  141. hidden // enable this once there's a way to show the toolbar again
  142. />
  143. </div>
  144. </div>
  145. )
  146. })