section-heading-dropdown.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import classnames from 'classnames'
  2. import {
  3. useCodeMirrorStateContext,
  4. useCodeMirrorViewContext,
  5. } from '../codemirror-editor'
  6. import {
  7. findCurrentSectionHeadingLevel,
  8. setSectionHeadingLevel,
  9. } from '../../extensions/toolbar/sections'
  10. import { useCallback, useMemo, useRef } from 'react'
  11. import { Overlay, Popover } from 'react-bootstrap'
  12. import useEventListener from '../../../../shared/hooks/use-event-listener'
  13. import useDropdown from '../../../../shared/hooks/use-dropdown'
  14. import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
  15. import Icon from '../../../../shared/components/icon'
  16. import { useTranslation } from 'react-i18next'
  17. const levels = new Map([
  18. ['text', 'Normal text'],
  19. ['section', 'Section'],
  20. ['subsection', 'Subsection'],
  21. ['subsubsection', 'Subsubsection'],
  22. ['paragraph', 'Paragraph'],
  23. ['subparagraph', 'Subparagraph'],
  24. ])
  25. const levelsEntries = [...levels.entries()]
  26. export const SectionHeadingDropdown = () => {
  27. const state = useCodeMirrorStateContext()
  28. const view = useCodeMirrorViewContext()
  29. const { t } = useTranslation()
  30. const { open: overflowOpen, onToggle: setOverflowOpen } = useDropdown()
  31. useEventListener(
  32. 'resize',
  33. useCallback(() => {
  34. setOverflowOpen(false)
  35. }, [setOverflowOpen])
  36. )
  37. const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
  38. const currentLevel = useMemo(
  39. () => findCurrentSectionHeadingLevel(state),
  40. [state]
  41. )
  42. const currentLabel = currentLevel
  43. ? levels.get(currentLevel.level) ?? currentLevel.level
  44. : '---'
  45. return (
  46. <>
  47. <button
  48. ref={toggleButtonRef}
  49. type="button"
  50. id="section-heading-menu-button"
  51. aria-haspopup="true"
  52. aria-controls="section-heading-menu"
  53. aria-label={t('toolbar_choose_section_heading_level')}
  54. className="ol-cm-toolbar-menu-toggle"
  55. onMouseDown={event => event.preventDefault()}
  56. onClick={() => setOverflowOpen(!overflowOpen)}
  57. >
  58. <span>{currentLabel}</span>
  59. <Icon type="caret-down" fw />
  60. </button>
  61. {overflowOpen && (
  62. <Overlay
  63. show
  64. onHide={() => setOverflowOpen(false)}
  65. animation={false}
  66. container={view.dom}
  67. containerPadding={0}
  68. placement="bottom"
  69. rootClose
  70. target={toggleButtonRef.current ?? undefined}
  71. >
  72. <Popover
  73. id="popover-toolbar-section-heading"
  74. className="ol-cm-toolbar-menu-popover"
  75. >
  76. <div
  77. className="ol-cm-toolbar-menu"
  78. id="section-heading-menu"
  79. role="menu"
  80. aria-labelledby="section-heading-menu-button"
  81. >
  82. {levelsEntries.map(([level, label]) => (
  83. <button
  84. type="button"
  85. role="menuitem"
  86. key={level}
  87. onClick={() => {
  88. emitToolbarEvent(view, 'section-level-change')
  89. setSectionHeadingLevel(view, level)
  90. view.focus()
  91. setOverflowOpen(false)
  92. }}
  93. className={classnames(
  94. 'ol-cm-toolbar-menu-item',
  95. `section-level-${level}`,
  96. {
  97. 'ol-cm-toolbar-menu-item-active':
  98. level === currentLevel?.level,
  99. }
  100. )}
  101. >
  102. {label}
  103. </button>
  104. ))}
  105. </div>
  106. </Popover>
  107. </Overlay>
  108. )}
  109. </>
  110. )
  111. }