react-native-enriched-html 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt +39 -0
  2. package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java +43 -20
  3. package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt +1 -0
  4. package/android/src/main/java/com/swmansion/enriched/text/EnrichedTextSpanFactory.kt +1 -0
  5. package/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt +2 -1
  6. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt +2 -1
  7. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +8 -5
  8. package/android/src/main/java/com/swmansion/enriched/textinput/events/MentionHandler.kt +12 -2
  9. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionEvent.kt +6 -0
  10. package/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt +6 -7
  11. package/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +13 -1
  12. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt +4 -3
  13. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt +4 -3
  14. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +38 -11
  15. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt +2 -0
  16. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt +2 -1
  17. package/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt +2 -1
  18. package/cpp/parser/GumboNormalizer.c +61 -3
  19. package/cpp/tests/GumboParserTest.cpp +54 -0
  20. package/ios/EnrichedTextInputView.mm +37 -25
  21. package/ios/extensions/LayoutManagerExtension.mm +73 -62
  22. package/ios/extensions/StringExtension.mm +75 -0
  23. package/ios/htmlParser/HtmlParser.h +4 -1
  24. package/ios/htmlParser/HtmlParser.mm +23 -5
  25. package/ios/inputHtmlParser/InputHtmlParser.mm +6 -3
  26. package/ios/interfaces/StyleHeaders.h +4 -0
  27. package/ios/styles/LinkStyle.mm +20 -34
  28. package/ios/styles/MentionStyle.mm +55 -4
  29. package/ios/textHtmlParser/TextHtmlParser.mm +2 -1
  30. package/ios/utils/ParagraphAttributesUtils.mm +7 -0
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/utils/expandMentionStylesForIndicators.js +2 -3
  34. package/lib/module/utils/expandMentionStylesForIndicators.js.map +1 -1
  35. package/lib/module/utils/normalizeHtmlStyle.js +1 -1
  36. package/lib/module/utils/normalizeHtmlStyle.js.map +1 -1
  37. package/lib/module/web/EnrichedText.css +343 -0
  38. package/lib/module/web/EnrichedText.js +55 -0
  39. package/lib/module/web/EnrichedText.js.map +1 -0
  40. package/lib/module/web/EnrichedTextInput.js +54 -52
  41. package/lib/module/web/EnrichedTextInput.js.map +1 -1
  42. package/lib/module/web/constants/brokenImageGlyph.js +9 -0
  43. package/lib/module/web/constants/brokenImageGlyph.js.map +1 -0
  44. package/lib/module/web/constants/classNames.js +5 -0
  45. package/lib/module/web/constants/classNames.js.map +1 -0
  46. package/lib/module/web/emitLinkDetected.js +12 -0
  47. package/lib/module/web/emitLinkDetected.js.map +1 -0
  48. package/lib/module/web/formats/EnrichedBlockquote.js +1 -1
  49. package/lib/module/web/formats/EnrichedBlockquote.js.map +1 -1
  50. package/lib/module/web/formats/EnrichedCheckboxList.js +2 -2
  51. package/lib/module/web/formats/EnrichedCheckboxList.js.map +1 -1
  52. package/lib/module/web/formats/EnrichedCodeBlock.js +1 -1
  53. package/lib/module/web/formats/EnrichedCodeBlock.js.map +1 -1
  54. package/lib/module/web/formats/EnrichedHeading.js +1 -1
  55. package/lib/module/web/formats/EnrichedHeading.js.map +1 -1
  56. package/lib/module/web/formats/EnrichedImageNodeView.js +1 -1
  57. package/lib/module/web/formats/EnrichedImageNodeView.js.map +1 -1
  58. package/lib/module/web/formats/EnrichedLink.js +43 -3
  59. package/lib/module/web/formats/EnrichedLink.js.map +1 -1
  60. package/lib/module/web/formats/EnrichedOrderedList.js +2 -2
  61. package/lib/module/web/formats/EnrichedOrderedList.js.map +1 -1
  62. package/lib/module/web/formats/EnrichedTextAlign.js +82 -0
  63. package/lib/module/web/formats/EnrichedTextAlign.js.map +1 -0
  64. package/lib/module/web/formats/EnrichedUnorderedList.js +2 -2
  65. package/lib/module/web/formats/EnrichedUnorderedList.js.map +1 -1
  66. package/lib/module/web/formats/applyWrappingListToSelection.js +3 -2
  67. package/lib/module/web/formats/applyWrappingListToSelection.js.map +1 -1
  68. package/lib/module/web/formats/formatRules.js +39 -3
  69. package/lib/module/web/formats/formatRules.js.map +1 -1
  70. package/lib/module/web/formats/listKeyboard.js +4 -1
  71. package/lib/module/web/formats/listKeyboard.js.map +1 -1
  72. package/lib/module/web/normalization/checkboxHtmlNormalizer.js.map +1 -0
  73. package/lib/module/web/normalization/htmlNormalizer.js +514 -0
  74. package/lib/module/web/normalization/htmlNormalizer.js.map +1 -0
  75. package/lib/module/web/normalization/prepareHtmlForWeb.js +78 -0
  76. package/lib/module/web/normalization/prepareHtmlForWeb.js.map +1 -0
  77. package/lib/module/web/{tiptapHtmlNormalizer.js → normalization/tiptapHtmlNormalizer.js} +6 -2
  78. package/lib/module/web/normalization/tiptapHtmlNormalizer.js.map +1 -0
  79. package/lib/module/web/pmPlugins/AutolinkPlugin/autolinkRegex.js +27 -0
  80. package/lib/module/web/pmPlugins/AutolinkPlugin/autolinkRegex.js.map +1 -0
  81. package/lib/module/web/pmPlugins/AutolinkPlugin/index.js +178 -0
  82. package/lib/module/web/pmPlugins/AutolinkPlugin/index.js.map +1 -0
  83. package/lib/module/web/pmPlugins/MentionPlugin/index.js +1 -1
  84. package/lib/module/web/pmPlugins/MentionPlugin/index.js.map +1 -1
  85. package/lib/module/web/pmPlugins/MentionPlugin/makeMentionPluginState.js +23 -4
  86. package/lib/module/web/pmPlugins/MentionPlugin/makeMentionPluginState.js.map +1 -1
  87. package/lib/module/web/pmPlugins/MentionPlugin/setMention.js +17 -18
  88. package/lib/module/web/pmPlugins/MentionPlugin/setMention.js.map +1 -1
  89. package/lib/module/web/pmPlugins/MentionPlugin/useMentionEvents.js +105 -0
  90. package/lib/module/web/pmPlugins/MentionPlugin/useMentionEvents.js.map +1 -0
  91. package/lib/module/web/pmPlugins/ShortcutPlugin.js +11 -0
  92. package/lib/module/web/pmPlugins/ShortcutPlugin.js.map +1 -1
  93. package/lib/module/web/pmPlugins/StrictMarksPlugin.js +2 -0
  94. package/lib/module/web/pmPlugins/StrictMarksPlugin.js.map +1 -1
  95. package/lib/module/web/pmPlugins/StripBoldInStyledHeadingsPlugin.js.map +1 -1
  96. package/lib/module/web/sanitization/htmlSanitizer.js +10 -0
  97. package/lib/module/web/sanitization/htmlSanitizer.js.map +1 -0
  98. package/lib/module/web/styleConversion/buildMentionRulesCSS.js +12 -6
  99. package/lib/module/web/styleConversion/buildMentionRulesCSS.js.map +1 -1
  100. package/lib/module/web/styleConversion/enrichedBaseStyleToCSSProperties.js +159 -0
  101. package/lib/module/web/styleConversion/enrichedBaseStyleToCSSProperties.js.map +1 -0
  102. package/lib/module/web/styleConversion/enrichedInputStyleToCSSProperties.js +3 -155
  103. package/lib/module/web/styleConversion/enrichedInputStyleToCSSProperties.js.map +1 -1
  104. package/lib/module/web/styleConversion/enrichedTextStyleToCSSProperties.js +44 -0
  105. package/lib/module/web/styleConversion/enrichedTextStyleToCSSProperties.js.map +1 -0
  106. package/lib/module/web/styleConversion/{enrichedInputThemingToCSSProperties.js → enrichedThemingToCSSProperties.js} +3 -3
  107. package/lib/module/web/styleConversion/enrichedThemingToCSSProperties.js.map +1 -0
  108. package/lib/module/web/styleConversion/htmlStyleToCSSVariables.js +87 -64
  109. package/lib/module/web/styleConversion/htmlStyleToCSSVariables.js.map +1 -1
  110. package/lib/module/web/styleConversion/inlineImageCSSVariables.js +7 -0
  111. package/lib/module/web/styleConversion/inlineImageCSSVariables.js.map +1 -0
  112. package/lib/module/web/useImageErrorFallback.js +33 -0
  113. package/lib/module/web/useImageErrorFallback.js.map +1 -0
  114. package/lib/module/web/useOnChangeHtml.js +1 -1
  115. package/lib/module/web/useOnChangeHtml.js.map +1 -1
  116. package/lib/module/web/useOnChangeState.js +6 -3
  117. package/lib/module/web/useOnChangeState.js.map +1 -1
  118. package/lib/module/web/useOnLinkDetected.js +32 -45
  119. package/lib/module/web/useOnLinkDetected.js.map +1 -1
  120. package/lib/module/web/usePressInteractions.js +19 -0
  121. package/lib/module/web/usePressInteractions.js.map +1 -0
  122. package/lib/module/web/useStableRef.js +13 -0
  123. package/lib/module/web/useStableRef.js.map +1 -0
  124. package/lib/typescript/src/index.d.ts +1 -0
  125. package/lib/typescript/src/index.d.ts.map +1 -1
  126. package/lib/typescript/src/native/EnrichedText.d.ts +1 -1
  127. package/lib/typescript/src/native/EnrichedText.d.ts.map +1 -1
  128. package/lib/typescript/src/native/EnrichedTextInput.d.ts +1 -1
  129. package/lib/typescript/src/native/EnrichedTextInput.d.ts.map +1 -1
  130. package/lib/typescript/src/types.d.ts +1 -1
  131. package/lib/typescript/src/types.d.ts.map +1 -1
  132. package/lib/typescript/src/utils/expandMentionStylesForIndicators.d.ts +1 -1
  133. package/lib/typescript/src/utils/expandMentionStylesForIndicators.d.ts.map +1 -1
  134. package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts.map +1 -1
  135. package/lib/typescript/src/web/EnrichedText.d.ts +4 -0
  136. package/lib/typescript/src/web/EnrichedText.d.ts.map +1 -0
  137. package/lib/typescript/src/web/EnrichedTextInput.d.ts +2 -2
  138. package/lib/typescript/src/web/EnrichedTextInput.d.ts.map +1 -1
  139. package/lib/typescript/src/web/constants/brokenImageGlyph.d.ts +3 -0
  140. package/lib/typescript/src/web/constants/brokenImageGlyph.d.ts.map +1 -0
  141. package/lib/typescript/src/web/constants/classNames.d.ts +3 -0
  142. package/lib/typescript/src/web/constants/classNames.d.ts.map +1 -0
  143. package/lib/typescript/src/web/emitLinkDetected.d.ts +10 -0
  144. package/lib/typescript/src/web/emitLinkDetected.d.ts.map +1 -0
  145. package/lib/typescript/src/web/formats/EnrichedBlockquote.d.ts.map +1 -1
  146. package/lib/typescript/src/web/formats/EnrichedCheckboxList.d.ts.map +1 -1
  147. package/lib/typescript/src/web/formats/EnrichedCodeBlock.d.ts.map +1 -1
  148. package/lib/typescript/src/web/formats/EnrichedHeading.d.ts.map +1 -1
  149. package/lib/typescript/src/web/formats/EnrichedImageNodeView.d.ts +1 -1
  150. package/lib/typescript/src/web/formats/EnrichedImageNodeView.d.ts.map +1 -1
  151. package/lib/typescript/src/web/formats/EnrichedLink.d.ts +4 -1
  152. package/lib/typescript/src/web/formats/EnrichedLink.d.ts.map +1 -1
  153. package/lib/typescript/src/web/formats/EnrichedOrderedList.d.ts.map +1 -1
  154. package/lib/typescript/src/web/formats/EnrichedTextAlign.d.ts +2 -0
  155. package/lib/typescript/src/web/formats/EnrichedTextAlign.d.ts.map +1 -0
  156. package/lib/typescript/src/web/formats/EnrichedUnorderedList.d.ts.map +1 -1
  157. package/lib/typescript/src/web/formats/applyWrappingListToSelection.d.ts.map +1 -1
  158. package/lib/typescript/src/web/formats/formatRules.d.ts +3 -1
  159. package/lib/typescript/src/web/formats/formatRules.d.ts.map +1 -1
  160. package/lib/typescript/src/web/formats/listKeyboard.d.ts.map +1 -1
  161. package/lib/typescript/src/web/normalization/checkboxHtmlNormalizer.d.ts.map +1 -0
  162. package/lib/typescript/src/web/normalization/htmlNormalizer.d.ts +2 -0
  163. package/lib/typescript/src/web/normalization/htmlNormalizer.d.ts.map +1 -0
  164. package/lib/typescript/src/web/normalization/prepareHtmlForWeb.d.ts +2 -0
  165. package/lib/typescript/src/web/normalization/prepareHtmlForWeb.d.ts.map +1 -0
  166. package/lib/typescript/src/web/normalization/tiptapHtmlNormalizer.d.ts +3 -0
  167. package/lib/typescript/src/web/normalization/tiptapHtmlNormalizer.d.ts.map +1 -0
  168. package/lib/typescript/src/web/pmPlugins/AutolinkPlugin/autolinkRegex.d.ts +7 -0
  169. package/lib/typescript/src/web/pmPlugins/AutolinkPlugin/autolinkRegex.d.ts.map +1 -0
  170. package/lib/typescript/src/web/pmPlugins/AutolinkPlugin/index.d.ts +8 -0
  171. package/lib/typescript/src/web/pmPlugins/AutolinkPlugin/index.d.ts.map +1 -0
  172. package/lib/typescript/src/web/pmPlugins/MentionPlugin/index.d.ts +1 -1
  173. package/lib/typescript/src/web/pmPlugins/MentionPlugin/index.d.ts.map +1 -1
  174. package/lib/typescript/src/web/pmPlugins/MentionPlugin/makeMentionPluginState.d.ts.map +1 -1
  175. package/lib/typescript/src/web/pmPlugins/MentionPlugin/setMention.d.ts.map +1 -1
  176. package/lib/typescript/src/web/pmPlugins/MentionPlugin/useMentionEvents.d.ts +4 -0
  177. package/lib/typescript/src/web/pmPlugins/MentionPlugin/useMentionEvents.d.ts.map +1 -0
  178. package/lib/typescript/src/web/pmPlugins/ShortcutPlugin.d.ts.map +1 -1
  179. package/lib/typescript/src/web/pmPlugins/StrictMarksPlugin.d.ts.map +1 -1
  180. package/lib/typescript/src/web/pmPlugins/StripBoldInStyledHeadingsPlugin.d.ts +6 -1
  181. package/lib/typescript/src/web/pmPlugins/StripBoldInStyledHeadingsPlugin.d.ts.map +1 -1
  182. package/lib/typescript/src/web/sanitization/htmlSanitizer.d.ts +2 -0
  183. package/lib/typescript/src/web/sanitization/htmlSanitizer.d.ts.map +1 -0
  184. package/lib/typescript/src/web/styleConversion/buildMentionRulesCSS.d.ts +2 -2
  185. package/lib/typescript/src/web/styleConversion/buildMentionRulesCSS.d.ts.map +1 -1
  186. package/lib/typescript/src/web/styleConversion/enrichedBaseStyleToCSSProperties.d.ts +8 -0
  187. package/lib/typescript/src/web/styleConversion/enrichedBaseStyleToCSSProperties.d.ts.map +1 -0
  188. package/lib/typescript/src/web/styleConversion/enrichedInputStyleToCSSProperties.d.ts +2 -5
  189. package/lib/typescript/src/web/styleConversion/enrichedInputStyleToCSSProperties.d.ts.map +1 -1
  190. package/lib/typescript/src/web/styleConversion/enrichedTextStyleToCSSProperties.d.ts +4 -0
  191. package/lib/typescript/src/web/styleConversion/enrichedTextStyleToCSSProperties.d.ts.map +1 -0
  192. package/lib/typescript/src/web/styleConversion/{enrichedInputThemingToCSSProperties.d.ts → enrichedThemingToCSSProperties.d.ts} +1 -1
  193. package/lib/typescript/src/web/styleConversion/enrichedThemingToCSSProperties.d.ts.map +1 -0
  194. package/lib/typescript/src/web/styleConversion/htmlStyleToCSSVariables.d.ts +5 -4
  195. package/lib/typescript/src/web/styleConversion/htmlStyleToCSSVariables.d.ts.map +1 -1
  196. package/lib/typescript/src/web/styleConversion/inlineImageCSSVariables.d.ts +3 -0
  197. package/lib/typescript/src/web/styleConversion/inlineImageCSSVariables.d.ts.map +1 -0
  198. package/lib/typescript/src/web/useImageErrorFallback.d.ts +3 -0
  199. package/lib/typescript/src/web/useImageErrorFallback.d.ts.map +1 -0
  200. package/lib/typescript/src/web/useOnChangeState.d.ts.map +1 -1
  201. package/lib/typescript/src/web/useOnLinkDetected.d.ts +2 -2
  202. package/lib/typescript/src/web/useOnLinkDetected.d.ts.map +1 -1
  203. package/lib/typescript/src/web/usePressInteractions.d.ts +2 -0
  204. package/lib/typescript/src/web/usePressInteractions.d.ts.map +1 -0
  205. package/lib/typescript/src/web/useStableRef.d.ts +3 -0
  206. package/lib/typescript/src/web/useStableRef.d.ts.map +1 -0
  207. package/package.json +54 -28
  208. package/src/index.tsx +1 -0
  209. package/src/types.ts +5 -3
  210. package/src/utils/expandMentionStylesForIndicators.ts +3 -3
  211. package/src/utils/normalizeHtmlStyle.ts +2 -1
  212. package/src/web/EnrichedText.css +343 -0
  213. package/src/web/EnrichedText.tsx +82 -0
  214. package/src/web/EnrichedTextInput.tsx +69 -55
  215. package/src/web/constants/brokenImageGlyph.ts +7 -0
  216. package/src/web/constants/classNames.ts +2 -0
  217. package/src/web/emitLinkDetected.ts +29 -0
  218. package/src/web/formats/EnrichedBlockquote.ts +2 -1
  219. package/src/web/formats/EnrichedCheckboxList.ts +5 -2
  220. package/src/web/formats/EnrichedCodeBlock.ts +2 -1
  221. package/src/web/formats/EnrichedHeading.ts +2 -1
  222. package/src/web/formats/EnrichedImageNodeView.tsx +1 -3
  223. package/src/web/formats/EnrichedLink.ts +59 -4
  224. package/src/web/formats/EnrichedOrderedList.ts +5 -2
  225. package/src/web/formats/EnrichedTextAlign.ts +88 -0
  226. package/src/web/formats/EnrichedUnorderedList.ts +5 -2
  227. package/src/web/formats/applyWrappingListToSelection.ts +4 -4
  228. package/src/web/formats/formatRules.ts +48 -4
  229. package/src/web/formats/listKeyboard.ts +6 -1
  230. package/src/web/normalization/htmlNormalizer.ts +656 -0
  231. package/src/web/normalization/prepareHtmlForWeb.ts +93 -0
  232. package/src/web/{tiptapHtmlNormalizer.ts → normalization/tiptapHtmlNormalizer.ts} +12 -2
  233. package/src/web/pmPlugins/AutolinkPlugin/autolinkRegex.ts +43 -0
  234. package/src/web/pmPlugins/AutolinkPlugin/index.ts +274 -0
  235. package/src/web/pmPlugins/MentionPlugin/index.ts +1 -1
  236. package/src/web/pmPlugins/MentionPlugin/makeMentionPluginState.ts +32 -5
  237. package/src/web/pmPlugins/MentionPlugin/setMention.ts +19 -29
  238. package/src/web/pmPlugins/MentionPlugin/useMentionEvents.ts +114 -0
  239. package/src/web/pmPlugins/ShortcutPlugin.ts +7 -0
  240. package/src/web/pmPlugins/StrictMarksPlugin.ts +3 -0
  241. package/src/web/pmPlugins/StripBoldInStyledHeadingsPlugin.ts +41 -36
  242. package/src/web/sanitization/htmlSanitizer.ts +8 -0
  243. package/src/web/styleConversion/buildMentionRulesCSS.ts +30 -11
  244. package/src/web/styleConversion/enrichedBaseStyleToCSSProperties.ts +228 -0
  245. package/src/web/styleConversion/enrichedInputStyleToCSSProperties.ts +6 -217
  246. package/src/web/styleConversion/enrichedTextStyleToCSSProperties.ts +53 -0
  247. package/src/web/styleConversion/{enrichedInputThemingToCSSProperties.ts → enrichedThemingToCSSProperties.ts} +4 -4
  248. package/src/web/styleConversion/htmlStyleToCSSVariables.ts +121 -70
  249. package/src/web/styleConversion/inlineImageCSSVariables.ts +6 -0
  250. package/src/web/useImageErrorFallback.ts +37 -0
  251. package/src/web/useOnChangeHtml.ts +1 -1
  252. package/src/web/useOnChangeState.ts +7 -5
  253. package/src/web/useOnLinkDetected.ts +38 -50
  254. package/src/web/usePressInteractions.tsx +22 -0
  255. package/src/web/useStableRef.ts +12 -0
  256. package/lib/module/web/EnrichedTextInput.css +0 -201
  257. package/lib/module/web/checkboxHtmlNormalizer.js.map +0 -1
  258. package/lib/module/web/pmPlugins/MentionPlugin/subscribeMentionEvents.js +0 -100
  259. package/lib/module/web/pmPlugins/MentionPlugin/subscribeMentionEvents.js.map +0 -1
  260. package/lib/module/web/styleConversion/enrichedInputThemingToCSSProperties.js.map +0 -1
  261. package/lib/module/web/tiptapHtmlNormalizer.js.map +0 -1
  262. package/lib/typescript/src/web/checkboxHtmlNormalizer.d.ts.map +0 -1
  263. package/lib/typescript/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.d.ts +0 -4
  264. package/lib/typescript/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.d.ts.map +0 -1
  265. package/lib/typescript/src/web/styleConversion/enrichedInputThemingToCSSProperties.d.ts.map +0 -1
  266. package/lib/typescript/src/web/tiptapHtmlNormalizer.d.ts +0 -3
  267. package/lib/typescript/src/web/tiptapHtmlNormalizer.d.ts.map +0 -1
  268. package/src/web/EnrichedTextInput.css +0 -201
  269. package/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.ts +0 -105
  270. /package/lib/module/web/{checkboxHtmlNormalizer.js → normalization/checkboxHtmlNormalizer.js} +0 -0
  271. /package/lib/typescript/src/web/{checkboxHtmlNormalizer.d.ts → normalization/checkboxHtmlNormalizer.d.ts} +0 -0
  272. /package/src/web/{checkboxHtmlNormalizer.ts → normalization/checkboxHtmlNormalizer.ts} +0 -0
@@ -0,0 +1,93 @@
1
+ export function prepareHtmlForWeb(html: string): string {
2
+ if (typeof DOMParser === 'undefined') return html;
3
+
4
+ const parser = new DOMParser();
5
+ const doc = parser.parseFromString(html, 'text/html');
6
+
7
+ wrapBareLiContentInParagraph(doc);
8
+ checkboxHtmlToWeb(doc);
9
+
10
+ return doc.body.innerHTML;
11
+ }
12
+
13
+ /*
14
+ * Native list format:
15
+ * <ul>
16
+ * <li>foo</li>
17
+ * <li></li>
18
+ * </ul>
19
+ *
20
+ * Web-native, with <p> wrappers to display the content correctly:
21
+ * <ul>
22
+ * <li>
23
+ * <p>foo</p>
24
+ * </li>
25
+ * <li>
26
+ * <p></p>
27
+ * </li>
28
+ * </ul>
29
+ */
30
+ function wrapBareLiContentInParagraph(doc: Document) {
31
+ // Target only standard lists (ignore checkbox lists, as they get wrapped in <label> later)
32
+ const listItems = doc.querySelectorAll(
33
+ 'ul:not([data-type="checkbox"]) > li, ol > li'
34
+ );
35
+
36
+ listItems.forEach((li) => {
37
+ if (li.firstElementChild?.tagName.toUpperCase() === 'P') return;
38
+
39
+ const nodesToWrap: Node[] = [];
40
+ const childNodes = Array.from(li.childNodes);
41
+
42
+ for (const node of childNodes) {
43
+ if (
44
+ node.nodeType === Node.ELEMENT_NODE &&
45
+ ['UL', 'OL'].includes((node as Element).tagName.toUpperCase())
46
+ ) {
47
+ break;
48
+ }
49
+ nodesToWrap.push(node);
50
+ }
51
+
52
+ if (nodesToWrap.length === 0 && childNodes.length > 0) return;
53
+
54
+ const p = doc.createElement('p');
55
+
56
+ li.insertBefore(p, childNodes[0] || null);
57
+
58
+ nodesToWrap.forEach((node) => p.appendChild(node));
59
+ });
60
+ }
61
+
62
+ /*
63
+ * Native checkbox format (as produced by the editor):
64
+ * <ul data-type="checkbox">
65
+ * <li checked>foo</li>
66
+ * <li>bar</li>
67
+ * </ul>
68
+ *
69
+ * Web-native, display-only format:
70
+ * <ul data-type="checkbox">
71
+ * <li>
72
+ * <input type="checkbox" checked>
73
+ * <label>foo</label>
74
+ * </li>
75
+ * <li>
76
+ * <input type="checkbox">
77
+ * <label>bar</label>
78
+ * </li>
79
+ * </ul>
80
+ */
81
+ function checkboxHtmlToWeb(doc: Document) {
82
+ doc.querySelectorAll('ul[data-type="checkbox"]').forEach((ul) => {
83
+ ul.querySelectorAll('li').forEach((li) => {
84
+ const checked = li.hasAttribute('checked');
85
+ const labelContent = li.innerHTML;
86
+
87
+ li.removeAttribute('checked');
88
+ li.innerHTML =
89
+ `<input type="checkbox"${checked ? ' checked' : ''}>` +
90
+ `<label>${labelContent}</label>`;
91
+ });
92
+ });
93
+ }
@@ -2,8 +2,15 @@ import {
2
2
  checkboxHtmlForTiptap,
3
3
  checkboxHtmlFromTiptap,
4
4
  } from './checkboxHtmlNormalizer';
5
+ import { normalizeHtml } from './htmlNormalizer';
5
6
 
6
- export function prepareHtmlForTiptap(html: string): string {
7
+ export function prepareHtmlForTiptap(
8
+ html: string,
9
+ useHtmlNormalizer: boolean | undefined
10
+ ): string {
11
+ if (useHtmlNormalizer) {
12
+ html = normalizeHtml(html);
13
+ }
7
14
  html = checkboxHtmlForTiptap(html);
8
15
  html = html.replace(/<br\s*\/?>/gi, '<p></p>');
9
16
  return html;
@@ -16,7 +23,10 @@ export function normalizeHtmlFromTiptap(html: string): string {
16
23
  // TipTap renders <li><p>text</p></li> but native expects <li>text</li>.
17
24
  // This regex is safe because EnrichedListItem.content is 'paragraph', which
18
25
  // prevents TipTap from ever emitting nested lists
19
- html = html.replace(/<li([^>]*)><p>(.*?)<\/p><\/li>/gs, '<li$1>$2</li>');
26
+ html = html.replace(
27
+ /<li([^>]*)>\s*<p[^>]*>(.*?)<\/p>\s*<\/li>/gs,
28
+ '<li$1>$2</li>'
29
+ );
20
30
 
21
31
  // Convert remaining empty <p></p> to <br> (outside of lists)
22
32
  html = html.replace(/<p><\/p>/g, '<br>');
@@ -0,0 +1,43 @@
1
+ const FULL_URL_REGEX =
2
+ /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/;
3
+ const WWW_REGEX =
4
+ /www\.[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/;
5
+ const BARE_REGEX =
6
+ /[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/;
7
+
8
+ const DEFAULT_AUTOLINK_SUBSTRING_PATTERN = `(?:${FULL_URL_REGEX.source})|(?:${WWW_REGEX.source})|(?:${BARE_REGEX.source})`;
9
+
10
+ export type AutolinkRangeInWord = {
11
+ start: number;
12
+ endExclusive: number;
13
+ text: string;
14
+ };
15
+
16
+ function asGlobalRegex(re: RegExp): RegExp {
17
+ return re.global
18
+ ? re
19
+ : new RegExp(re.source, `${re.flags.replace(/g/g, '')}g`);
20
+ }
21
+
22
+ // URL-like substrings inside a single whitespace-delimited token (`\S+`),
23
+ export function findAutolinkRangesInWord(
24
+ word: string,
25
+ linkRegex: RegExp | undefined
26
+ ): readonly AutolinkRangeInWord[] {
27
+ const re = linkRegex
28
+ ? asGlobalRegex(linkRegex)
29
+ : new RegExp(DEFAULT_AUTOLINK_SUBSTRING_PATTERN, 'gi');
30
+
31
+ const out: AutolinkRangeInWord[] = [];
32
+ for (const m of word.matchAll(re)) {
33
+ const text = m[0] ?? '';
34
+ if (text.length === 0 || m.index === undefined) continue;
35
+ const start = m.index;
36
+ out.push({
37
+ start,
38
+ endExclusive: start + text.length,
39
+ text,
40
+ });
41
+ }
42
+ return out;
43
+ }
@@ -0,0 +1,274 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import type { MarkType, Node, Schema } from '@tiptap/pm/model';
3
+ import { Plugin, PluginKey, type Transaction } from '@tiptap/pm/state';
4
+ import { Mapping } from '@tiptap/pm/transform';
5
+ import type { OnLinkDetected } from '../../../types';
6
+ import {
7
+ emitLinkDetected,
8
+ type LinkEmitterState,
9
+ } from '../../emitLinkDetected';
10
+ import { tiptapPosToNativePos } from '../../positionMapping';
11
+ import { findAutolinkRangesInWord } from './autolinkRegex';
12
+
13
+ interface Run {
14
+ text: string;
15
+ startPos: number;
16
+ }
17
+
18
+ interface DirtyBlock {
19
+ node: Node;
20
+ pos: number;
21
+ }
22
+
23
+ interface AutolinkPluginOptions {
24
+ getLinkEmitter: () => LinkEmitterState;
25
+ }
26
+
27
+ const WHITESPACE_RE = /\S+/g;
28
+
29
+ function removeAutoLinksInRange(
30
+ doc: Node,
31
+ tr: Transaction,
32
+ linkType: MarkType,
33
+ from: number,
34
+ to: number
35
+ ): void {
36
+ if (from >= to) return;
37
+
38
+ doc.nodesBetween(from, to, (node, pos) => {
39
+ if (!node.isText) return true;
40
+
41
+ const link = linkType.isInSet(node.marks);
42
+ if (link?.attrs.auto === true) {
43
+ tr.removeMark(
44
+ Math.max(from, pos),
45
+ Math.min(to, pos + node.nodeSize),
46
+ linkType
47
+ );
48
+ }
49
+
50
+ return false;
51
+ });
52
+ }
53
+
54
+ function rangeHasManualLink(
55
+ doc: Node,
56
+ linkType: MarkType,
57
+ from: number,
58
+ to: number
59
+ ): boolean {
60
+ let found = false;
61
+
62
+ doc.nodesBetween(from, to, (node) => {
63
+ if (found) return false;
64
+ if (!node.isText) return true;
65
+
66
+ const link = linkType.isInSet(node.marks);
67
+ if (link && link.attrs.auto !== true) found = true;
68
+
69
+ return false;
70
+ });
71
+
72
+ return found;
73
+ }
74
+
75
+ function rangeHasExactAutoLink(
76
+ doc: Node,
77
+ linkType: MarkType,
78
+ from: number,
79
+ to: number,
80
+ href: string
81
+ ): boolean {
82
+ let hasExact = true;
83
+ let hasText = false;
84
+
85
+ doc.nodesBetween(from, to, (node) => {
86
+ if (!hasExact) return false;
87
+ if (!node.isText) return true;
88
+
89
+ hasText = true;
90
+ const link = linkType.isInSet(node.marks);
91
+ if (!link || link.attrs.auto !== true || link.attrs.href !== href) {
92
+ hasExact = false;
93
+ }
94
+ return false;
95
+ });
96
+
97
+ return hasText && hasExact;
98
+ }
99
+
100
+ function extractRuns(
101
+ block: Node,
102
+ blockStartPos: number,
103
+ schema: Schema
104
+ ): Run[] {
105
+ const runs: Run[] = [];
106
+ let current: Run | null = null;
107
+
108
+ block.forEach((child, offset) => {
109
+ const eligibleText =
110
+ child.isText &&
111
+ child.text &&
112
+ !schema.marks.code?.isInSet(child.marks) &&
113
+ !schema.marks.mention?.isInSet(child.marks);
114
+
115
+ if (eligibleText) {
116
+ if (!current) {
117
+ current = { text: '', startPos: blockStartPos + 1 + offset };
118
+ }
119
+ current.text += child.text;
120
+ return;
121
+ }
122
+
123
+ if (current) {
124
+ runs.push(current);
125
+ current = null;
126
+ }
127
+ });
128
+
129
+ if (current) runs.push(current);
130
+ return runs;
131
+ }
132
+
133
+ function getDirtyBlocks(
134
+ doc: Node,
135
+ transactions: readonly Transaction[]
136
+ ): DirtyBlock[] {
137
+ const mapping = new Mapping();
138
+ for (const tr of transactions) mapping.appendMapping(tr.mapping);
139
+
140
+ const blocks = new Map<number, Node>();
141
+ const docSize = doc.content.size;
142
+
143
+ mapping.maps.forEach((stepMap, i) => {
144
+ const rest = mapping.slice(i + 1);
145
+
146
+ stepMap.forEach((_oldStart, _oldEnd, newStart, newEnd) => {
147
+ const from = Math.max(0, rest.map(newStart, -1) - 1);
148
+ const to = Math.min(docSize, rest.map(newEnd, 1) + 1);
149
+
150
+ doc.nodesBetween(from, to, (node, pos) => {
151
+ if (node.type.name === 'codeBlock') return false;
152
+
153
+ if (node.inlineContent && !blocks.has(pos)) {
154
+ blocks.set(pos, node);
155
+ return false;
156
+ }
157
+
158
+ return true;
159
+ });
160
+ });
161
+ });
162
+
163
+ return Array.from(blocks, ([pos, node]) => ({ pos, node }));
164
+ }
165
+
166
+ export const AutolinkPlugin = Extension.create<AutolinkPluginOptions>({
167
+ name: 'autolinkDetector',
168
+ addOptions() {
169
+ return {
170
+ getLinkEmitter: () => {
171
+ throw new Error(
172
+ 'AutolinkPlugin.configure({ getLinkEmitter }) is required'
173
+ );
174
+ },
175
+ };
176
+ },
177
+ addProseMirrorPlugins() {
178
+ return [
179
+ new Plugin({
180
+ key: new PluginKey('autolinkDetector'),
181
+ appendTransaction: (transactions, _oldState, newState) => {
182
+ if (!transactions.some((tr) => tr.docChanged)) return null;
183
+
184
+ const state = this.options.getLinkEmitter();
185
+ if (!state || state.linkRegex === null) return null;
186
+ const { schema, doc, tr } = newState;
187
+ const linkType = schema.marks.link;
188
+ if (!linkType) return null;
189
+
190
+ const dirtyBlocks = getDirtyBlocks(doc, transactions);
191
+ if (dirtyBlocks.length === 0) return null;
192
+
193
+ const detected: OnLinkDetected[] = [];
194
+
195
+ for (const { node, pos } of dirtyBlocks) {
196
+ const blockFrom = pos + 1;
197
+ const blockTo = pos + node.nodeSize - 1;
198
+
199
+ // find all valid links that should exist in this block
200
+ const desiredLinks: Array<{
201
+ start: number;
202
+ end: number;
203
+ href: string;
204
+ }> = [];
205
+
206
+ for (const run of extractRuns(node, pos, schema)) {
207
+ for (const match of run.text.matchAll(WHITESPACE_RE)) {
208
+ const word = match[0];
209
+ const wordStart = run.startPos + match.index!;
210
+ const wordEnd = wordStart + word.length;
211
+
212
+ const ranges = findAutolinkRangesInWord(word, state.linkRegex);
213
+ const fullMatch = ranges.some(
214
+ (r) => r.start === 0 && r.endExclusive === word.length
215
+ );
216
+
217
+ if (!fullMatch) continue;
218
+ if (rangeHasManualLink(doc, linkType, wordStart, wordEnd))
219
+ continue;
220
+
221
+ desiredLinks.push({
222
+ start: wordStart,
223
+ end: wordEnd,
224
+ href: word,
225
+ });
226
+ }
227
+ }
228
+
229
+ let lastPos = blockFrom;
230
+
231
+ for (const link of desiredLinks) {
232
+ // strip auto links in the "gap" before this desired link
233
+ removeAutoLinksInRange(doc, tr, linkType, lastPos, link.start);
234
+
235
+ tr.addMark(
236
+ link.start,
237
+ link.end,
238
+ linkType.create({ href: link.href, auto: true })
239
+ );
240
+
241
+ const alreadyExisted = rangeHasExactAutoLink(
242
+ doc,
243
+ linkType,
244
+ link.start,
245
+ link.end,
246
+ link.href
247
+ );
248
+
249
+ // don't emit if the link was not changed
250
+ if (!alreadyExisted) {
251
+ detected.push({
252
+ text: link.href,
253
+ url: link.href,
254
+ start: tiptapPosToNativePos(doc, link.start),
255
+ end: tiptapPosToNativePos(doc, link.end),
256
+ });
257
+ }
258
+
259
+ lastPos = link.end;
260
+ }
261
+
262
+ // strip rest of the auto links that are now not desired
263
+ removeAutoLinksInRange(doc, tr, linkType, lastPos, blockTo);
264
+ }
265
+
266
+ if (tr.steps.length === 0) return null;
267
+
268
+ for (const event of detected) emitLinkDetected(state, event);
269
+ return tr;
270
+ },
271
+ }),
272
+ ];
273
+ },
274
+ });
@@ -11,7 +11,7 @@ export type { MentionPluginOptions, TriggerState } from './types';
11
11
  export { mentionPluginKey } from './mentionPluginKey';
12
12
  export { setMention } from './setMention';
13
13
  export { startMention } from './startMention';
14
- export { subscribeMentionEvents } from './subscribeMentionEvents';
14
+ export { useMentionEvents } from './useMentionEvents';
15
15
 
16
16
  export const MentionPlugin = Extension.create<MentionPluginOptions>({
17
17
  name: 'mentionTrigger',
@@ -1,6 +1,7 @@
1
- import type { StateField } from '@tiptap/pm/state';
1
+ import type { EditorState, StateField } from '@tiptap/pm/state';
2
2
  import { isCaretInBlockedContext } from './isCaretInBlockedContext';
3
3
  import type { MentionPluginOptions, TriggerState } from './types';
4
+ import type { ResolvedPos } from '@tiptap/pm/model';
4
5
 
5
6
  export function makeMentionPluginState(
6
7
  getIndicators: MentionPluginOptions['getIndicators']
@@ -19,9 +20,11 @@ export function makeMentionPluginState(
19
20
  return { active: false };
20
21
 
21
22
  const blockStart = $from.start();
23
+ const wordEndPos = getCurrentWordEndPosition(newEditorState, $from);
24
+
22
25
  const text = newEditorState.doc.textBetween(
23
26
  blockStart,
24
- $from.pos,
27
+ wordEndPos,
25
28
  '\n',
26
29
  '\n'
27
30
  );
@@ -45,7 +48,7 @@ export function makeMentionPluginState(
45
48
  active: true,
46
49
  indicator: found.indicator,
47
50
  from: blockStart + found.indexInText,
48
- to: $from.pos,
51
+ to: wordEndPos,
49
52
  query,
50
53
  };
51
54
  },
@@ -65,11 +68,35 @@ function findLastValidMentionIndicator(
65
68
  const isAfterSpace = idx > 0 && text[idx - 1] === ' ';
66
69
  if (!isAtStart && !isAfterSpace) continue;
67
70
 
68
- // Skip indicators inside a finalized mention
69
- if (isIndicatorInsideFinalizedMention(idx)) continue;
71
+ // Stops inside a finalized mention
72
+ if (isIndicatorInsideFinalizedMention(idx)) return null;
70
73
 
71
74
  return { indexInText: idx, indicator: ch };
72
75
  }
73
76
 
74
77
  return null;
75
78
  }
79
+
80
+ function getCurrentWordEndPosition(state: EditorState, $from: ResolvedPos) {
81
+ const blockEnd = $from.end();
82
+
83
+ let wordEndPos = $from.pos;
84
+
85
+ while (wordEndPos < blockEnd) {
86
+ const char = state.doc.textBetween(wordEndPos, wordEndPos + 1);
87
+
88
+ if (/\s/.test(char)) {
89
+ break;
90
+ }
91
+
92
+ // Break if advancing enters a blocked context
93
+ const $nextPos = state.doc.resolve(wordEndPos + 1);
94
+ if (isCaretInBlockedContext($nextPos, state.schema)) {
95
+ break;
96
+ }
97
+
98
+ wordEndPos++;
99
+ }
100
+
101
+ return wordEndPos;
102
+ }
@@ -1,5 +1,5 @@
1
1
  import { Fragment } from '@tiptap/pm/model';
2
- import type { EditorState } from '@tiptap/pm/state';
2
+ import { TextSelection } from '@tiptap/pm/state';
3
3
  import type { Editor } from '@tiptap/react';
4
4
  import { isCaretInBlockedContext } from './isCaretInBlockedContext';
5
5
  import { mentionPluginKey } from './mentionPluginKey';
@@ -34,10 +34,13 @@ export function setMention(
34
34
 
35
35
  const { from, to } = triggerState;
36
36
 
37
- const extendedTo = Math.max(
38
- to,
39
- exclusiveEndThroughMatchingMentionTail(state, from, text)
40
- );
37
+ // avoid inserting a space if there already is one
38
+ const parentEnd = state.doc.resolve(to).end();
39
+ let hasSpaceAfter = false;
40
+ if (to < parentEnd) {
41
+ const charAfter = state.doc.textBetween(to, to + 1, '');
42
+ hasSpaceAfter = /\s/.test(charAfter);
43
+ }
41
44
 
42
45
  const mentionMark = mentionType.create({
43
46
  indicator,
@@ -48,36 +51,23 @@ export function setMention(
48
51
  .resolve(from)
49
52
  .marks()
50
53
  .filter((m) => m.type.name !== 'mention');
51
- const fragment = Fragment.fromArray([
52
- state.schema.text(text, mentionMark.addToSet(baseMarks)),
53
- state.schema.text(' ', baseMarks),
54
- ]);
54
+
55
+ const nodes = [state.schema.text(text, mentionMark.addToSet(baseMarks))];
56
+ if (!hasSpaceAfter) {
57
+ nodes.push(state.schema.text(' ', baseMarks));
58
+ }
59
+ const fragment = Fragment.fromArray(nodes);
55
60
 
56
61
  editor
57
62
  .chain()
58
63
  .focus()
59
64
  .command(({ tr }) => {
60
- tr.replaceWith(from, extendedTo, fragment);
65
+ tr.replaceWith(from, to, fragment);
66
+
67
+ const targetPos = from + fragment.size + (hasSpaceAfter ? 1 : 0);
68
+ tr.setSelection(TextSelection.create(tr.doc, targetPos));
69
+
61
70
  return true;
62
71
  })
63
72
  .run();
64
73
  }
65
-
66
- // If the user moved the caret back into a partial match, extend `to` over the
67
- // matching tail to avoid leftover characters after the inserted mention.
68
- function exclusiveEndThroughMatchingMentionTail(
69
- state: EditorState,
70
- from: number,
71
- text: string
72
- ): number {
73
- const parentEnd = state.doc.resolve(from).end();
74
- let scanPos = from;
75
- for (const ch of text) {
76
- const end = scanPos + ch.length;
77
- if (end > parentEnd || state.doc.textBetween(scanPos, end, '') !== ch) {
78
- break;
79
- }
80
- scanPos = end;
81
- }
82
- return scanPos;
83
- }
@@ -0,0 +1,114 @@
1
+ import { getMarkRange } from '@tiptap/core';
2
+ import type { Editor } from '@tiptap/react';
3
+ import type { OnMentionDetected } from '../../../types';
4
+ import { mentionPluginKey } from './mentionPluginKey';
5
+ import type { MentionCallbacks, TriggerState } from './types';
6
+ import { useEffect } from 'react';
7
+ import type { EditorState } from '@tiptap/pm/state';
8
+
9
+ export function useMentionEvents(
10
+ editor: Editor,
11
+ getCallbacks: () => MentionCallbacks
12
+ ) {
13
+ useEffect(() => {
14
+ let prevTriggerState: TriggerState = { active: false };
15
+ let prevMentionKey: string | null = null;
16
+ let wasInMention = false;
17
+
18
+ const handleUpdate = () => {
19
+ const cb = getCallbacks();
20
+ const curr = mentionPluginKey.getState(editor.state);
21
+ if (!curr) return;
22
+
23
+ if (!prevTriggerState.active && curr.active) {
24
+ cb.onStartMention?.(curr.indicator);
25
+ if (curr.query !== '')
26
+ cb.onChangeMention?.({ indicator: curr.indicator, text: curr.query });
27
+ } else if (
28
+ prevTriggerState.active &&
29
+ curr.active &&
30
+ prevTriggerState.indicator !== curr.indicator
31
+ ) {
32
+ cb.onEndMention?.(prevTriggerState.indicator);
33
+ cb.onStartMention?.(curr.indicator);
34
+ if (curr.query !== '')
35
+ cb.onChangeMention?.({ indicator: curr.indicator, text: curr.query });
36
+ } else if (
37
+ prevTriggerState.active &&
38
+ curr.active &&
39
+ curr.query !== prevTriggerState.query
40
+ ) {
41
+ cb.onChangeMention?.({ indicator: curr.indicator, text: curr.query });
42
+ } else if (prevTriggerState.active && !curr.active) {
43
+ cb.onEndMention?.(prevTriggerState.indicator);
44
+ }
45
+ prevTriggerState = curr;
46
+
47
+ if (!cb.onMentionDetected) return;
48
+
49
+ const mention = getMentionInCurrentSelection(editor.state);
50
+ if (!mention) {
51
+ if (wasInMention) {
52
+ cb.onMentionDetected({
53
+ text: '',
54
+ indicator: '',
55
+ attributes: {},
56
+ });
57
+ wasInMention = false;
58
+ prevMentionKey = null;
59
+ }
60
+ } else {
61
+ wasInMention = true;
62
+ if (mention.key === prevMentionKey) return;
63
+ prevMentionKey = mention.key;
64
+ cb.onMentionDetected({
65
+ text: mention.text,
66
+ indicator: mention.indicator,
67
+ attributes: mention.attributes,
68
+ });
69
+ }
70
+ };
71
+
72
+ const handleBlur = () => {
73
+ const cb = getCallbacks();
74
+ if (prevTriggerState.active) {
75
+ cb.onEndMention?.(prevTriggerState.indicator);
76
+ prevTriggerState = { active: false };
77
+ }
78
+ prevMentionKey = null;
79
+ };
80
+
81
+ editor.on('transaction', handleUpdate);
82
+ editor.on('blur', handleBlur);
83
+
84
+ return () => {
85
+ editor.off('transaction', handleUpdate);
86
+ editor.off('blur', handleBlur);
87
+ };
88
+ }, [editor, getCallbacks]);
89
+ }
90
+
91
+ function getMentionInCurrentSelection(
92
+ state: EditorState
93
+ ): (OnMentionDetected & { key: string }) | null {
94
+ const mentionType = state.schema.marks.mention;
95
+ if (!mentionType) return null;
96
+
97
+ const { from: selFrom, to: selTo } = state.selection;
98
+ const $from = state.doc.resolve(selFrom);
99
+ const mark = mentionType.isInSet($from.marks());
100
+ if (!mark) return null;
101
+
102
+ const range = getMarkRange($from, mentionType);
103
+ if (!range) return null;
104
+
105
+ if (selFrom < range.from || selTo > range.to) return null;
106
+
107
+ const { text, indicator, attributes } = mark.attrs;
108
+ return {
109
+ key: `${range.from}:${range.to}:${text}:${indicator}`,
110
+ text: text as string,
111
+ indicator: indicator as string,
112
+ attributes: (attributes ?? {}) as Record<string, string>,
113
+ };
114
+ }