codemirror-toolbar.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import { memo, useCallback, 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. 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 showSourceToolbar: boolean = getMeta('ol-showSourceToolbar')
  33. const state = useCodeMirrorStateContext()
  34. const view = useCodeMirrorViewContext()
  35. const [overflowed, setOverflowed] = useState(false)
  36. const [collapsed, setCollapsed] = useState(false)
  37. const overflowBeforeRef = useRef<HTMLDivElement>(null)
  38. const overflowedItemsRef = useRef<Set<string>>(new Set())
  39. const {
  40. open: overflowOpen,
  41. onToggle: setOverflowOpen,
  42. ref: overflowRef,
  43. } = useDropdown()
  44. const buildOverflow = useCallback(
  45. (element: Element) => {
  46. setOverflowOpen(false)
  47. setOverflowed(true)
  48. if (overflowBeforeRef.current) {
  49. overflowedItemsRef.current = new Set()
  50. const buttonGroups = [
  51. ...element.querySelectorAll<HTMLDivElement>('[data-overflow]'),
  52. ].reverse()
  53. // restore all the overflowed items
  54. for (const buttonGroup of buttonGroups) {
  55. buttonGroup.classList.remove('overflow-hidden')
  56. }
  57. // find all the available items
  58. for (const buttonGroup of buttonGroups) {
  59. if (element.scrollWidth <= element.clientWidth) {
  60. break
  61. }
  62. // add this item to the overflow
  63. overflowedItemsRef.current.add(buttonGroup.dataset.overflow!)
  64. buttonGroup.classList.add('overflow-hidden')
  65. }
  66. setOverflowed(overflowedItemsRef.current.size > 0)
  67. }
  68. },
  69. [setOverflowOpen]
  70. )
  71. // build when the container resizes
  72. const resizeRef = useResizeObserver(buildOverflow)
  73. const toggleToolbar = useCallback(() => {
  74. setCollapsed(value => !value)
  75. }, [])
  76. if (collapsed) {
  77. return null
  78. }
  79. return (
  80. <div className="ol-cm-toolbar toolbar-editor" ref={resizeRef}>
  81. {showSourceToolbar && <EditorSwitch />}
  82. <ToolbarItems state={state} />
  83. <div
  84. className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch"
  85. ref={overflowBeforeRef}
  86. >
  87. <ToolbarOverflow
  88. overflowed={overflowed}
  89. target={overflowBeforeRef.current ?? undefined}
  90. overflowOpen={overflowOpen}
  91. setOverflowOpen={setOverflowOpen}
  92. overflowRef={overflowRef}
  93. >
  94. <ToolbarItems state={state} overflowed={overflowedItemsRef.current} />
  95. </ToolbarOverflow>
  96. <div className="formatting-buttons-wrapper" />
  97. </div>
  98. <div className="ol-cm-toolbar-button-group ol-cm-toolbar-end">
  99. <ToolbarButton
  100. id="toolbar-toggle-search"
  101. label="Toggle Search"
  102. command={commands.toggleSearch}
  103. active={searchPanelOpen(state)}
  104. icon="search"
  105. />
  106. {!isVisual(view) && (
  107. <SplitTestBadge
  108. splitTestName="source-editor-toolbar"
  109. displayOnVariants={['enabled']}
  110. />
  111. )}
  112. {showSourceToolbar && (
  113. <>
  114. <SwitchToPDFButton />
  115. <DetacherSynctexControl />
  116. <DetachCompileButtonWrapper />
  117. </>
  118. )}
  119. </div>
  120. <div className="ol-cm-toolbar-button-group hidden">
  121. <ToolbarButton
  122. id="toolbar-expand-less"
  123. label="Hide Toolbar"
  124. command={toggleToolbar}
  125. icon="caret-up"
  126. hidden // enable this once there's a way to show the toolbar again
  127. />
  128. </div>
  129. </div>
  130. )
  131. })