spelling-suggestions.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import {
  2. FC,
  3. MouseEventHandler,
  4. useCallback,
  5. useEffect,
  6. useMemo,
  7. useState,
  8. } from 'react'
  9. import { SpellChecker, Word } from './spellchecker'
  10. import { useTranslation } from 'react-i18next'
  11. import getMeta from '@/utils/meta'
  12. import classnames from 'classnames'
  13. import { sendMB } from '@/infrastructure/event-tracking'
  14. import { SpellingSuggestionsLanguage } from './spelling-suggestions-language'
  15. import { captureException } from '@/infrastructure/error-reporter'
  16. import { debugConsole } from '@/utils/debugging'
  17. import { SpellCheckLanguage } from '../../../../../../types/project-settings'
  18. import { Dropdown } from 'react-bootstrap'
  19. const ITEMS_TO_SHOW = 8
  20. type SpellingSuggestionsProps = {
  21. word: Word
  22. spellCheckLanguage?: string
  23. spellChecker?: SpellChecker | null
  24. handleClose: () => void
  25. handleLearnWord: () => void
  26. handleCorrectWord: (text: string) => void
  27. }
  28. export const SpellingSuggestions: FC<SpellingSuggestionsProps> = ({
  29. word,
  30. spellCheckLanguage,
  31. spellChecker,
  32. handleClose,
  33. handleLearnWord,
  34. handleCorrectWord,
  35. }) => {
  36. const [suggestions, setSuggestions] = useState<string[]>([])
  37. const [waiting, setWaiting] = useState(true)
  38. useEffect(() => {
  39. spellChecker
  40. ?.suggest(word.text)
  41. .then(result => {
  42. setSuggestions(result.suggestions.slice(0, ITEMS_TO_SHOW))
  43. setWaiting(false)
  44. sendMB('spelling-suggestion-shown', {
  45. language: spellCheckLanguage,
  46. count: result.suggestions.length,
  47. // word: transaction.state.sliceDoc(mark.from, mark.to),
  48. })
  49. })
  50. .catch(error => {
  51. captureException(error, {
  52. tags: { ol_spell_check_language: spellCheckLanguage },
  53. })
  54. debugConsole.error(error)
  55. })
  56. }, [word, spellChecker, spellCheckLanguage])
  57. const language = useMemo(() => {
  58. if (spellCheckLanguage) {
  59. return (getMeta('ol-languages') ?? []).find(
  60. item => item.code === spellCheckLanguage
  61. )
  62. }
  63. }, [spellCheckLanguage])
  64. if (!language) {
  65. return null
  66. }
  67. const innerProps = {
  68. suggestions,
  69. waiting,
  70. handleClose,
  71. handleCorrectWord,
  72. handleLearnWord,
  73. language,
  74. }
  75. return <B5SpellingSuggestions {...innerProps} />
  76. }
  77. type SpellingSuggestionsInnerProps = {
  78. suggestions: string[]
  79. waiting: boolean
  80. handleClose: () => void
  81. handleCorrectWord: (text: string) => void
  82. handleLearnWord: () => void
  83. language: SpellCheckLanguage
  84. }
  85. const B5SpellingSuggestions: FC<SpellingSuggestionsInnerProps> = ({
  86. suggestions,
  87. waiting,
  88. language,
  89. handleClose,
  90. handleCorrectWord,
  91. handleLearnWord,
  92. }) => {
  93. const { t } = useTranslation()
  94. return (
  95. <Dropdown>
  96. <Dropdown.Menu
  97. className={classnames('dropdown-menu', 'dropdown-menu-unpositioned', {
  98. hidden: waiting,
  99. })}
  100. show={!waiting}
  101. tabIndex={0}
  102. role="menu"
  103. onKeyDown={event => {
  104. switch (event.code) {
  105. case 'Escape':
  106. case 'Tab':
  107. event.preventDefault()
  108. handleClose()
  109. break
  110. }
  111. }}
  112. >
  113. {Array.isArray(suggestions) &&
  114. suggestions.map((suggestion, index) => (
  115. <BS5ListItem
  116. key={suggestion}
  117. content={suggestion}
  118. handleClick={event => {
  119. event.preventDefault()
  120. handleCorrectWord(suggestion)
  121. }}
  122. // eslint-disable-next-line jsx-a11y/no-autofocus
  123. autoFocus={index === 0}
  124. />
  125. ))}
  126. {suggestions?.length > 0 && <Dropdown.Divider />}
  127. <BS5ListItem
  128. content={t('add_to_dictionary')}
  129. handleClick={event => {
  130. event.preventDefault()
  131. handleLearnWord()
  132. }}
  133. // eslint-disable-next-line jsx-a11y/no-autofocus
  134. autoFocus={suggestions?.length === 0}
  135. />
  136. <Dropdown.Divider />
  137. <SpellingSuggestionsLanguage
  138. language={language}
  139. handleClose={handleClose}
  140. />
  141. </Dropdown.Menu>
  142. </Dropdown>
  143. )
  144. }
  145. const BS5ListItem: FC<{
  146. content: string
  147. handleClick: MouseEventHandler<HTMLButtonElement>
  148. autoFocus?: boolean
  149. }> = ({ content, handleClick, autoFocus }) => {
  150. const handleListItem = useCallback(
  151. (node: HTMLElement | null) => {
  152. if (node && autoFocus) node.focus()
  153. },
  154. [autoFocus]
  155. )
  156. return (
  157. <Dropdown.Item
  158. role="menuitem"
  159. className="btn-link text-left dropdown-menu-button"
  160. onClick={handleClick}
  161. ref={handleListItem}
  162. >
  163. {content}
  164. </Dropdown.Item>
  165. )
  166. }