codemirror-toolbar.tsx 6.0 KB

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