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
@@ -1,11 +1,12 @@
1
1
  import {
2
+ useCallback,
2
3
  useEffect,
3
4
  useImperativeHandle,
4
5
  useMemo,
5
6
  useRef,
6
7
  type CSSProperties,
7
8
  } from 'react';
8
- import './EnrichedTextInput.css';
9
+ import './EnrichedText.css';
9
10
  import type { Node } from '@tiptap/pm/model';
10
11
  import type {
11
12
  EnrichedTextInputInstance,
@@ -32,13 +33,14 @@ import { useOnChangeHtml } from './useOnChangeHtml';
32
33
  import { useOnChangeText } from './useOnChangeText';
33
34
  import { useOnChangeState } from './useOnChangeState';
34
35
  import { useOnLinkDetected } from './useOnLinkDetected';
36
+ import type { LinkEmitterState } from './emitLinkDetected';
35
37
  import {
36
38
  prepareHtmlForTiptap,
37
39
  normalizeHtmlFromTiptap,
38
- } from './tiptapHtmlNormalizer';
40
+ } from './normalization/tiptapHtmlNormalizer';
39
41
  import { ENRICHED_TEXT_INPUT_DEFAULT_PROPS } from '../utils/EnrichedTextInputDefaultProps';
40
42
  import { enrichedInputStyleToCSSProperties } from './styleConversion/enrichedInputStyleToCSSProperties';
41
- import { enrichedInputThemingToCSSProperties } from './styleConversion/enrichedInputThemingToCSSProperties';
43
+ import { enrichedInputThemingToCSSProperties } from './styleConversion/enrichedThemingToCSSProperties';
42
44
  import { buildMentionRulesCSS } from './styleConversion/buildMentionRulesCSS';
43
45
  import {
44
46
  htmlStyleToCSSVariables,
@@ -60,6 +62,7 @@ import { EnrichedUnorderedList } from './formats/EnrichedUnorderedList';
60
62
  import { EnrichedOrderedList } from './formats/EnrichedOrderedList';
61
63
  import { EnrichedCheckboxItem } from './formats/EnrichedCheckboxItem';
62
64
  import { EnrichedCheckboxList } from './formats/EnrichedCheckboxList';
65
+ import { EnrichedTextAlign } from './formats/EnrichedTextAlign';
63
66
  import { StripBoldInStyledHeadingsPlugin } from './pmPlugins/StripBoldInStyledHeadingsPlugin';
64
67
  import { StrictMarksPlugin } from './pmPlugins/StrictMarksPlugin';
65
68
  import { MergeAdjacentSameKindBlocksPlugin } from './pmPlugins/MergeAdjacentSameKindBlocksPlugin';
@@ -69,11 +72,15 @@ import {
69
72
  MentionPlugin,
70
73
  setMention,
71
74
  startMention,
72
- subscribeMentionEvents,
75
+ useMentionEvents,
73
76
  } from './pmPlugins/MentionPlugin';
74
77
  import { StripMarksOnImagePlugin } from './pmPlugins/StripMarksOnImagePlugin';
75
78
  import { ShortcutPlugin } from './pmPlugins/ShortcutPlugin';
76
79
  import { returnKeyTypeToEnterKeyHint } from './returnKeyTypeToEnterKeyHint';
80
+ import { ENRICHED_TEXT_INPUT_CLASSNAME } from './constants/classNames';
81
+ import { AutolinkPlugin } from './pmPlugins/AutolinkPlugin';
82
+ import { useStableRef } from './useStableRef';
83
+
77
84
  function runFocused(
78
85
  editor: Editor,
79
86
  apply: (chain: ChainedCommands) => ChainedCommands
@@ -110,60 +117,39 @@ export const EnrichedTextInput = ({
110
117
  onStartMention,
111
118
  onChangeMention,
112
119
  onEndMention,
120
+ linkRegex,
113
121
  htmlStyle,
122
+ useHtmlNormalizer,
114
123
  }: EnrichedTextInputProps) => {
115
124
  const tiptapContent =
116
- defaultValue != null ? prepareHtmlForTiptap(defaultValue) : defaultValue;
125
+ defaultValue != null
126
+ ? prepareHtmlForTiptap(defaultValue, useHtmlNormalizer)
127
+ : defaultValue;
117
128
 
118
129
  const resolvedHtmlStyle = useMemo(
119
130
  () => mergeWithDefaultHtmlStyle(htmlStyle),
120
131
  [htmlStyle]
121
132
  );
122
-
123
- const htmlStyleRef = useRef(resolvedHtmlStyle);
124
- useEffect(() => {
125
- htmlStyleRef.current = resolvedHtmlStyle;
126
- }, [resolvedHtmlStyle]);
127
-
128
- const onPasteImagesRef = useRef(onPasteImages);
129
- useEffect(() => {
130
- onPasteImagesRef.current = onPasteImages;
131
- }, [onPasteImages]);
132
-
133
- const mentionIndicatorsRef = useRef(mentionIndicators);
134
- useEffect(() => {
135
- mentionIndicatorsRef.current = mentionIndicators;
136
- }, [mentionIndicators]);
137
-
138
- const mentionCallbacksRef = useRef({
139
- onStartMention,
140
- onChangeMention,
141
- onEndMention,
142
- onMentionDetected,
143
- });
144
- useEffect(() => {
145
- mentionCallbacksRef.current = {
133
+ const mentionCallbacks = useMemo(
134
+ () => ({
146
135
  onStartMention,
147
136
  onChangeMention,
148
137
  onEndMention,
149
138
  onMentionDetected,
150
- };
151
- }, [onStartMention, onChangeMention, onEndMention, onMentionDetected]);
139
+ }),
140
+ [onStartMention, onChangeMention, onEndMention, onMentionDetected]
141
+ );
152
142
 
153
- const submitBehaviorRef = useRef(submitBehavior);
154
- const onSubmitEditingRef = useRef(onSubmitEditing);
155
- const onKeyPressRef = useRef(onKeyPress);
156
- const editorInstanceRef = useRef<Editor | null>(null);
143
+ const htmlStyleRef = useStableRef(resolvedHtmlStyle);
144
+ const onPasteImagesRef = useStableRef(onPasteImages);
145
+ const mentionIndicatorsRef = useStableRef(mentionIndicators);
146
+ const submitBehaviorRef = useStableRef(submitBehavior);
147
+ const onSubmitEditingRef = useStableRef(onSubmitEditing);
148
+ const onKeyPressRef = useStableRef(onKeyPress);
149
+ const useHtmlNormalizerRef = useStableRef(useHtmlNormalizer);
150
+ const mentionCallbacksRef = useStableRef(mentionCallbacks);
157
151
 
158
- useEffect(() => {
159
- submitBehaviorRef.current = submitBehavior;
160
- }, [submitBehavior]);
161
- useEffect(() => {
162
- onSubmitEditingRef.current = onSubmitEditing;
163
- }, [onSubmitEditing]);
164
- useEffect(() => {
165
- onKeyPressRef.current = onKeyPress;
166
- }, [onKeyPress]);
152
+ const editorInstanceRef = useRef<Editor | null>(null);
167
153
 
168
154
  const handleKeyDown = (doc: Node, event: KeyboardEvent): boolean => {
169
155
  onKeyPressRef.current?.(adaptWebToNativeEvent(event, { key: event.key }));
@@ -185,6 +171,16 @@ export const EnrichedTextInput = ({
185
171
  return false;
186
172
  };
187
173
 
174
+ const linkEmitterRef = useRef<LinkEmitterState>({
175
+ linkRegex,
176
+ onLinkDetected,
177
+ lastEmitted: null,
178
+ });
179
+ useEffect(() => {
180
+ linkEmitterRef.current.linkRegex = linkRegex;
181
+ linkEmitterRef.current.onLinkDetected = onLinkDetected;
182
+ }, [linkRegex, onLinkDetected]);
183
+
188
184
  const extensions = useMemo(
189
185
  () => [
190
186
  Document,
@@ -196,7 +192,9 @@ export const EnrichedTextInput = ({
196
192
  EnrichedUnderline,
197
193
  EnrichedStrike,
198
194
  EnrichedCode,
199
- EnrichedLink,
195
+ EnrichedLink.configure({
196
+ getLinkRegex: () => linkEmitterRef.current.linkRegex,
197
+ }),
200
198
  EnrichedImage,
201
199
  EnrichedMention,
202
200
  EnrichedHeading,
@@ -207,6 +205,7 @@ export const EnrichedTextInput = ({
207
205
  EnrichedUnorderedList,
208
206
  EnrichedOrderedList,
209
207
  EnrichedCheckboxList,
208
+ EnrichedTextAlign,
210
209
  StripMarksInCodeBlockPlugin,
211
210
  StripMarksOnImagePlugin,
212
211
  StripBoldInStyledHeadingsPlugin.configure({
@@ -220,12 +219,15 @@ export const EnrichedTextInput = ({
220
219
  ShortcutPlugin.configure({
221
220
  getHtmlStyle: () => htmlStyleRef.current,
222
221
  }),
222
+ AutolinkPlugin.configure({
223
+ getLinkEmitter: () => linkEmitterRef.current,
224
+ }),
223
225
  Placeholder.configure({
224
226
  placeholder,
225
227
  showOnlyWhenEditable: true,
226
228
  }),
227
229
  ],
228
- [placeholder]
230
+ [placeholder, htmlStyleRef, mentionIndicatorsRef]
229
231
  );
230
232
 
231
233
  const editor = useEditor(
@@ -264,6 +266,9 @@ export const EnrichedTextInput = ({
264
266
  autoCapitalize,
265
267
  enterkeyhint: returnKeyTypeToEnterKeyHint(returnKeyType),
266
268
  },
269
+ transformPastedHTML: (html) => {
270
+ return prepareHtmlForTiptap(html, useHtmlNormalizerRef.current);
271
+ },
267
272
  },
268
273
  },
269
274
  [tiptapContent, extensions]
@@ -291,15 +296,16 @@ export const EnrichedTextInput = ({
291
296
  editor?.commands.normalizeBoldInStyledHeadings();
292
297
  }, [editor, resolvedHtmlStyle]);
293
298
 
294
- useEffect(() => {
295
- if (!editor) return;
296
- return subscribeMentionEvents(editor, () => mentionCallbacksRef.current);
297
- }, [editor]);
299
+ const getMentionCallbacks = useCallback(
300
+ () => mentionCallbacksRef.current,
301
+ [mentionCallbacksRef]
302
+ );
298
303
 
304
+ useMentionEvents(editor, getMentionCallbacks);
299
305
  useOnChangeHtml(editor, onChangeHtml);
300
306
  useOnChangeText(editor, onChangeText);
301
307
  useOnChangeState(editor, resolvedHtmlStyle, onChangeState);
302
- useOnLinkDetected(editor, onLinkDetected);
308
+ useOnLinkDetected(editor, linkEmitterRef);
303
309
 
304
310
  useImperativeHandle(
305
311
  ref,
@@ -307,7 +313,9 @@ export const EnrichedTextInput = ({
307
313
  focus: () => editor.commands.focus(),
308
314
  blur: () => editor.commands.blur(),
309
315
  setValue: (value: string) =>
310
- editor.commands.setContent(prepareHtmlForTiptap(value)),
316
+ editor.commands.setContent(
317
+ prepareHtmlForTiptap(value, useHtmlNormalizerRef.current)
318
+ ),
311
319
  setSelection: (start, end) => {
312
320
  const doc = editor.state.doc;
313
321
  runFocused(editor, (c) =>
@@ -354,9 +362,15 @@ export const EnrichedTextInput = ({
354
362
  measureInWindow: () => {},
355
363
  measureLayout: () => {},
356
364
  setNativeProps: () => {},
357
- setTextAlignment: () => {},
365
+ setTextAlignment: (alignment) => {
366
+ if (alignment === 'auto') {
367
+ runFocused(editor, (c) => c.unsetTextAlign());
368
+ } else {
369
+ runFocused(editor, (c) => c.setTextAlign(alignment));
370
+ }
371
+ },
358
372
  }),
359
- [editor]
373
+ [editor, mentionIndicatorsRef, useHtmlNormalizerRef]
360
374
  );
361
375
 
362
376
  const editorStyle: CSSProperties = useMemo(
@@ -394,7 +408,7 @@ export const EnrichedTextInput = ({
394
408
  {mentionRulesCSS ? <style>{mentionRulesCSS}</style> : null}
395
409
  <EditorContent
396
410
  editor={editor}
397
- className="eti-editor"
411
+ className={ENRICHED_TEXT_INPUT_CLASSNAME}
398
412
  style={finalStyle}
399
413
  data-placeholder={placeholder}
400
414
  />
@@ -0,0 +1,7 @@
1
+ // SVG path of the broken-image glyph, shared between the editor's
2
+ // image node view (rendered as an inline <svg>).
3
+ export const BROKEN_IMAGE_PATH_D =
4
+ 'M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM240,503L400,343L560,503L720,343L760,383L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,463L240,503ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,496L720,456L560,616L400,456L240,616L200,576L200,760Q200,760 200,760Q200,760 200,760ZM200,760L200,760Q200,760 200,760Q200,760 200,760L200,496L200,576L200,463L200,383L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,463L200,463L200,576L200,576L200,760Q200,760 200,760Q200,760 200,760Z';
5
+
6
+ // Glyph as a `mask`-ready data URI, used in EnrichedText.css.
7
+ export const BROKEN_IMAGE_GLYPH_MASK = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 960 960'%3E%3Cpath d='${BROKEN_IMAGE_PATH_D}'/%3E%3C/svg%3E")`;
@@ -0,0 +1,2 @@
1
+ export const ENRICHED_TEXT_INPUT_CLASSNAME = 'eti-editor';
2
+ export const ENRICHED_TEXT_CLASSNAME = 'et-view';
@@ -0,0 +1,29 @@
1
+ import type { RefObject } from 'react';
2
+ import type { OnLinkDetected } from '../types';
3
+
4
+ export interface LinkEmitterState {
5
+ linkRegex?: RegExp | null;
6
+ onLinkDetected?: (e: OnLinkDetected) => void;
7
+ lastEmitted: OnLinkDetected | null;
8
+ }
9
+
10
+ export type LinkEmitterRef = RefObject<LinkEmitterState>;
11
+
12
+ function isSamePayload(a: OnLinkDetected, b: OnLinkDetected): boolean {
13
+ return (
14
+ a.text === b.text &&
15
+ a.url === b.url &&
16
+ a.start === b.start &&
17
+ a.end === b.end
18
+ );
19
+ }
20
+
21
+ export function emitLinkDetected(
22
+ state: LinkEmitterState,
23
+ next: OnLinkDetected
24
+ ): void {
25
+ const prev = state.lastEmitted;
26
+ if (prev && isSamePayload(prev, next)) return;
27
+ state.lastEmitted = next;
28
+ state.onLinkDetected?.(next);
29
+ }
@@ -29,7 +29,8 @@ export const EnrichedBlockquote = Blockquote.extend({
29
29
  () => editor.isActive('blockquote'),
30
30
  () => commands.lift('blockquote'),
31
31
  (c) => c.toggleWrap('blockquote'),
32
- chain
32
+ chain,
33
+ editor
33
34
  ),
34
35
  };
35
36
  },
@@ -2,6 +2,7 @@ import { type CommandProps } from '@tiptap/core';
2
2
  import { TaskList } from '@tiptap/extension-list';
3
3
 
4
4
  import { applyWrappingListToSelection } from './applyWrappingListToSelection';
5
+ import { withPreservedAlignment } from './formatRules';
5
6
 
6
7
  declare module '@tiptap/core' {
7
8
  interface Commands<ReturnType> {
@@ -24,9 +25,11 @@ export const EnrichedCheckboxList = TaskList.extend({
24
25
  addCommands() {
25
26
  return {
26
27
  toggleCheckboxList: (checked: boolean) => {
27
- return ({ editor, commands, chain }: CommandProps): boolean => {
28
+ return ({ editor, chain }: CommandProps): boolean => {
28
29
  if (editor.isActive('checkboxList')) {
29
- return commands.setParagraph();
30
+ return withPreservedAlignment(editor, chain(), (c) =>
31
+ c.clearNodes().setParagraph()
32
+ );
30
33
  }
31
34
 
32
35
  return applyWrappingListToSelection(
@@ -35,7 +35,8 @@ export const EnrichedCodeBlock = Blockquote.extend({
35
35
  () => editor.isActive('codeBlock'),
36
36
  () => commands.lift('codeBlock'),
37
37
  (c) => c.toggleWrap('codeBlock'),
38
- chain
38
+ chain,
39
+ editor
39
40
  ),
40
41
  };
41
42
  },
@@ -29,7 +29,8 @@ export const EnrichedHeading = Heading.configure({
29
29
  () => editor.isActive('heading', attrs),
30
30
  () => commands.setParagraph(),
31
31
  (c) => c.setHeading(attrs),
32
- chain
32
+ chain,
33
+ editor
33
34
  ),
34
35
  };
35
36
  },
@@ -1,12 +1,10 @@
1
1
  import { NodeViewWrapper, type ReactNodeViewProps } from '@tiptap/react';
2
2
  import type { CSSProperties } from 'react';
3
3
  import { useState } from 'react';
4
+ import { BROKEN_IMAGE_PATH_D } from '../constants/brokenImageGlyph';
4
5
 
5
6
  const IMAGE_FALLBACK_SIZE = 80;
6
7
 
7
- const BROKEN_IMAGE_PATH_D =
8
- 'M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM240,503L400,343L560,503L720,343L760,383L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,463L240,503ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,496L720,456L560,616L400,456L240,616L200,576L200,760Q200,760 200,760Q200,760 200,760ZM200,760L200,760Q200,760 200,760Q200,760 200,760L200,496L200,576L200,463L200,383L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,463L200,463L200,576L200,576L200,760Q200,760 200,760Q200,760 200,760Z';
9
-
10
8
  function BrokenImageGlyph() {
11
9
  return (
12
10
  <svg
@@ -1,15 +1,63 @@
1
- import Link from '@tiptap/extension-link';
2
- import type { CommandProps } from '@tiptap/core';
1
+ import Link, { type LinkOptions } from '@tiptap/extension-link';
2
+ import { mergeAttributes, type CommandProps } from '@tiptap/core';
3
3
  import type { Editor } from '@tiptap/react';
4
4
 
5
5
  import { nativePosToTiptapPos } from '../positionMapping';
6
6
  import { isLinkBlocked } from './formatRules';
7
+ import { findAutolinkRangesInWord } from '../pmPlugins/AutolinkPlugin/autolinkRegex';
7
8
 
8
- export const EnrichedLink = Link.extend({
9
+ function isFullLinkMatch(text: string, linkRegex: RegExp | undefined): boolean {
10
+ const ranges = findAutolinkRangesInWord(text, linkRegex);
11
+ return ranges.some((r) => r.start === 0 && r.endExclusive === text.length);
12
+ }
13
+
14
+ export const EnrichedLink = Link.extend<
15
+ LinkOptions & { getLinkRegex: () => RegExp | null | undefined }
16
+ >({
9
17
  excludes: 'link code',
10
18
 
19
+ parseHTML() {
20
+ return [
21
+ {
22
+ tag: 'a[href]',
23
+ getAttrs: (node: string | HTMLElement) => {
24
+ if (typeof node === 'string') return null;
25
+ const href = node.getAttribute('href');
26
+ if (!href || /^javascript:/i.test(href)) return false;
27
+ return null;
28
+ },
29
+ },
30
+ ];
31
+ },
32
+
33
+ addAttributes() {
34
+ return {
35
+ ...this.parent?.(),
36
+ auto: {
37
+ default: false,
38
+ parseHTML: (el) => {
39
+ const href = el.getAttribute('href');
40
+ const textContent = el.textContent;
41
+ if (!href || href !== textContent) return false;
42
+
43
+ const linkRegex = this.options.getLinkRegex();
44
+ if (linkRegex === null) return false;
45
+
46
+ return isFullLinkMatch(href, linkRegex);
47
+ },
48
+ renderHTML: () => {
49
+ return {};
50
+ },
51
+ },
52
+ };
53
+ },
54
+
11
55
  renderHTML({ HTMLAttributes }) {
12
- return ['a', { href: HTMLAttributes.href }, 0];
56
+ return [
57
+ 'a',
58
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
59
+ 0,
60
+ ];
13
61
  },
14
62
 
15
63
  addOptions() {
@@ -24,6 +72,9 @@ export const EnrichedLink = Link.extend({
24
72
  target: null,
25
73
  rel: null,
26
74
  },
75
+ getLinkRegex: () => {
76
+ throw new Error('EnrichedLink.configure({ getLinkRegex }) is required');
77
+ },
27
78
  };
28
79
  },
29
80
 
@@ -55,6 +106,10 @@ export const EnrichedLink = Link.extend({
55
106
  },
56
107
  };
57
108
  },
109
+
110
+ addPasteRules() {
111
+ return [];
112
+ },
58
113
  });
59
114
 
60
115
  export function removeLink(editor: Editor, start: number, end: number) {
@@ -2,6 +2,7 @@ import { wrappingInputRule } from '@tiptap/core';
2
2
  import { OrderedList } from '@tiptap/extension-list';
3
3
 
4
4
  import { applyWrappingListToSelection } from './applyWrappingListToSelection';
5
+ import { withPreservedAlignment } from './formatRules';
5
6
 
6
7
  const ORDERED_LIST_INPUT_REGEX = /^1\.\s$/;
7
8
 
@@ -23,9 +24,11 @@ export const EnrichedOrderedList = OrderedList.extend({
23
24
  return {
24
25
  toggleOrderedList:
25
26
  () =>
26
- ({ editor, commands, chain }) => {
27
+ ({ editor, chain }) => {
27
28
  if (editor.isActive('orderedList')) {
28
- return commands.setParagraph();
29
+ return withPreservedAlignment(editor, chain(), (c) =>
30
+ c.clearNodes().setParagraph()
31
+ );
29
32
  }
30
33
 
31
34
  return applyWrappingListToSelection(
@@ -0,0 +1,88 @@
1
+ import { isTextSelection } from '@tiptap/core';
2
+ import { TextAlign } from '@tiptap/extension-text-align';
3
+
4
+ export const EnrichedTextAlign = TextAlign.extend({
5
+ addCommands() {
6
+ return {
7
+ ...this.parent?.(),
8
+ setTextAlign: (alignment) => (props) => {
9
+ const { state, dispatch, tr } = props;
10
+
11
+ if (!this.options.alignments.includes(alignment)) {
12
+ return false;
13
+ }
14
+
15
+ const { $from } = state.selection;
16
+ let listNode: typeof $from.parent | null = null;
17
+ let listPos = -1;
18
+
19
+ // Walk up the tree to see if the cursor is inside a list wrapper
20
+ for (let depth = $from.depth; depth > 0; depth--) {
21
+ const node = $from.node(depth);
22
+ if (
23
+ ['orderedList', 'unorderedList', 'checkboxList'].includes(
24
+ node.type.name
25
+ )
26
+ ) {
27
+ listNode = node;
28
+ listPos = $from.before(depth);
29
+ break;
30
+ }
31
+ }
32
+
33
+ // If in a list, manually apply the alignment to the parent wrapper
34
+ if (listNode) {
35
+ if (dispatch) {
36
+ tr.setNodeMarkup(listPos, undefined, {
37
+ ...listNode.attrs,
38
+ textAlign: alignment,
39
+ });
40
+ }
41
+ return true;
42
+ }
43
+
44
+ // If not in a list, fire the original Tiptap command
45
+ return this.parent?.().setTextAlign?.(alignment)(props) ?? false;
46
+ },
47
+ };
48
+ },
49
+
50
+ addKeyboardShortcuts() {
51
+ return {
52
+ ...this.parent?.(),
53
+ Backspace: () => {
54
+ const { selection } = this.editor.state;
55
+
56
+ if (!selection.empty || !isTextSelection(selection)) {
57
+ return false;
58
+ }
59
+
60
+ const { $cursor } = selection;
61
+
62
+ if (!$cursor || !this.editor.isEmpty) {
63
+ return false;
64
+ }
65
+
66
+ const currentAlignment = $cursor.parent.attrs.textAlign;
67
+ const hasAlignment = currentAlignment && currentAlignment !== 'auto';
68
+
69
+ // If the input is empty and has an alignment, clear the alignment
70
+ if (hasAlignment) {
71
+ return this.editor.commands.unsetTextAlign();
72
+ }
73
+
74
+ return false;
75
+ },
76
+ };
77
+ },
78
+ }).configure({
79
+ types: [
80
+ 'paragraph',
81
+ 'heading',
82
+ 'orderedList',
83
+ 'unorderedList',
84
+ 'checkboxList',
85
+ ],
86
+ defaultAlignment: null,
87
+ alignments: ['left', 'center', 'right', 'justify'],
88
+ });
@@ -2,6 +2,7 @@ import { wrappingInputRule, type CommandProps } from '@tiptap/core';
2
2
  import { BulletList } from '@tiptap/extension-list';
3
3
 
4
4
  import { applyWrappingListToSelection } from './applyWrappingListToSelection';
5
+ import { withPreservedAlignment } from './formatRules';
5
6
 
6
7
  declare module '@tiptap/core' {
7
8
  interface Commands<ReturnType> {
@@ -33,9 +34,11 @@ export const EnrichedUnorderedList = BulletList.extend({
33
34
  return {
34
35
  toggleUnorderedList:
35
36
  () =>
36
- ({ editor, commands, chain }: CommandProps) => {
37
+ ({ editor, chain }: CommandProps) => {
37
38
  if (editor.isActive('unorderedList')) {
38
- return commands.setParagraph();
39
+ return withPreservedAlignment(editor, chain(), (c) =>
40
+ c.clearNodes().setParagraph()
41
+ );
39
42
  }
40
43
 
41
44
  return applyWrappingListToSelection(
@@ -4,6 +4,7 @@ import { Fragment } from '@tiptap/pm/model';
4
4
  import { TextSelection } from '@tiptap/pm/state';
5
5
 
6
6
  import { nativePosToTiptapPos, tiptapPosToNativePos } from '../positionMapping';
7
+ import { withPreservedAlignment } from './formatRules';
7
8
 
8
9
  /**
9
10
  * Clears block styling with `clearNodes`, then wraps the selection’s blocks in a flat
@@ -31,9 +32,8 @@ export function applyWrappingListToSelection(
31
32
  const nativeAnchor = tiptapPosToNativePos(docBefore, selBefore.anchor);
32
33
  const nativeHead = tiptapPosToNativePos(docBefore, selBefore.head);
33
34
 
34
- return chain()
35
- .clearNodes()
36
- .command(({ tr, state }) => {
35
+ return withPreservedAlignment(editor, chain(), (c) =>
36
+ c.clearNodes().command(({ tr, state }) => {
37
37
  const listType = state.schema.nodes[listTypeName];
38
38
  const itemType = state.schema.nodes[itemTypeName];
39
39
  if (!listType || !itemType) {
@@ -72,5 +72,5 @@ export function applyWrappingListToSelection(
72
72
  );
73
73
  return true;
74
74
  })
75
- .run();
75
+ );
76
76
  }
@@ -1,7 +1,6 @@
1
1
  import type { Editor } from '@tiptap/core';
2
2
  import type { HtmlStyle } from '../../types';
3
3
  import { HEADING_LEVELS, HEADING_TAGS } from './EnrichedHeading';
4
-
5
4
  type ChainedCommands = ReturnType<Editor['chain']>;
6
5
 
7
6
  export function isAnyParagraphFormatActive(editor: Editor): boolean {
@@ -77,8 +76,53 @@ export function toggleParagraphFormat(
77
76
  isActive: () => boolean,
78
77
  deactivate: () => boolean,
79
78
  activate: (c: ChainedCommands) => ChainedCommands,
80
- chain: () => ChainedCommands
79
+ chain: () => ChainedCommands,
80
+ editor: Editor
81
81
  ): boolean {
82
- if (isActive()) return deactivate();
83
- return activate(chain().clearNodes()).run();
82
+ if (isActive()) {
83
+ return withPreservedAlignment(editor, chain(), (c) => {
84
+ deactivate();
85
+ return c;
86
+ });
87
+ }
88
+
89
+ return withPreservedAlignment(editor, chain(), (c) =>
90
+ activate(c.clearNodes())
91
+ );
92
+ }
93
+
94
+ export function getCurrentAlignment(editor: Editor): string | null {
95
+ const { doc, selection } = editor.state;
96
+ let { $from } = selection;
97
+
98
+ // If the user presses Cmd+A, the selection anchors to the document root (depth 0).
99
+ // We resolve position '1' to step exactly inside the first paragraph node
100
+ // so the loop can correctly read its alignment.
101
+ if ($from.depth === 0 && doc.content.size > 0) {
102
+ $from = doc.resolve(1);
103
+ }
104
+
105
+ for (let depth = $from.depth; depth >= 0; depth--) {
106
+ const node = $from.node(depth);
107
+ if (node.attrs.textAlign) {
108
+ return node.attrs.textAlign;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+
114
+ export function withPreservedAlignment(
115
+ editor: Editor,
116
+ chain: ChainedCommands,
117
+ mutateChain: (c: ChainedCommands) => ChainedCommands
118
+ ): boolean {
119
+ const currentAlignment = getCurrentAlignment(editor);
120
+
121
+ const c = mutateChain(chain);
122
+
123
+ if (currentAlignment && currentAlignment !== 'auto') {
124
+ c.setTextAlign(currentAlignment);
125
+ }
126
+
127
+ return c.run();
84
128
  }