section-heading-dropdown.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  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, 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 { emitCommandEvent } 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 = findCurrentSectionHeadingLevel(state)
  39. const currentLabel = currentLevel
  40. ? levels.get(currentLevel.level) ?? currentLevel.level
  41. : '---'
  42. return (
  43. <>
  44. <button
  45. ref={toggleButtonRef}
  46. type="button"
  47. id="section-heading-menu-button"
  48. aria-haspopup="true"
  49. aria-controls="section-heading-menu"
  50. aria-label={t('toolbar_choose_section_heading_level')}
  51. className="ol-cm-toolbar-menu-toggle"
  52. onMouseDown={event => event.preventDefault()}
  53. onClick={() => setOverflowOpen(!overflowOpen)}
  54. >
  55. <span>{currentLabel}</span>
  56. <Icon type="caret-down" fw />
  57. </button>
  58. <Overlay
  59. show={overflowOpen}
  60. onHide={() => setOverflowOpen(false)}
  61. animation={false}
  62. container={document.querySelector('.cm-editor')}
  63. containerPadding={0}
  64. placement="bottom"
  65. rootClose
  66. target={toggleButtonRef.current ?? undefined}
  67. >
  68. <Popover
  69. id="popover-toolbar-section-heading"
  70. className="ol-cm-toolbar-menu-popover"
  71. >
  72. <div
  73. className="ol-cm-toolbar-menu"
  74. id="section-heading-menu"
  75. role="menu"
  76. aria-labelledby="section-heading-menu-button"
  77. >
  78. {levelsEntries.map(([level, label]) => (
  79. <button
  80. type="button"
  81. role="menuitem"
  82. key={level}
  83. onClick={() => {
  84. emitCommandEvent(view, 'section-level-change')
  85. setSectionHeadingLevel(view, level)
  86. view.focus()
  87. setOverflowOpen(false)
  88. }}
  89. className={classnames(
  90. 'ol-cm-toolbar-menu-item',
  91. `section-level-${level}`,
  92. {
  93. 'ol-cm-toolbar-menu-item-active':
  94. level === currentLevel?.level,
  95. }
  96. )}
  97. >
  98. {label}
  99. </button>
  100. ))}
  101. </div>
  102. </Popover>
  103. </Overlay>
  104. </>
  105. )
  106. }