codemirror-toolbar.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  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. export const CodeMirrorToolbar = () => {
  17. const view = useCodeMirrorViewContext()
  18. const panel = getPanel(view, createToolbarPanel)
  19. if (!panel) {
  20. return null
  21. }
  22. return createPortal(<Toolbar />, panel.dom)
  23. }
  24. const Toolbar = memo(function Toolbar() {
  25. const state = useCodeMirrorStateContext()
  26. const [overflowed, setOverflowed] = useState(false)
  27. const [collapsed, setCollapsed] = useState(false)
  28. const overflowBeforeRef = useRef<HTMLDivElement>(null)
  29. const overflowedItemsRef = useRef<Set<string>>(new Set())
  30. const {
  31. open: overflowOpen,
  32. onToggle: setOverflowOpen,
  33. ref: overflowRef,
  34. } = useDropdown()
  35. const buildOverflow = useCallback(
  36. (element: Element) => {
  37. setOverflowOpen(false)
  38. setOverflowed(false)
  39. if (overflowBeforeRef.current) {
  40. overflowedItemsRef.current = new Set()
  41. const buttonGroups = [
  42. ...element.querySelectorAll<HTMLDivElement>('[data-overflow]'),
  43. ].reverse()
  44. // restore all the overflowed items
  45. for (const buttonGroup of buttonGroups) {
  46. buttonGroup.classList.remove('overflow-hidden')
  47. }
  48. // find all the available items
  49. for (const buttonGroup of buttonGroups) {
  50. if (element.scrollWidth <= element.clientWidth) {
  51. break
  52. }
  53. // add this item to the overflow
  54. overflowedItemsRef.current.add(buttonGroup.dataset.overflow!)
  55. buttonGroup.classList.add('overflow-hidden')
  56. }
  57. setOverflowed(overflowedItemsRef.current.size > 0)
  58. }
  59. },
  60. [setOverflowOpen]
  61. )
  62. // build when the container resizes
  63. const resizeRef = useResizeObserver(buildOverflow)
  64. const toggleToolbar = useCallback(() => {
  65. setCollapsed(value => !value)
  66. }, [])
  67. if (collapsed) {
  68. return null
  69. }
  70. return (
  71. <div className="ol-cm-toolbar" ref={resizeRef}>
  72. <ToolbarItems state={state} />
  73. <div className="ol-cm-toolbar-button-group" ref={overflowBeforeRef}>
  74. <ToolbarOverflow
  75. overflowed={overflowed}
  76. target={overflowBeforeRef.current ?? undefined}
  77. overflowOpen={overflowOpen}
  78. setOverflowOpen={setOverflowOpen}
  79. overflowRef={overflowRef}
  80. >
  81. <ToolbarItems state={state} overflowed={overflowedItemsRef.current} />
  82. </ToolbarOverflow>
  83. </div>
  84. <div className="ol-cm-toolbar-button-group ol-cm-toolbar-end">
  85. <ToolbarButton
  86. id="toolbar-toggle-search"
  87. label="Toggle Search"
  88. command={commands.toggleSearch}
  89. active={searchPanelOpen(state)}
  90. icon="search"
  91. />
  92. </div>
  93. <div className="ol-cm-toolbar-button-group hidden">
  94. <ToolbarButton
  95. id="toolbar-expand-less"
  96. label="Hide Toolbar"
  97. command={toggleToolbar}
  98. icon="caret-up"
  99. hidden // enable this once there's a way to show the toolbar again
  100. />
  101. </div>
  102. </div>
  103. )
  104. })