@fendent/react-native-enriched 0.5.2-fork.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 (278) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +343 -0
  3. package/ReactNativeEnriched.podspec +31 -0
  4. package/android/build.gradle +106 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +197 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +72 -0
  7. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ComponentDescriptors.cpp +22 -0
  8. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ComponentDescriptors.h +24 -0
  9. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.cpp +434 -0
  10. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.h +391 -0
  11. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.cpp +173 -0
  12. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.h +833 -0
  13. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ShadowNodes.cpp +17 -0
  14. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ShadowNodes.h +23 -0
  15. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/States.cpp +16 -0
  16. package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/States.h +20 -0
  17. package/android/gradle.properties +5 -0
  18. package/android/lint.gradle +70 -0
  19. package/android/src/main/AndroidManifest.xml +3 -0
  20. package/android/src/main/AndroidManifestNew.xml +2 -0
  21. package/android/src/main/java/com/swmansion/enriched/ReactNativeEnrichedPackage.kt +20 -0
  22. package/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt +126 -0
  23. package/android/src/main/java/com/swmansion/enriched/common/CheckboxDrawable.kt +81 -0
  24. package/android/src/main/java/com/swmansion/enriched/common/EnrichedConstants.kt +11 -0
  25. package/android/src/main/java/com/swmansion/enriched/common/EnrichedStyle.kt +57 -0
  26. package/android/src/main/java/com/swmansion/enriched/common/ForceRedrawSpan.kt +14 -0
  27. package/android/src/main/java/com/swmansion/enriched/common/GumboNormalizer.kt +5 -0
  28. package/android/src/main/java/com/swmansion/enriched/common/MentionStyle.kt +7 -0
  29. package/android/src/main/java/com/swmansion/enriched/common/ResourceManager.kt +26 -0
  30. package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java +956 -0
  31. package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt +79 -0
  32. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedBlockQuoteSpan.kt +53 -0
  33. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedBoldSpan.kt +12 -0
  34. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCheckboxListSpan.kt +92 -0
  35. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCodeBlockSpan.kt +81 -0
  36. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH1Span.kt +20 -0
  37. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH2Span.kt +20 -0
  38. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH3Span.kt +20 -0
  39. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH4Span.kt +21 -0
  40. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH5Span.kt +20 -0
  41. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH6Span.kt +20 -0
  42. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedImageSpan.kt +184 -0
  43. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedInlineCodeSpan.kt +24 -0
  44. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedItalicSpan.kt +12 -0
  45. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedLinkSpan.kt +29 -0
  46. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedMentionSpan.kt +35 -0
  47. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedOrderedListSpan.kt +79 -0
  48. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedStrikeThroughSpan.kt +11 -0
  49. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnderlineSpan.kt +11 -0
  50. package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnorderedListSpan.kt +62 -0
  51. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedBlockSpan.kt +5 -0
  52. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedHeadingSpan.kt +3 -0
  53. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedInlineSpan.kt +3 -0
  54. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedParagraphSpan.kt +5 -0
  55. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedSpan.kt +3 -0
  56. package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedZeroWidthSpaceSpan.kt +4 -0
  57. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputConnectionWrapper.kt +140 -0
  58. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt +83 -0
  59. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +1120 -0
  60. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewLayoutManager.kt +27 -0
  61. package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt +478 -0
  62. package/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt +225 -0
  63. package/android/src/main/java/com/swmansion/enriched/textinput/events/MentionHandler.kt +55 -0
  64. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeHtmlEvent.kt +27 -0
  65. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeSelectionEvent.kt +30 -0
  66. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeStateEvent.kt +21 -0
  67. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeTextEvent.kt +30 -0
  68. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnContextMenuItemPressEvent.kt +35 -0
  69. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputBlurEvent.kt +25 -0
  70. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputFocusEvent.kt +25 -0
  71. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputKeyPressEvent.kt +27 -0
  72. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnLinkDetectedEvent.kt +32 -0
  73. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionDetectedEvent.kt +30 -0
  74. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionEvent.kt +34 -0
  75. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnPasteImagesEvent.kt +47 -0
  76. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnRequestHtmlResultEvent.kt +32 -0
  77. package/android/src/main/java/com/swmansion/enriched/textinput/events/OnSubmitEditingEvent.kt +29 -0
  78. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputBlockQuoteSpan.kt +14 -0
  79. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputBoldSpan.kt +14 -0
  80. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputCheckboxListSpan.kt +15 -0
  81. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputCodeBlockSpan.kt +14 -0
  82. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH1Span.kt +14 -0
  83. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH2Span.kt +14 -0
  84. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH3Span.kt +14 -0
  85. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH4Span.kt +14 -0
  86. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH5Span.kt +14 -0
  87. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH6Span.kt +14 -0
  88. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputImageSpan.kt +36 -0
  89. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputInlineCodeSpan.kt +14 -0
  90. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputItalicSpan.kt +14 -0
  91. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputLinkSpan.kt +16 -0
  92. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputMentionSpan.kt +18 -0
  93. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputOrderedListSpan.kt +21 -0
  94. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputStrikeThroughSpan.kt +14 -0
  95. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputUnderlineSpan.kt +14 -0
  96. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputUnorderedListSpan.kt +14 -0
  97. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt +44 -0
  98. package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedSpans.kt +241 -0
  99. package/android/src/main/java/com/swmansion/enriched/textinput/spans/interfaces/EnrichedInputSpan.kt +10 -0
  100. package/android/src/main/java/com/swmansion/enriched/textinput/styles/HtmlStyle.kt +372 -0
  101. package/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +164 -0
  102. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt +263 -0
  103. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt +434 -0
  104. package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +394 -0
  105. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedEditableFactory.kt +17 -0
  106. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt +320 -0
  107. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpanState.kt +310 -0
  108. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt +106 -0
  109. package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannableStringBuilder.kt +24 -0
  110. package/android/src/main/java/com/swmansion/enriched/textinput/utils/RichContentReceiver.kt +127 -0
  111. package/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt +106 -0
  112. package/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedSpanWatcher.kt +107 -0
  113. package/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedTextWatcher.kt +74 -0
  114. package/android/src/main/new_arch/CMakeLists.txt +62 -0
  115. package/android/src/main/new_arch/GumboNormalizerJni.cpp +14 -0
  116. package/android/src/main/new_arch/ReactNativeEnrichedSpec.cpp +11 -0
  117. package/android/src/main/new_arch/ReactNativeEnrichedSpec.h +15 -0
  118. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputComponentDescriptor.h +35 -0
  119. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputMeasurementManager.cpp +53 -0
  120. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputMeasurementManager.h +25 -0
  121. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputShadowNode.cpp +35 -0
  122. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputShadowNode.h +53 -0
  123. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputState.cpp +9 -0
  124. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputState.h +24 -0
  125. package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h +27 -0
  126. package/android/src/main/res/drawable/broken_image.xml +10 -0
  127. package/cpp/CMakeLists.txt +50 -0
  128. package/cpp/GumboParser/GumboParser.h +34043 -0
  129. package/cpp/README.md +59 -0
  130. package/cpp/parser/GumboNormalizer.c +915 -0
  131. package/cpp/parser/GumboParser.cpp +16 -0
  132. package/cpp/parser/GumboParser.hpp +23 -0
  133. package/cpp/tests/GumboParserTest.cpp +457 -0
  134. package/ios/EnrichedTextInputView.h +53 -0
  135. package/ios/EnrichedTextInputView.mm +2360 -0
  136. package/ios/EnrichedTextInputViewManager.mm +13 -0
  137. package/ios/attributesManager/AttributesManager.h +17 -0
  138. package/ios/attributesManager/AttributesManager.mm +195 -0
  139. package/ios/config/InputConfig.h +104 -0
  140. package/ios/config/InputConfig.mm +664 -0
  141. package/ios/extensions/ColorExtension.h +7 -0
  142. package/ios/extensions/ColorExtension.mm +38 -0
  143. package/ios/extensions/FontExtension.h +11 -0
  144. package/ios/extensions/FontExtension.mm +72 -0
  145. package/ios/extensions/ImageExtension.h +34 -0
  146. package/ios/extensions/ImageExtension.mm +165 -0
  147. package/ios/extensions/LayoutManagerExtension.h +6 -0
  148. package/ios/extensions/LayoutManagerExtension.mm +443 -0
  149. package/ios/extensions/StringExtension.h +15 -0
  150. package/ios/extensions/StringExtension.mm +69 -0
  151. package/ios/generated/ReactNativeEnrichedSpec/ComponentDescriptors.cpp +22 -0
  152. package/ios/generated/ReactNativeEnrichedSpec/ComponentDescriptors.h +24 -0
  153. package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.cpp +434 -0
  154. package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.h +391 -0
  155. package/ios/generated/ReactNativeEnrichedSpec/Props.cpp +173 -0
  156. package/ios/generated/ReactNativeEnrichedSpec/Props.h +833 -0
  157. package/ios/generated/ReactNativeEnrichedSpec/RCTComponentViewHelpers.h +582 -0
  158. package/ios/generated/ReactNativeEnrichedSpec/ShadowNodes.cpp +17 -0
  159. package/ios/generated/ReactNativeEnrichedSpec/ShadowNodes.h +23 -0
  160. package/ios/generated/ReactNativeEnrichedSpec/States.cpp +16 -0
  161. package/ios/generated/ReactNativeEnrichedSpec/States.h +20 -0
  162. package/ios/inputParser/InputParser.h +11 -0
  163. package/ios/inputParser/InputParser.mm +1463 -0
  164. package/ios/inputTextView/InputTextView.h +6 -0
  165. package/ios/inputTextView/InputTextView.mm +285 -0
  166. package/ios/interfaces/AttributeEntry.h +9 -0
  167. package/ios/interfaces/AttributeEntry.mm +4 -0
  168. package/ios/interfaces/BaseStyleProtocol.h +17 -0
  169. package/ios/interfaces/ImageAttachment.h +11 -0
  170. package/ios/interfaces/ImageAttachment.mm +107 -0
  171. package/ios/interfaces/ImageData.h +10 -0
  172. package/ios/interfaces/ImageData.mm +4 -0
  173. package/ios/interfaces/LinkData.h +11 -0
  174. package/ios/interfaces/LinkData.mm +29 -0
  175. package/ios/interfaces/LinkRegexConfig.h +19 -0
  176. package/ios/interfaces/LinkRegexConfig.mm +37 -0
  177. package/ios/interfaces/MediaAttachment.h +23 -0
  178. package/ios/interfaces/MediaAttachment.mm +31 -0
  179. package/ios/interfaces/MentionParams.h +8 -0
  180. package/ios/interfaces/MentionParams.mm +4 -0
  181. package/ios/interfaces/MentionStyleProps.h +13 -0
  182. package/ios/interfaces/MentionStyleProps.mm +63 -0
  183. package/ios/interfaces/StyleBase.h +36 -0
  184. package/ios/interfaces/StyleBase.mm +256 -0
  185. package/ios/interfaces/StyleHeaders.h +102 -0
  186. package/ios/interfaces/StylePair.h +9 -0
  187. package/ios/interfaces/StylePair.mm +4 -0
  188. package/ios/interfaces/StyleTypeEnum.h +26 -0
  189. package/ios/interfaces/TextDecorationLineEnum.h +6 -0
  190. package/ios/interfaces/TextDecorationLineEnum.mm +4 -0
  191. package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +19 -0
  192. package/ios/internals/EnrichedTextInputViewShadowNode.h +44 -0
  193. package/ios/internals/EnrichedTextInputViewShadowNode.mm +103 -0
  194. package/ios/internals/EnrichedTextInputViewState.cpp +10 -0
  195. package/ios/internals/EnrichedTextInputViewState.h +22 -0
  196. package/ios/styles/BlockQuoteStyle.mm +55 -0
  197. package/ios/styles/BoldStyle.mm +37 -0
  198. package/ios/styles/CheckboxListStyle.mm +153 -0
  199. package/ios/styles/CodeBlockStyle.mm +49 -0
  200. package/ios/styles/H1Style.mm +20 -0
  201. package/ios/styles/H2Style.mm +20 -0
  202. package/ios/styles/H3Style.mm +20 -0
  203. package/ios/styles/H4Style.mm +20 -0
  204. package/ios/styles/H5Style.mm +20 -0
  205. package/ios/styles/H6Style.mm +20 -0
  206. package/ios/styles/HeadingStyleBase.mm +65 -0
  207. package/ios/styles/ImageStyle.mm +146 -0
  208. package/ios/styles/InlineCodeStyle.mm +65 -0
  209. package/ios/styles/ItalicStyle.mm +37 -0
  210. package/ios/styles/LinkStyle.mm +532 -0
  211. package/ios/styles/MentionStyle.mm +538 -0
  212. package/ios/styles/OrderedListStyle.mm +86 -0
  213. package/ios/styles/StrikethroughStyle.mm +25 -0
  214. package/ios/styles/UnderlineStyle.mm +24 -0
  215. package/ios/styles/UnorderedListStyle.mm +86 -0
  216. package/ios/utils/CheckboxHitTestUtils.h +10 -0
  217. package/ios/utils/CheckboxHitTestUtils.mm +122 -0
  218. package/ios/utils/DotReplacementUtils.h +10 -0
  219. package/ios/utils/DotReplacementUtils.mm +68 -0
  220. package/ios/utils/KeyboardUtils.h +7 -0
  221. package/ios/utils/KeyboardUtils.mm +31 -0
  222. package/ios/utils/OccurenceUtils.h +44 -0
  223. package/ios/utils/OccurenceUtils.mm +179 -0
  224. package/ios/utils/ParagraphAttributesUtils.h +15 -0
  225. package/ios/utils/ParagraphAttributesUtils.mm +257 -0
  226. package/ios/utils/RangeUtils.h +12 -0
  227. package/ios/utils/RangeUtils.mm +183 -0
  228. package/ios/utils/TextBlockTapGestureRecognizer.h +17 -0
  229. package/ios/utils/TextBlockTapGestureRecognizer.mm +56 -0
  230. package/ios/utils/TextInsertionUtils.h +17 -0
  231. package/ios/utils/TextInsertionUtils.mm +64 -0
  232. package/ios/utils/WordsUtils.h +7 -0
  233. package/ios/utils/WordsUtils.mm +98 -0
  234. package/ios/utils/ZeroWidthSpaceUtils.h +9 -0
  235. package/ios/utils/ZeroWidthSpaceUtils.mm +270 -0
  236. package/lib/module/index.js +4 -0
  237. package/lib/module/index.js.map +1 -0
  238. package/lib/module/native/EnrichedTextInput.js +304 -0
  239. package/lib/module/native/EnrichedTextInput.js.map +1 -0
  240. package/lib/module/package.json +1 -0
  241. package/lib/module/spec/EnrichedTextInputNativeComponent.ts +517 -0
  242. package/lib/module/types.js +4 -0
  243. package/lib/module/types.js.map +1 -0
  244. package/lib/module/utils/EnrichedTextInputDefaultProps.js +12 -0
  245. package/lib/module/utils/EnrichedTextInputDefaultProps.js.map +1 -0
  246. package/lib/module/utils/normalizeHtmlStyle.js +155 -0
  247. package/lib/module/utils/normalizeHtmlStyle.js.map +1 -0
  248. package/lib/module/utils/nullthrows.js +9 -0
  249. package/lib/module/utils/nullthrows.js.map +1 -0
  250. package/lib/module/utils/regexParser.js +46 -0
  251. package/lib/module/utils/regexParser.js.map +1 -0
  252. package/lib/typescript/package.json +1 -0
  253. package/lib/typescript/src/index.d.ts +3 -0
  254. package/lib/typescript/src/index.d.ts.map +1 -0
  255. package/lib/typescript/src/native/EnrichedTextInput.d.ts +3 -0
  256. package/lib/typescript/src/native/EnrichedTextInput.d.ts.map +1 -0
  257. package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts +397 -0
  258. package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts.map +1 -0
  259. package/lib/typescript/src/types.d.ts +447 -0
  260. package/lib/typescript/src/types.d.ts.map +1 -0
  261. package/lib/typescript/src/utils/EnrichedTextInputDefaultProps.d.ts +10 -0
  262. package/lib/typescript/src/utils/EnrichedTextInputDefaultProps.d.ts.map +1 -0
  263. package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts +4 -0
  264. package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts.map +1 -0
  265. package/lib/typescript/src/utils/nullthrows.d.ts +2 -0
  266. package/lib/typescript/src/utils/nullthrows.d.ts.map +1 -0
  267. package/lib/typescript/src/utils/regexParser.d.ts +3 -0
  268. package/lib/typescript/src/utils/regexParser.d.ts.map +1 -0
  269. package/package.json +226 -0
  270. package/react-native.config.js +13 -0
  271. package/src/index.tsx +20 -0
  272. package/src/native/EnrichedTextInput.tsx +370 -0
  273. package/src/spec/EnrichedTextInputNativeComponent.ts +517 -0
  274. package/src/types.ts +499 -0
  275. package/src/utils/EnrichedTextInputDefaultProps.ts +9 -0
  276. package/src/utils/normalizeHtmlStyle.ts +199 -0
  277. package/src/utils/nullthrows.ts +7 -0
  278. package/src/utils/regexParser.ts +56 -0
@@ -0,0 +1,1120 @@
1
+ package com.swmansion.enriched.textinput
2
+
3
+ import android.content.ClipData
4
+ import android.content.ClipboardManager
5
+ import android.content.Context
6
+ import android.graphics.BlendMode
7
+ import android.graphics.BlendModeColorFilter
8
+ import android.graphics.Color
9
+ import android.graphics.Rect
10
+ import android.graphics.text.LineBreaker
11
+ import android.os.Build
12
+ import android.text.Editable
13
+ import android.text.InputType
14
+ import android.text.Spannable
15
+ import android.text.SpannableString
16
+ import android.util.AttributeSet
17
+ import android.util.Log
18
+ import android.util.Patterns
19
+ import android.util.TypedValue
20
+ import android.view.ActionMode
21
+ import android.view.Gravity
22
+ import android.view.KeyEvent
23
+ import android.view.Menu
24
+ import android.view.MenuItem
25
+ import android.view.MotionEvent
26
+ import android.view.inputmethod.EditorInfo
27
+ import android.view.inputmethod.InputConnection
28
+ import android.view.inputmethod.InputMethodManager
29
+ import android.widget.TextView
30
+ import androidx.appcompat.widget.AppCompatEditText
31
+ import androidx.core.view.ViewCompat
32
+ import com.facebook.react.bridge.ReactContext
33
+ import com.facebook.react.bridge.ReadableArray
34
+ import com.facebook.react.bridge.ReadableMap
35
+ import com.facebook.react.common.ReactConstants
36
+ import com.facebook.react.uimanager.PixelUtil
37
+ import com.facebook.react.uimanager.StateWrapper
38
+ import com.facebook.react.uimanager.UIManagerHelper
39
+ import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
40
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
41
+ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
42
+ import com.swmansion.enriched.common.EnrichedConstants
43
+ import com.swmansion.enriched.common.GumboNormalizer
44
+ import com.swmansion.enriched.common.parser.EnrichedParser
45
+ import com.swmansion.enriched.textinput.events.MentionHandler
46
+ import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
47
+ import com.swmansion.enriched.textinput.events.OnInputBlurEvent
48
+ import com.swmansion.enriched.textinput.events.OnInputFocusEvent
49
+ import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
50
+ import com.swmansion.enriched.textinput.events.OnSubmitEditingEvent
51
+ import com.swmansion.enriched.textinput.spans.EnrichedInputH1Span
52
+ import com.swmansion.enriched.textinput.spans.EnrichedInputH2Span
53
+ import com.swmansion.enriched.textinput.spans.EnrichedInputH3Span
54
+ import com.swmansion.enriched.textinput.spans.EnrichedInputH4Span
55
+ import com.swmansion.enriched.textinput.spans.EnrichedInputH5Span
56
+ import com.swmansion.enriched.textinput.spans.EnrichedInputH6Span
57
+ import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan
58
+ import com.swmansion.enriched.textinput.spans.EnrichedInputLinkSpan
59
+ import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
60
+ import com.swmansion.enriched.textinput.spans.EnrichedSpans
61
+ import com.swmansion.enriched.textinput.spans.interfaces.EnrichedInputSpan
62
+ import com.swmansion.enriched.textinput.styles.HtmlStyle
63
+ import com.swmansion.enriched.textinput.styles.InlineStyles
64
+ import com.swmansion.enriched.textinput.styles.ListStyles
65
+ import com.swmansion.enriched.textinput.styles.ParagraphStyles
66
+ import com.swmansion.enriched.textinput.styles.ParametrizedStyles
67
+ import com.swmansion.enriched.textinput.utils.EnrichedEditableFactory
68
+ import com.swmansion.enriched.textinput.utils.EnrichedSelection
69
+ import com.swmansion.enriched.textinput.utils.EnrichedSpanState
70
+ import com.swmansion.enriched.textinput.utils.RichContentReceiver
71
+ import com.swmansion.enriched.textinput.utils.mergeSpannables
72
+ import com.swmansion.enriched.textinput.utils.setCheckboxClickListener
73
+ import com.swmansion.enriched.textinput.utils.zwsCountBefore
74
+ import com.swmansion.enriched.textinput.watchers.EnrichedSpanWatcher
75
+ import com.swmansion.enriched.textinput.watchers.EnrichedTextWatcher
76
+ import java.util.regex.Pattern
77
+ import java.util.regex.PatternSyntaxException
78
+ import kotlin.math.ceil
79
+
80
+ class EnrichedTextInputView :
81
+ AppCompatEditText,
82
+ TextView.OnEditorActionListener {
83
+ var stateWrapper: StateWrapper? = null
84
+ val selection: EnrichedSelection? = EnrichedSelection(this)
85
+ val spanState: EnrichedSpanState? = EnrichedSpanState(this)
86
+ val inlineStyles: InlineStyles? = InlineStyles(this)
87
+ val paragraphStyles: ParagraphStyles? = ParagraphStyles(this)
88
+ val listStyles: ListStyles? = ListStyles(this)
89
+ val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
90
+ var isDuringTransaction: Boolean = false
91
+ var isRemovingMany: Boolean = false
92
+ var scrollEnabled: Boolean = true
93
+
94
+ val mentionHandler: MentionHandler? = MentionHandler(this)
95
+ var htmlStyle: HtmlStyle = HtmlStyle(this, null)
96
+ set(value) {
97
+ if (field != value) {
98
+ val prev = field
99
+ field = value
100
+ reApplyHtmlStyleForSpans(prev, value)
101
+ }
102
+ }
103
+
104
+ var linkRegex: Pattern? = Patterns.WEB_URL
105
+ var spanWatcher: EnrichedSpanWatcher? = null
106
+ var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
107
+
108
+ var shouldEmitHtml: Boolean = false
109
+ var shouldEmitOnChangeText: Boolean = false
110
+ var experimentalSynchronousEvents: Boolean = false
111
+ var useHtmlNormalizer: Boolean = false
112
+
113
+ var fontSize: Float? = null
114
+ private var lineHeight: Float? = null
115
+ var submitBehavior: String? = null
116
+ private var autoFocus = false
117
+ private var typefaceDirty = false
118
+ private var didAttachToWindow = false
119
+ private var detectScrollMovement = false
120
+ private var fontFamily: String? = null
121
+ private var fontStyle: Int = ReactConstants.UNSET
122
+ private var fontWeight: Int = ReactConstants.UNSET
123
+ private var defaultValue: CharSequence? = null
124
+ private var defaultValueDirty: Boolean = false
125
+
126
+ private var inputMethodManager: InputMethodManager? = null
127
+ private val spannableFactory = EnrichedTextInputSpannableFactory()
128
+ private var contextMenuItems: List<Pair<Int, String>> = emptyList()
129
+
130
+ constructor(context: Context) : super(context) {
131
+ prepareComponent()
132
+ }
133
+
134
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
135
+ prepareComponent()
136
+ }
137
+
138
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
139
+ context,
140
+ attrs,
141
+ defStyleAttr,
142
+ ) {
143
+ prepareComponent()
144
+ }
145
+
146
+ override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
147
+ var inputConnection = super.onCreateInputConnection(outAttrs)
148
+
149
+ if (shouldSubmitOnReturn()) {
150
+ // Remove the "No Enter Action" flag if it exists
151
+ outAttrs.imeOptions = outAttrs.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION.inv()
152
+
153
+ // Force the key to be "Done" (or whatever label you set) instead of "Return"
154
+ // This ensures onEditorAction gets called instead of just inserting \n
155
+ if (outAttrs.imeOptions and EditorInfo.IME_MASK_ACTION == EditorInfo.IME_ACTION_UNSPECIFIED) {
156
+ outAttrs.imeOptions = outAttrs.imeOptions or EditorInfo.IME_ACTION_DONE
157
+ }
158
+ }
159
+
160
+ if (inputConnection != null) {
161
+ inputConnection =
162
+ EnrichedTextInputConnectionWrapper(
163
+ inputConnection,
164
+ context as ReactContext,
165
+ this,
166
+ experimentalSynchronousEvents,
167
+ )
168
+ }
169
+
170
+ return inputConnection
171
+ }
172
+
173
+ init {
174
+ inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
175
+ ViewCompat.setOnReceiveContentListener(
176
+ this,
177
+ RichContentReceiver.MIME_TYPES,
178
+ RichContentReceiver(this, context as ReactContext),
179
+ )
180
+ }
181
+
182
+ private fun prepareComponent() {
183
+ isSingleLine = false
184
+ isHorizontalScrollBarEnabled = false
185
+ isVerticalScrollBarEnabled = true
186
+ gravity = Gravity.TOP or Gravity.START
187
+ inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
188
+
189
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
190
+ breakStrategy = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
191
+ }
192
+
193
+ setPadding(0, 0, 0, 0)
194
+ setBackgroundColor(Color.TRANSPARENT)
195
+
196
+ // Ensure that every time new editable is created, it has EnrichedSpanWatcher attached
197
+ val spanWatcher = EnrichedSpanWatcher(this)
198
+ this.spanWatcher = spanWatcher
199
+
200
+ setEditableFactory(EnrichedEditableFactory(spanWatcher))
201
+ addTextChangedListener(EnrichedTextWatcher(this))
202
+
203
+ // Handle checkbox list item clicks
204
+ this.setCheckboxClickListener()
205
+
206
+ setOnEditorActionListener(this)
207
+ setReturnKeyLabel(DEFAULT_IME_ACTION_LABEL)
208
+ }
209
+
210
+ // Similar implementation to: https://github.com/facebook/react-native/blob/c1f5445f4a59d0035389725e47da58eb3d2c267c/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt#L940
211
+ override fun onEditorAction(
212
+ v: TextView?,
213
+ actionId: Int,
214
+ event: KeyEvent?,
215
+ ): Boolean {
216
+ // Check if it's a valid keyboard action (Done, Next, etc.) or the Enter key (IME_NULL)
217
+ val isAction = (actionId and EditorInfo.IME_MASK_ACTION) != 0 || actionId == EditorInfo.IME_NULL
218
+
219
+ if (isAction) {
220
+ val shouldSubmit = shouldSubmitOnReturn()
221
+ val shouldBlur = shouldBlurOnReturn()
222
+
223
+ if (shouldSubmit) {
224
+ emitSubmitEditing()
225
+ }
226
+
227
+ if (shouldBlur) {
228
+ clearFocus()
229
+ }
230
+
231
+ if (shouldSubmit || shouldBlur) {
232
+ return true
233
+ }
234
+ }
235
+
236
+ // Return false to let the system handle default behavior (like inserting \n)
237
+ return false
238
+ }
239
+
240
+ private fun emitSubmitEditing() {
241
+ val context = context as ReactContext
242
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
243
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id)
244
+ dispatcher?.dispatchEvent(
245
+ OnSubmitEditingEvent(
246
+ surfaceId,
247
+ id,
248
+ text,
249
+ experimentalSynchronousEvents,
250
+ ),
251
+ )
252
+ }
253
+
254
+ // https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L295C1-L296C1
255
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
256
+ when (ev.action) {
257
+ MotionEvent.ACTION_DOWN -> {
258
+ detectScrollMovement = true
259
+ // Disallow parent views to intercept touch events, until we can detect if we should be
260
+ // capturing these touches or not.
261
+ this.parent.requestDisallowInterceptTouchEvent(true)
262
+ }
263
+
264
+ MotionEvent.ACTION_MOVE -> {
265
+ if (detectScrollMovement) {
266
+ if (!canScrollVertically(-1) &&
267
+ !canScrollVertically(1) &&
268
+ !canScrollHorizontally(-1) &&
269
+ !canScrollHorizontally(1)
270
+ ) {
271
+ // We cannot scroll, let parent views take care of these touches.
272
+ this.parent.requestDisallowInterceptTouchEvent(false)
273
+ }
274
+ detectScrollMovement = false
275
+ }
276
+ }
277
+ }
278
+
279
+ return super.onTouchEvent(ev)
280
+ }
281
+
282
+ override fun canScrollVertically(direction: Int): Boolean = scrollEnabled
283
+
284
+ override fun canScrollHorizontally(direction: Int): Boolean = scrollEnabled
285
+
286
+ override fun onSelectionChanged(
287
+ selStart: Int,
288
+ selEnd: Int,
289
+ ) {
290
+ super.onSelectionChanged(selStart, selEnd)
291
+ selection?.onSelection(selStart, selEnd)
292
+ }
293
+
294
+ override fun clearFocus() {
295
+ super.clearFocus()
296
+ inputMethodManager?.hideSoftInputFromWindow(windowToken, 0)
297
+ }
298
+
299
+ override fun onFocusChanged(
300
+ focused: Boolean,
301
+ direction: Int,
302
+ previouslyFocusedRect: Rect?,
303
+ ) {
304
+ super.onFocusChanged(focused, direction, previouslyFocusedRect)
305
+ val context = context as ReactContext
306
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
307
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id)
308
+
309
+ if (focused) {
310
+ dispatcher?.dispatchEvent(OnInputFocusEvent(surfaceId, id, experimentalSynchronousEvents))
311
+ } else {
312
+ dispatcher?.dispatchEvent(OnInputBlurEvent(surfaceId, id, experimentalSynchronousEvents))
313
+ }
314
+ }
315
+
316
+ override fun onTextContextMenuItem(id: Int): Boolean {
317
+ when (id) {
318
+ android.R.id.copy -> {
319
+ handleCustomCopy()
320
+ return true
321
+ }
322
+ }
323
+ return super.onTextContextMenuItem(id)
324
+ }
325
+
326
+ private fun handleCustomCopy() {
327
+ val start = selectionStart
328
+ val end = selectionEnd
329
+ val spannable = text as Spannable
330
+
331
+ if (start < end) {
332
+ val selectedText = spannable.subSequence(start, end) as Spannable
333
+ val selectedHtml = EnrichedParser.toHtml(selectedText)
334
+
335
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
336
+ val clip = ClipData.newHtmlText(CLIPBOARD_TAG, selectedText, selectedHtml)
337
+ clipboard.setPrimaryClip(clip)
338
+ }
339
+ }
340
+
341
+ fun handleTextPaste(item: ClipData.Item) {
342
+ val currentText = text as Spannable
343
+ val start = selectionStart.coerceAtLeast(0)
344
+ val end = selectionEnd.coerceAtLeast(0)
345
+ val lengthBefore = currentText.length
346
+
347
+ val pastedSpannable: Spannable =
348
+ when {
349
+ item.htmlText != null -> {
350
+ val parsed = parseText(item.htmlText)
351
+ (parsed as? Spannable) ?: return
352
+ }
353
+
354
+ item.text != null -> {
355
+ SpannableString(item.text.toString())
356
+ }
357
+
358
+ else -> {
359
+ return
360
+ }
361
+ }
362
+
363
+ val finalText = currentText.mergeSpannables(start, end, pastedSpannable)
364
+ setValue(finalText, false)
365
+
366
+ // replacement-safe: oldLength - removed + inserted
367
+ val insertedLength = finalText.length - (lengthBefore - (end - start))
368
+ val pasteEnd = (start + insertedLength).coerceIn(0, finalText.length)
369
+ setSelection(pasteEnd)
370
+
371
+ // Detect links in the newly pasted range
372
+ parametrizedStyles?.detectLinksInRange(finalText, start.coerceAtMost(pasteEnd), pasteEnd)
373
+ }
374
+
375
+ fun requestFocusProgrammatically() {
376
+ requestFocus()
377
+ inputMethodManager?.showSoftInput(this, 0)
378
+ setSelection(selection?.start ?: text?.length ?: 0)
379
+ }
380
+
381
+ private fun normalizeHtmlIfNeeded(text: CharSequence): CharSequence {
382
+ if (!useHtmlNormalizer) return text
383
+ val normalized = GumboNormalizer.normalizeHtml(text.toString()) ?: return text
384
+
385
+ return try {
386
+ val parsed = EnrichedParser.fromHtml(normalized, htmlStyle, spannableFactory)
387
+ parsed.trimEnd('\n')
388
+ } catch (e: Exception) {
389
+ Log.e(TAG, "Error parsing normalized HTML: ${e.message}")
390
+ text
391
+ }
392
+ }
393
+
394
+ private fun parseText(text: CharSequence): CharSequence {
395
+ val isInternalHtml = text.startsWith("<html>") && text.endsWith("</html>")
396
+
397
+ if (isInternalHtml) {
398
+ try {
399
+ val parsed = EnrichedParser.fromHtml(text.toString(), htmlStyle, spannableFactory)
400
+ return parsed.trimEnd('\n')
401
+ } catch (e: Exception) {
402
+ Log.e(TAG, "Error parsing HTML: ${e.message}")
403
+ return normalizeHtmlIfNeeded(text)
404
+ }
405
+ }
406
+
407
+ return normalizeHtmlIfNeeded(text)
408
+ }
409
+
410
+ fun setValue(
411
+ value: CharSequence?,
412
+ shouldParseHtml: Boolean = true,
413
+ ) {
414
+ if (value == null) return
415
+
416
+ runAsATransaction {
417
+ val newText = if (shouldParseHtml) parseText(value) else value
418
+ setText(newText)
419
+ applyLineSpacing()
420
+
421
+ observeAsyncImages()
422
+
423
+ // Scroll to the last line of text
424
+ setSelection(text?.length ?: 0)
425
+ }
426
+ layoutManager.invalidateLayout()
427
+ }
428
+
429
+ fun insertText(
430
+ text: String,
431
+ start: Int,
432
+ end: Int,
433
+ ) {
434
+ val editable = this.text ?: return
435
+ val replaceStart: Int
436
+ val replaceEnd: Int
437
+
438
+ if (start < 0) {
439
+ // Use current selection
440
+ replaceStart = selectionStart
441
+ replaceEnd = selectionEnd
442
+ } else {
443
+ replaceStart = getActualIndex(start)
444
+ replaceEnd = getActualIndex(end)
445
+ }
446
+
447
+ editable.replace(replaceStart, replaceEnd, text)
448
+ setSelection(replaceStart + text.length)
449
+ }
450
+
451
+ fun setCustomSelection(
452
+ visibleStart: Int,
453
+ visibleEnd: Int,
454
+ ) {
455
+ val actualStart = getActualIndex(visibleStart)
456
+ val actualEnd = getActualIndex(visibleEnd)
457
+
458
+ setSelection(actualStart, actualEnd)
459
+ }
460
+
461
+ // Helper: Walks through the string skipping ZWSPs to find the Nth visible character
462
+ private fun getActualIndex(visibleIndex: Int): Int {
463
+ val currentText = text as Spannable
464
+ var currentVisibleCount = 0
465
+ var actualIndex = 0
466
+
467
+ while (actualIndex < currentText.length) {
468
+ if (currentVisibleCount == visibleIndex) {
469
+ return actualIndex
470
+ }
471
+
472
+ // If the current char is not a hidden space, it counts towards our visible index
473
+ if (currentText[actualIndex] != EnrichedConstants.ZWS) {
474
+ currentVisibleCount++
475
+ }
476
+ actualIndex++
477
+ }
478
+
479
+ return actualIndex
480
+ }
481
+
482
+ /**
483
+ * Finds all async images in the current text and sets up listeners
484
+ * to redraw the text layout when they finish downloading.
485
+ */
486
+ private fun observeAsyncImages() {
487
+ val liveText = text ?: return
488
+
489
+ val spans = liveText.getSpans(0, liveText.length, EnrichedInputImageSpan::class.java)
490
+
491
+ for (span in spans) {
492
+ span.observeAsyncDrawableLoaded(liveText)
493
+ }
494
+ }
495
+
496
+ fun setAutoFocus(autoFocus: Boolean) {
497
+ this.autoFocus = autoFocus
498
+ }
499
+
500
+ fun setPlaceholder(placeholder: String?) {
501
+ if (placeholder == null) return
502
+
503
+ hint = placeholder
504
+ }
505
+
506
+ fun setPlaceholderTextColor(colorInt: Int?) {
507
+ if (colorInt == null) return
508
+
509
+ setHintTextColor(colorInt)
510
+ }
511
+
512
+ fun setSelectionColor(colorInt: Int?) {
513
+ if (colorInt == null) return
514
+
515
+ highlightColor = colorInt
516
+ }
517
+
518
+ fun setCursorColor(colorInt: Int?) {
519
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
520
+ val cursorDrawable = textCursorDrawable ?: return
521
+
522
+ if (colorInt != null) {
523
+ cursorDrawable.colorFilter = BlendModeColorFilter(colorInt, BlendMode.SRC_IN)
524
+ } else {
525
+ cursorDrawable.clearColorFilter()
526
+ }
527
+
528
+ textCursorDrawable = cursorDrawable
529
+ }
530
+ }
531
+
532
+ fun setReturnKeyLabel(returnKeyLabel: String?) {
533
+ setImeActionLabel(returnKeyLabel, EditorInfo.IME_ACTION_UNSPECIFIED)
534
+ }
535
+
536
+ fun setColor(colorInt: Int?) {
537
+ if (colorInt == null) {
538
+ setTextColor(Color.BLACK)
539
+ return
540
+ }
541
+
542
+ setTextColor(colorInt)
543
+ }
544
+
545
+ fun setFontSize(size: Float) {
546
+ if (size == 0f) return
547
+
548
+ val sizeInt = ceil(PixelUtil.toPixelFromSP(size))
549
+ fontSize = sizeInt
550
+ setTextSize(TypedValue.COMPLEX_UNIT_PX, sizeInt)
551
+
552
+ // This ensured that newly created spans will take the new font size into account
553
+ htmlStyle.invalidateStyles()
554
+ layoutManager.invalidateLayout()
555
+ forceScrollToSelection()
556
+ }
557
+
558
+ fun setLineHeight(height: Float) {
559
+ lineHeight = if (height == 0f) null else height
560
+ applyLineSpacing()
561
+ layoutManager.invalidateLayout()
562
+ forceScrollToSelection()
563
+ }
564
+
565
+ private fun applyLineSpacing() {
566
+ val spannable = text as? Spannable ?: return
567
+ spannable
568
+ .getSpans(0, spannable.length, EnrichedLineHeightSpan::class.java)
569
+ .forEach { spannable.removeSpan(it) }
570
+
571
+ val lh = lineHeight ?: return
572
+ spannable.setSpan(
573
+ EnrichedLineHeightSpan(lh),
574
+ 0,
575
+ spannable.length,
576
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE,
577
+ )
578
+ }
579
+
580
+ fun setFontFamily(family: String?) {
581
+ if (family != fontFamily) {
582
+ fontFamily = family
583
+ typefaceDirty = true
584
+ }
585
+ }
586
+
587
+ fun setFontWeight(weight: String?) {
588
+ val fontWeight = parseFontWeight(weight)
589
+
590
+ if (fontWeight != fontStyle) {
591
+ this.fontWeight = fontWeight
592
+ typefaceDirty = true
593
+ }
594
+ }
595
+
596
+ fun setFontStyle(style: String?) {
597
+ val fontStyle = parseFontStyle(style)
598
+
599
+ if (fontStyle != this.fontStyle) {
600
+ this.fontStyle = fontStyle
601
+ typefaceDirty = true
602
+ }
603
+ }
604
+
605
+ fun setAutoCapitalize(flagName: String?) {
606
+ val flag =
607
+ when (flagName) {
608
+ "none" -> InputType.TYPE_NULL
609
+ "sentences" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
610
+ "words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
611
+ "characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
612
+ else -> InputType.TYPE_NULL
613
+ }
614
+
615
+ inputType = (
616
+ inputType and
617
+ InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS.inv() and
618
+ InputType.TYPE_TEXT_FLAG_CAP_WORDS.inv() and
619
+ InputType.TYPE_TEXT_FLAG_CAP_SENTENCES.inv()
620
+ ) or if (flag == InputType.TYPE_NULL) 0 else flag
621
+ }
622
+
623
+ fun setLinkRegex(config: ReadableMap?) {
624
+ val patternStr = config?.getString("pattern")
625
+ if (patternStr == null) {
626
+ linkRegex = Patterns.WEB_URL
627
+ return
628
+ }
629
+
630
+ if (config.getBoolean("isDefault")) {
631
+ linkRegex = Patterns.WEB_URL
632
+ return
633
+ }
634
+
635
+ if (config.getBoolean("isDisabled")) {
636
+ linkRegex = null
637
+ return
638
+ }
639
+
640
+ var flags = 0
641
+ if (config.getBoolean("caseInsensitive")) flags = flags or Pattern.CASE_INSENSITIVE
642
+ if (config.getBoolean("dotAll")) flags = flags or Pattern.DOTALL
643
+
644
+ try {
645
+ linkRegex = Pattern.compile("(?s).*?($patternStr).*", flags)
646
+ } catch (_: PatternSyntaxException) {
647
+ Log.w(TAG, "Invalid link regex pattern: $patternStr")
648
+ linkRegex = Patterns.WEB_URL
649
+ }
650
+ }
651
+
652
+ fun setContextMenuItems(items: ReadableArray?) {
653
+ if (items == null) {
654
+ contextMenuItems = emptyList()
655
+ return
656
+ }
657
+
658
+ val result = mutableListOf<Pair<Int, String>>()
659
+ for (i in 0 until items.size()) {
660
+ val item = items.getMap(i) ?: continue
661
+ val text = item.getString("text") ?: continue
662
+ result.add(Pair(i, text))
663
+ }
664
+
665
+ contextMenuItems = result
666
+ }
667
+
668
+ override fun startActionMode(
669
+ callback: ActionMode.Callback?,
670
+ type: Int,
671
+ ): ActionMode? {
672
+ if (contextMenuItems.isEmpty()) {
673
+ return super.startActionMode(callback, type)
674
+ }
675
+
676
+ val wrappedCallback =
677
+ object : ActionMode.Callback2() {
678
+ override fun onCreateActionMode(
679
+ mode: ActionMode,
680
+ menu: Menu,
681
+ ): Boolean {
682
+ val result = callback?.onCreateActionMode(mode, menu) ?: false
683
+ for ((index, text) in contextMenuItems) {
684
+ menu.add(Menu.NONE, CONTEXT_MENU_ITEM_ID + index, Menu.NONE, text)
685
+ }
686
+
687
+ return result
688
+ }
689
+
690
+ override fun onPrepareActionMode(
691
+ mode: ActionMode,
692
+ menu: Menu,
693
+ ) = callback?.onPrepareActionMode(mode, menu) ?: false
694
+
695
+ override fun onActionItemClicked(
696
+ mode: ActionMode,
697
+ menuItem: MenuItem,
698
+ ): Boolean {
699
+ val itemId = menuItem.itemId
700
+ if (itemId < CONTEXT_MENU_ITEM_ID) {
701
+ return callback?.onActionItemClicked(mode, menuItem) ?: false
702
+ }
703
+
704
+ val selStart = selection?.start ?: 0
705
+ val selEnd = selection?.end ?: 0
706
+ val itemText = contextMenuItems.getOrNull(itemId - CONTEXT_MENU_ITEM_ID)?.second ?: return false
707
+ emitContextMenuItemPressEvent(itemText)
708
+ mode.finish()
709
+ post {
710
+ // Ensures selection is not lost after the action mode is finished
711
+ if (selStart in 0..selEnd) {
712
+ setSelection(selStart, selEnd)
713
+ }
714
+ }
715
+ return true
716
+ }
717
+
718
+ override fun onDestroyActionMode(mode: ActionMode) {
719
+ callback?.onDestroyActionMode(mode)
720
+ }
721
+ }
722
+
723
+ return super.startActionMode(wrappedCallback, type)
724
+ }
725
+
726
+ private fun emitContextMenuItemPressEvent(itemText: String) {
727
+ val start = selection?.start ?: return
728
+ val end = selection.end
729
+ val styleState = spanState?.getStyleStatePayload() ?: return
730
+ val currentText = text ?: return
731
+ val selectedText = currentText.subSequence(start, end).toString().replace(EnrichedConstants.ZWS_STRING, "")
732
+
733
+ val visibleStart = start - currentText.zwsCountBefore(start)
734
+ val visibleEnd = end - currentText.zwsCountBefore(end)
735
+
736
+ val reactContext = context as ReactContext
737
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
738
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
739
+ dispatcher?.dispatchEvent(
740
+ OnContextMenuItemPressEvent(
741
+ surfaceId,
742
+ id,
743
+ itemText,
744
+ selectedText,
745
+ visibleStart,
746
+ visibleEnd,
747
+ styleState,
748
+ experimentalSynchronousEvents,
749
+ ),
750
+ )
751
+ }
752
+
753
+ // https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L283C2-L284C1
754
+ // After the text changes inside an EditText, TextView checks if a layout() has been requested.
755
+ // If it has, it will not scroll the text to the end of the new text inserted, but wait for the
756
+ // next layout() to be called. However, we do not perform a layout() after a requestLayout(), so
757
+ // we need to override isLayoutRequested to force EditText to scroll to the end of the new text
758
+ // immediately.
759
+ override fun isLayoutRequested(): Boolean = false
760
+
761
+ fun afterUpdateTransaction() {
762
+ updateTypeface()
763
+ updateDefaultValue()
764
+ }
765
+
766
+ fun setDefaultValue(value: CharSequence?) {
767
+ defaultValue = value
768
+ defaultValueDirty = true
769
+ }
770
+
771
+ fun shouldBlurOnReturn(): Boolean = submitBehavior == "blurAndSubmit"
772
+
773
+ fun shouldSubmitOnReturn(): Boolean = submitBehavior == "submit" || submitBehavior == "blurAndSubmit"
774
+
775
+ private fun updateDefaultValue() {
776
+ if (!defaultValueDirty) return
777
+
778
+ defaultValueDirty = false
779
+ setValue(defaultValue ?: "")
780
+ }
781
+
782
+ private fun updateTypeface() {
783
+ if (!typefaceDirty) return
784
+ typefaceDirty = false
785
+
786
+ val newTypeface = applyStyles(typeface, fontStyle, fontWeight, fontFamily, context.assets)
787
+ typeface = newTypeface
788
+ paint.typeface = newTypeface
789
+
790
+ layoutManager.invalidateLayout()
791
+ }
792
+
793
+ private fun toggleStyle(name: String) {
794
+ when (name) {
795
+ EnrichedSpans.BOLD -> inlineStyles?.toggleStyle(EnrichedSpans.BOLD)
796
+ EnrichedSpans.ITALIC -> inlineStyles?.toggleStyle(EnrichedSpans.ITALIC)
797
+ EnrichedSpans.UNDERLINE -> inlineStyles?.toggleStyle(EnrichedSpans.UNDERLINE)
798
+ EnrichedSpans.STRIKETHROUGH -> inlineStyles?.toggleStyle(EnrichedSpans.STRIKETHROUGH)
799
+ EnrichedSpans.INLINE_CODE -> inlineStyles?.toggleStyle(EnrichedSpans.INLINE_CODE)
800
+ EnrichedSpans.H1 -> paragraphStyles?.toggleStyle(EnrichedSpans.H1)
801
+ EnrichedSpans.H2 -> paragraphStyles?.toggleStyle(EnrichedSpans.H2)
802
+ EnrichedSpans.H3 -> paragraphStyles?.toggleStyle(EnrichedSpans.H3)
803
+ EnrichedSpans.H4 -> paragraphStyles?.toggleStyle(EnrichedSpans.H4)
804
+ EnrichedSpans.H5 -> paragraphStyles?.toggleStyle(EnrichedSpans.H5)
805
+ EnrichedSpans.H6 -> paragraphStyles?.toggleStyle(EnrichedSpans.H6)
806
+ EnrichedSpans.CODE_BLOCK -> paragraphStyles?.toggleStyle(EnrichedSpans.CODE_BLOCK)
807
+ EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.toggleStyle(EnrichedSpans.BLOCK_QUOTE)
808
+ EnrichedSpans.ORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.ORDERED_LIST)
809
+ EnrichedSpans.UNORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.UNORDERED_LIST)
810
+ EnrichedSpans.CHECKBOX_LIST -> listStyles?.toggleStyle(EnrichedSpans.CHECKBOX_LIST)
811
+ else -> Log.w(TAG, "Unknown style: $name")
812
+ }
813
+
814
+ layoutManager.invalidateLayout()
815
+ }
816
+
817
+ private fun removeStyle(
818
+ name: String,
819
+ start: Int,
820
+ end: Int,
821
+ ): Boolean {
822
+ val removed =
823
+ when (name) {
824
+ EnrichedSpans.BOLD -> inlineStyles?.removeStyle(EnrichedSpans.BOLD, start, end)
825
+ EnrichedSpans.ITALIC -> inlineStyles?.removeStyle(EnrichedSpans.ITALIC, start, end)
826
+ EnrichedSpans.UNDERLINE -> inlineStyles?.removeStyle(EnrichedSpans.UNDERLINE, start, end)
827
+ EnrichedSpans.STRIKETHROUGH -> inlineStyles?.removeStyle(EnrichedSpans.STRIKETHROUGH, start, end)
828
+ EnrichedSpans.INLINE_CODE -> inlineStyles?.removeStyle(EnrichedSpans.INLINE_CODE, start, end)
829
+ EnrichedSpans.H1 -> paragraphStyles?.removeStyle(EnrichedSpans.H1, start, end)
830
+ EnrichedSpans.H2 -> paragraphStyles?.removeStyle(EnrichedSpans.H2, start, end)
831
+ EnrichedSpans.H3 -> paragraphStyles?.removeStyle(EnrichedSpans.H3, start, end)
832
+ EnrichedSpans.H4 -> paragraphStyles?.removeStyle(EnrichedSpans.H4, start, end)
833
+ EnrichedSpans.H5 -> paragraphStyles?.removeStyle(EnrichedSpans.H5, start, end)
834
+ EnrichedSpans.H6 -> paragraphStyles?.removeStyle(EnrichedSpans.H6, start, end)
835
+ EnrichedSpans.CODE_BLOCK -> paragraphStyles?.removeStyle(EnrichedSpans.CODE_BLOCK, start, end)
836
+ EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.removeStyle(EnrichedSpans.BLOCK_QUOTE, start, end)
837
+ EnrichedSpans.ORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.ORDERED_LIST, start, end)
838
+ EnrichedSpans.UNORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.UNORDERED_LIST, start, end)
839
+ EnrichedSpans.CHECKBOX_LIST -> listStyles?.removeStyle(EnrichedSpans.CHECKBOX_LIST, start, end)
840
+ EnrichedSpans.LINK -> parametrizedStyles?.removeStyle(EnrichedSpans.LINK, start, end)
841
+ EnrichedSpans.IMAGE -> parametrizedStyles?.removeStyle(EnrichedSpans.IMAGE, start, end)
842
+ EnrichedSpans.MENTION -> parametrizedStyles?.removeStyle(EnrichedSpans.MENTION, start, end)
843
+ else -> false
844
+ }
845
+
846
+ return removed == true
847
+ }
848
+
849
+ private fun getTargetRange(name: String): Pair<Int, Int> {
850
+ val result =
851
+ when (name) {
852
+ EnrichedSpans.BOLD -> inlineStyles?.getStyleRange()
853
+ EnrichedSpans.ITALIC -> inlineStyles?.getStyleRange()
854
+ EnrichedSpans.UNDERLINE -> inlineStyles?.getStyleRange()
855
+ EnrichedSpans.STRIKETHROUGH -> inlineStyles?.getStyleRange()
856
+ EnrichedSpans.INLINE_CODE -> inlineStyles?.getStyleRange()
857
+ EnrichedSpans.H1 -> paragraphStyles?.getStyleRange()
858
+ EnrichedSpans.H2 -> paragraphStyles?.getStyleRange()
859
+ EnrichedSpans.H3 -> paragraphStyles?.getStyleRange()
860
+ EnrichedSpans.H4 -> paragraphStyles?.getStyleRange()
861
+ EnrichedSpans.H5 -> paragraphStyles?.getStyleRange()
862
+ EnrichedSpans.H6 -> paragraphStyles?.getStyleRange()
863
+ EnrichedSpans.CODE_BLOCK -> paragraphStyles?.getStyleRange()
864
+ EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.getStyleRange()
865
+ EnrichedSpans.ORDERED_LIST -> listStyles?.getStyleRange()
866
+ EnrichedSpans.UNORDERED_LIST -> listStyles?.getStyleRange()
867
+ EnrichedSpans.CHECKBOX_LIST -> listStyles?.getStyleRange()
868
+ EnrichedSpans.LINK -> parametrizedStyles?.getStyleRange()
869
+ EnrichedSpans.IMAGE -> parametrizedStyles?.getStyleRange()
870
+ EnrichedSpans.MENTION -> parametrizedStyles?.getStyleRange()
871
+ else -> Pair(0, 0)
872
+ }
873
+
874
+ return result ?: Pair(0, 0)
875
+ }
876
+
877
+ private fun verifyStyle(name: String): Boolean {
878
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(name, htmlStyle) ?: return true
879
+ val conflictingStyles = mergingConfig.conflictingStyles
880
+ val blockingStyles = mergingConfig.blockingStyles
881
+ val isEnabling = spanState?.getStart(name) == null
882
+ if (!isEnabling) return true
883
+
884
+ for (style in blockingStyles) {
885
+ if (spanState?.getStart(style) != null) {
886
+ spanState.setStart(name, null)
887
+ return false
888
+ }
889
+ }
890
+
891
+ for (style in conflictingStyles) {
892
+ val start = selection?.start ?: 0
893
+ val end = selection?.end ?: 0
894
+ val lengthBefore = text?.length ?: 0
895
+
896
+ runAsATransaction {
897
+ val targetRange = getTargetRange(name)
898
+ val removed = removeStyle(style, targetRange.first, targetRange.second)
899
+ if (removed) {
900
+ spanState?.setStart(style, null)
901
+ }
902
+ }
903
+
904
+ val lengthAfter = text?.length ?: 0
905
+ val charactersRemoved = lengthBefore - lengthAfter
906
+ val finalEnd =
907
+ if (charactersRemoved > 0) {
908
+ (end - charactersRemoved).coerceAtLeast(0)
909
+ } else {
910
+ end
911
+ }
912
+
913
+ val finalStart = start.coerceAtLeast(0).coerceAtMost(finalEnd)
914
+ selection?.onSelection(finalStart, finalEnd)
915
+ }
916
+
917
+ return true
918
+ }
919
+
920
+ fun verifyAndToggleStyle(name: String) {
921
+ val isValid = verifyStyle(name)
922
+ if (!isValid) return
923
+
924
+ toggleStyle(name)
925
+ }
926
+
927
+ fun toggleCheckboxListItem(checked: Boolean) {
928
+ val isValid = verifyStyle(EnrichedSpans.CHECKBOX_LIST)
929
+ if (!isValid) return
930
+
931
+ listStyles?.toggleCheckboxListStyle(checked)
932
+ }
933
+
934
+ fun addLink(
935
+ start: Int,
936
+ end: Int,
937
+ text: String,
938
+ url: String,
939
+ ) {
940
+ val isValid = verifyStyle(EnrichedSpans.LINK)
941
+ if (!isValid) return
942
+
943
+ parametrizedStyles?.setLinkSpan(getActualIndex(start), getActualIndex(end), text, url)
944
+ }
945
+
946
+ fun removeLink(
947
+ start: Int,
948
+ end: Int,
949
+ ) {
950
+ parametrizedStyles?.removeLinkSpans(getActualIndex(start), getActualIndex(end))
951
+ }
952
+
953
+ fun addImage(
954
+ src: String,
955
+ width: Float,
956
+ height: Float,
957
+ ) {
958
+ val isValid = verifyStyle(EnrichedSpans.IMAGE)
959
+ if (!isValid) return
960
+
961
+ parametrizedStyles?.setImageSpan(src, width, height)
962
+ layoutManager.invalidateLayout()
963
+ }
964
+
965
+ fun startMention(indicator: String) {
966
+ val isValid = verifyStyle(EnrichedSpans.MENTION)
967
+ if (!isValid) return
968
+
969
+ parametrizedStyles?.startMention(indicator)
970
+ }
971
+
972
+ fun addMention(
973
+ indicator: String,
974
+ text: String,
975
+ attributes: Map<String, String>,
976
+ ) {
977
+ val isValid = verifyStyle(EnrichedSpans.MENTION)
978
+ if (!isValid) return
979
+
980
+ parametrizedStyles?.setMentionSpan(text, indicator, attributes)
981
+ }
982
+
983
+ fun requestHTML(requestId: Int) {
984
+ val html =
985
+ try {
986
+ EnrichedParser.toHtmlWithDefault(text)
987
+ } catch (_: Exception) {
988
+ null
989
+ }
990
+
991
+ val reactContext = context as ReactContext
992
+ val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
993
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
994
+ dispatcher?.dispatchEvent(OnRequestHtmlResultEvent(surfaceId, id, requestId, html, experimentalSynchronousEvents))
995
+ }
996
+
997
+ // Sometimes setting up style triggers many changes in sequence
998
+ // Eg. removing conflicting styles -> changing text -> applying spans
999
+ // In such scenario we want to prevent from handling side effects (eg. onTextChanged)
1000
+ fun runAsATransaction(block: () -> Unit) {
1001
+ try {
1002
+ isDuringTransaction = true
1003
+ block()
1004
+ } finally {
1005
+ isDuringTransaction = false
1006
+ }
1007
+ }
1008
+
1009
+ private fun forceScrollToSelection() {
1010
+ val textLayout = layout ?: return
1011
+ val cursorOffset = selectionStart
1012
+ if (cursorOffset <= 0) return
1013
+
1014
+ val selectedLineIndex = textLayout.getLineForOffset(cursorOffset)
1015
+ val selectedLineTop = textLayout.getLineTop(selectedLineIndex)
1016
+ val selectedLineBottom = textLayout.getLineBottom(selectedLineIndex)
1017
+ val visibleTextHeight = height - paddingTop - paddingBottom
1018
+
1019
+ if (visibleTextHeight <= 0) return
1020
+
1021
+ val visibleTop = scrollY
1022
+ val visibleBottom = scrollY + visibleTextHeight
1023
+ var targetScrollY = scrollY
1024
+
1025
+ if (selectedLineTop < visibleTop) {
1026
+ targetScrollY = selectedLineTop
1027
+ } else if (selectedLineBottom > visibleBottom) {
1028
+ targetScrollY = selectedLineBottom - visibleTextHeight
1029
+ }
1030
+
1031
+ val maxScrollY = (textLayout.height - visibleTextHeight).coerceAtLeast(0)
1032
+ targetScrollY = targetScrollY.coerceIn(0, maxScrollY)
1033
+ scrollTo(scrollX, targetScrollY)
1034
+ }
1035
+
1036
+ private fun isHeadingBold(
1037
+ style: HtmlStyle,
1038
+ span: EnrichedInputSpan,
1039
+ ): Boolean =
1040
+ when (span) {
1041
+ is EnrichedInputH1Span -> style.h1Bold
1042
+ is EnrichedInputH2Span -> style.h2Bold
1043
+ is EnrichedInputH3Span -> style.h3Bold
1044
+ is EnrichedInputH4Span -> style.h4Bold
1045
+ is EnrichedInputH5Span -> style.h5Bold
1046
+ is EnrichedInputH6Span -> style.h6Bold
1047
+ else -> false
1048
+ }
1049
+
1050
+ private fun shouldRemoveBoldFromHeading(
1051
+ span: EnrichedInputSpan,
1052
+ prevStyle: HtmlStyle,
1053
+ nextStyle: HtmlStyle,
1054
+ ): Boolean {
1055
+ val wasBold = isHeadingBold(prevStyle, span)
1056
+ val isNowBold = isHeadingBold(nextStyle, span)
1057
+
1058
+ return !wasBold && isNowBold
1059
+ }
1060
+
1061
+ private fun reApplyHtmlStyleForSpans(
1062
+ previousHtmlStyle: HtmlStyle,
1063
+ nextHtmlStyle: HtmlStyle,
1064
+ ) {
1065
+ val spannable = text as? Spannable ?: return
1066
+ if (spannable.isEmpty()) return
1067
+
1068
+ var shouldEmitStateChange = false
1069
+
1070
+ runAsATransaction {
1071
+ val spans = spannable.getSpans(0, spannable.length, EnrichedInputSpan::class.java)
1072
+ for (span in spans) {
1073
+ if (!span.dependsOnHtmlStyle) continue
1074
+
1075
+ val start = spannable.getSpanStart(span)
1076
+ val end = spannable.getSpanEnd(span)
1077
+ val flags = spannable.getSpanFlags(span)
1078
+
1079
+ if (start == -1 || end == -1) continue
1080
+
1081
+ // Check if we need to remove explicit bold spans
1082
+ if (shouldRemoveBoldFromHeading(span, previousHtmlStyle, nextHtmlStyle)) {
1083
+ val isRemoved = removeStyle(EnrichedSpans.BOLD, start, end)
1084
+ if (isRemoved) shouldEmitStateChange = true
1085
+ }
1086
+
1087
+ spannable.removeSpan(span)
1088
+ val newSpan = span.rebuildWithStyle(htmlStyle)
1089
+ spannable.setSpan(newSpan, start, end, flags)
1090
+ }
1091
+
1092
+ if (shouldEmitStateChange) {
1093
+ selection?.validateStyles()
1094
+ }
1095
+ }
1096
+ layoutManager.invalidateLayout()
1097
+ forceScrollToSelection()
1098
+ }
1099
+
1100
+ override fun onAttachedToWindow() {
1101
+ super.onAttachedToWindow()
1102
+
1103
+ // https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L946
1104
+ // setTextIsSelectable internally calls setText(), which fires afterTextChanged that should be marked as a transaction to avoid unwanted side effects
1105
+ runAsATransaction { super.setTextIsSelectable(true) }
1106
+
1107
+ if (autoFocus && !didAttachToWindow) {
1108
+ requestFocusProgrammatically()
1109
+ }
1110
+
1111
+ didAttachToWindow = true
1112
+ }
1113
+
1114
+ companion object {
1115
+ const val TAG = "EnrichedTextInputView"
1116
+ const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
1117
+ private const val CONTEXT_MENU_ITEM_ID = 10000
1118
+ const val DEFAULT_IME_ACTION_LABEL = "DONE"
1119
+ }
1120
+ }