react-native-enriched 0.0.0 → 0.1.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 (169) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +875 -0
  3. package/ReactNativeEnriched.podspec +27 -0
  4. package/android/build.gradle +101 -0
  5. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +146 -0
  6. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +55 -0
  7. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ComponentDescriptors.cpp +22 -0
  8. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ComponentDescriptors.h +24 -0
  9. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.cpp +118 -0
  10. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.h +95 -0
  11. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +128 -0
  12. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +577 -0
  13. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ShadowNodes.cpp +17 -0
  14. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/ShadowNodes.h +23 -0
  15. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/States.cpp +16 -0
  16. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/States.h +20 -0
  17. package/android/gradle.properties +5 -0
  18. package/android/src/main/AndroidManifest.xml +3 -0
  19. package/android/src/main/AndroidManifestNew.xml +2 -0
  20. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +535 -0
  21. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +64 -0
  22. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +292 -0
  23. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +19 -0
  24. package/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +40 -0
  25. package/android/src/main/java/com/swmansion/enriched/events/OnChangeHtmlEvent.kt +28 -0
  26. package/android/src/main/java/com/swmansion/enriched/events/OnChangeSelectionEvent.kt +29 -0
  27. package/android/src/main/java/com/swmansion/enriched/events/OnChangeStateEvent.kt +24 -0
  28. package/android/src/main/java/com/swmansion/enriched/events/OnChangeTextEvent.kt +30 -0
  29. package/android/src/main/java/com/swmansion/enriched/events/OnInputBlurEvent.kt +27 -0
  30. package/android/src/main/java/com/swmansion/enriched/events/OnInputFocusEvent.kt +27 -0
  31. package/android/src/main/java/com/swmansion/enriched/events/OnLinkDetectedEvent.kt +30 -0
  32. package/android/src/main/java/com/swmansion/enriched/events/OnMentionDetectedEvent.kt +29 -0
  33. package/android/src/main/java/com/swmansion/enriched/events/OnMentionEvent.kt +33 -0
  34. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +34 -0
  35. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBoldSpan.kt +10 -0
  36. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +38 -0
  37. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH1Span.kt +17 -0
  38. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH2Span.kt +17 -0
  39. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH3Span.kt +17 -0
  40. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +41 -0
  41. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +16 -0
  42. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedItalicSpan.kt +10 -0
  43. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedLinkSpan.kt +24 -0
  44. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedMentionSpan.kt +36 -0
  45. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +71 -0
  46. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +111 -0
  47. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedStrikeThroughSpan.kt +9 -0
  48. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnderlineSpan.kt +9 -0
  49. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +49 -0
  50. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedBlockSpan.kt +4 -0
  51. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedHeadingSpan.kt +4 -0
  52. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedInlineSpan.kt +4 -0
  53. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedParagraphSpan.kt +4 -0
  54. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedSpan.kt +4 -0
  55. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedZeroWidthSpaceSpan.kt +5 -0
  56. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +227 -0
  57. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +146 -0
  58. package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +173 -0
  59. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +186 -0
  60. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +223 -0
  61. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +857 -0
  62. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt +285 -0
  63. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt +204 -0
  64. package/android/src/main/java/com/swmansion/enriched/utils/Utils.kt +91 -0
  65. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +73 -0
  66. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +51 -0
  67. package/android/src/main/new_arch/CMakeLists.txt +56 -0
  68. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.cpp +22 -0
  69. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.h +26 -0
  70. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputComponentDescriptor.h +35 -0
  71. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +51 -0
  72. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +26 -0
  73. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +34 -0
  74. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.h +54 -0
  75. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.cpp +9 -0
  76. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.h +25 -0
  77. package/ios/EnrichedTextInputView.h +33 -0
  78. package/ios/EnrichedTextInputView.mm +1190 -0
  79. package/ios/EnrichedTextInputViewManager.mm +13 -0
  80. package/ios/config/InputConfig.h +67 -0
  81. package/ios/config/InputConfig.mm +382 -0
  82. package/ios/generated/RNEnrichedTextInputViewSpec/ComponentDescriptors.cpp +22 -0
  83. package/ios/generated/RNEnrichedTextInputViewSpec/ComponentDescriptors.h +24 -0
  84. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.cpp +118 -0
  85. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.h +95 -0
  86. package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +128 -0
  87. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +577 -0
  88. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +384 -0
  89. package/ios/generated/RNEnrichedTextInputViewSpec/ShadowNodes.cpp +17 -0
  90. package/ios/generated/RNEnrichedTextInputViewSpec/ShadowNodes.h +23 -0
  91. package/ios/generated/RNEnrichedTextInputViewSpec/States.cpp +16 -0
  92. package/ios/generated/RNEnrichedTextInputViewSpec/States.h +20 -0
  93. package/ios/inputParser/InputParser.h +11 -0
  94. package/ios/inputParser/InputParser.mm +659 -0
  95. package/ios/inputTextView/InputTextView.h +6 -0
  96. package/ios/inputTextView/InputTextView.mm +115 -0
  97. package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +17 -0
  98. package/ios/internals/EnrichedTextInputViewShadowNode.h +40 -0
  99. package/ios/internals/EnrichedTextInputViewShadowNode.mm +83 -0
  100. package/ios/internals/EnrichedTextInputViewState.cpp +10 -0
  101. package/ios/internals/EnrichedTextInputViewState.h +20 -0
  102. package/ios/styles/BlockQuoteStyle.mm +248 -0
  103. package/ios/styles/BoldStyle.mm +122 -0
  104. package/ios/styles/H1Style.mm +10 -0
  105. package/ios/styles/H2Style.mm +10 -0
  106. package/ios/styles/H3Style.mm +10 -0
  107. package/ios/styles/HeadingStyleBase.mm +144 -0
  108. package/ios/styles/InlineCodeStyle.mm +163 -0
  109. package/ios/styles/ItalicStyle.mm +110 -0
  110. package/ios/styles/LinkStyle.mm +463 -0
  111. package/ios/styles/MentionStyle.mm +476 -0
  112. package/ios/styles/OrderedListStyle.mm +225 -0
  113. package/ios/styles/StrikethroughStyle.mm +80 -0
  114. package/ios/styles/UnderlineStyle.mm +112 -0
  115. package/ios/styles/UnorderedListStyle.mm +225 -0
  116. package/ios/utils/BaseStyleProtocol.h +16 -0
  117. package/ios/utils/ColorExtension.h +6 -0
  118. package/ios/utils/ColorExtension.mm +27 -0
  119. package/ios/utils/FontExtension.h +13 -0
  120. package/ios/utils/FontExtension.mm +91 -0
  121. package/ios/utils/LayoutManagerExtension.h +6 -0
  122. package/ios/utils/LayoutManagerExtension.mm +171 -0
  123. package/ios/utils/LinkData.h +9 -0
  124. package/ios/utils/LinkData.mm +4 -0
  125. package/ios/utils/MentionParams.h +9 -0
  126. package/ios/utils/MentionParams.mm +4 -0
  127. package/ios/utils/MentionStyleProps.h +13 -0
  128. package/ios/utils/MentionStyleProps.mm +56 -0
  129. package/ios/utils/OccurenceUtils.h +37 -0
  130. package/ios/utils/OccurenceUtils.mm +124 -0
  131. package/ios/utils/ParagraphsUtils.h +7 -0
  132. package/ios/utils/ParagraphsUtils.mm +54 -0
  133. package/ios/utils/StringExtension.h +15 -0
  134. package/ios/utils/StringExtension.mm +57 -0
  135. package/ios/utils/StyleHeaders.h +74 -0
  136. package/ios/utils/StylePair.h +9 -0
  137. package/ios/utils/StylePair.mm +4 -0
  138. package/ios/utils/StyleTypeEnum.h +22 -0
  139. package/ios/utils/TextDecorationLineEnum.h +6 -0
  140. package/ios/utils/TextDecorationLineEnum.mm +4 -0
  141. package/ios/utils/TextInsertionUtils.h +6 -0
  142. package/ios/utils/TextInsertionUtils.mm +48 -0
  143. package/ios/utils/WordsUtils.h +6 -0
  144. package/ios/utils/WordsUtils.mm +88 -0
  145. package/ios/utils/ZeroWidthSpaceUtils.h +7 -0
  146. package/ios/utils/ZeroWidthSpaceUtils.mm +164 -0
  147. package/lib/module/EnrichedTextInput.js +191 -0
  148. package/lib/module/EnrichedTextInput.js.map +1 -0
  149. package/lib/module/EnrichedTextInputNativeComponent.ts +235 -0
  150. package/lib/module/index.js +4 -0
  151. package/lib/module/index.js.map +1 -0
  152. package/lib/module/normalizeHtmlStyle.js +141 -0
  153. package/lib/module/normalizeHtmlStyle.js.map +1 -0
  154. package/lib/module/package.json +1 -0
  155. package/lib/typescript/package.json +1 -0
  156. package/lib/typescript/src/EnrichedTextInput.d.ts +113 -0
  157. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -0
  158. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +160 -0
  159. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -0
  160. package/lib/typescript/src/index.d.ts +3 -0
  161. package/lib/typescript/src/index.d.ts.map +1 -0
  162. package/lib/typescript/src/normalizeHtmlStyle.d.ts +4 -0
  163. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -0
  164. package/package.json +172 -1
  165. package/react-native.config.js +13 -0
  166. package/src/EnrichedTextInput.tsx +358 -0
  167. package/src/EnrichedTextInputNativeComponent.ts +235 -0
  168. package/src/index.tsx +9 -0
  169. package/src/normalizeHtmlStyle.ts +188 -0
@@ -0,0 +1,285 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import android.text.Editable
4
+ import android.text.Spannable
5
+ import com.facebook.react.bridge.ReactContext
6
+ import com.facebook.react.uimanager.UIManagerHelper
7
+ import com.swmansion.enriched.EnrichedTextInputView
8
+ import com.swmansion.enriched.events.OnChangeSelectionEvent
9
+ import com.swmansion.enriched.events.OnLinkDetectedEvent
10
+ import com.swmansion.enriched.events.OnMentionDetectedEvent
11
+ import com.swmansion.enriched.spans.EnrichedLinkSpan
12
+ import com.swmansion.enriched.spans.EnrichedMentionSpan
13
+ import com.swmansion.enriched.spans.EnrichedSpans
14
+ import org.json.JSONObject
15
+
16
+ class EnrichedSelection(private val view: EnrichedTextInputView) {
17
+ var start: Int = 0
18
+ var end: Int = 0
19
+
20
+ private var previousLinkDetectedEvent: MutableMap<String, String> = mutableMapOf("text" to "", "url" to "")
21
+ private var previousMentionDetectedEvent: MutableMap<String, String> = mutableMapOf("text" to "", "payload" to "")
22
+
23
+ fun onSelection(selStart: Int, selEnd: Int) {
24
+ var shouldValidateStyles = false
25
+ var newStart = start
26
+ var newEnd = end
27
+
28
+ if (selStart != -1 && selStart != newStart) {
29
+ newStart = selStart
30
+ shouldValidateStyles = true
31
+ }
32
+
33
+ if (selEnd != -1 && selEnd != newEnd) {
34
+ newEnd = selEnd
35
+ shouldValidateStyles = true
36
+ }
37
+
38
+ val textLength = view.text?.length ?: 0
39
+ val finalStart = newStart.coerceAtMost(newEnd).coerceAtLeast(0).coerceAtMost(textLength)
40
+ val finalEnd = newEnd.coerceAtLeast(newStart).coerceAtLeast(0).coerceAtMost(textLength)
41
+
42
+ if (isZeroWidthSelection(finalStart, finalEnd) && !view.isSettingValue) {
43
+ view.setSelection(finalStart + 1)
44
+ shouldValidateStyles = false
45
+ }
46
+
47
+ if (!shouldValidateStyles) return
48
+
49
+ start = finalStart
50
+ end = finalEnd
51
+ validateStyles()
52
+ emitSelectionChangeEvent(view.text, finalStart, finalEnd)
53
+ }
54
+
55
+ private fun isZeroWidthSelection(start: Int, end: Int): Boolean {
56
+ val text = view.text ?: return false
57
+
58
+ if (start != end) {
59
+ return text.substring(start, end) == "\u200B"
60
+ }
61
+
62
+ val isNewLine = if (start > 0 ) text.substring(start - 1, start) == "\n" else true
63
+ val isNextCharacterZeroWidth = if (start < text.length) {
64
+ text.substring(start, start + 1) == "\u200B"
65
+ } else {
66
+ false
67
+ }
68
+
69
+ return isNewLine && isNextCharacterZeroWidth
70
+ }
71
+
72
+ fun validateStyles() {
73
+ val state = view.spanState ?: return
74
+
75
+ // We don't validate inline styles when removing many characters at once
76
+ // We don't want to remove styles on auto-correction
77
+ // If user removes many characters at once, we want to keep the styles config
78
+ if (!view.isRemovingMany) {
79
+ for ((style, config) in EnrichedSpans.inlineSpans) {
80
+ state.setStart(style, getInlineStyleStart(config.clazz))
81
+ }
82
+ } else {
83
+ view.isRemovingMany = false
84
+ }
85
+
86
+ for ((style, config) in EnrichedSpans.paragraphSpans) {
87
+ state.setStart(style, getParagraphStyleStart(config.clazz))
88
+ }
89
+
90
+ for ((style, config) in EnrichedSpans.listSpans) {
91
+ state.setStart(style, getListStyleStart(config.clazz))
92
+ }
93
+
94
+ for ((style, config) in EnrichedSpans.parametrizedStyles) {
95
+ state.setStart(style, getParametrizedStyleStart(config.clazz))
96
+ }
97
+ }
98
+
99
+ fun getInlineSelection(): Pair<Int, Int> {
100
+ val finalStart = start.coerceAtMost(end).coerceAtLeast(0)
101
+ val finalEnd = end.coerceAtLeast(start).coerceAtLeast(0)
102
+
103
+ return Pair(finalStart, finalEnd)
104
+ }
105
+
106
+ private fun <T>getInlineStyleStart(type: Class<T>): Int? {
107
+ val (start, end) = getInlineSelection()
108
+ val spannable = view.text as Spannable
109
+ val spans = spannable.getSpans(start, end, type)
110
+ var styleStart: Int? = null
111
+
112
+ for (span in spans) {
113
+ val spanStart = spannable.getSpanStart(span)
114
+ val spanEnd = spannable.getSpanEnd(span)
115
+
116
+ if (start == end && start == spanStart) {
117
+ styleStart = null
118
+ } else if (start >= spanStart && end <= spanEnd) {
119
+ styleStart = spanStart
120
+ }
121
+ }
122
+
123
+ return styleStart
124
+ }
125
+
126
+ fun getParagraphSelection(): Pair<Int, Int> {
127
+ val (currentStart, currentEnd) = getInlineSelection()
128
+ val spannable = view.text as Spannable
129
+ return spannable.getParagraphBounds(currentStart, currentEnd)
130
+ }
131
+
132
+ private fun <T>getParagraphStyleStart(type: Class<T>): Int? {
133
+ val (start, end) = getParagraphSelection()
134
+ val spannable = view.text as Spannable
135
+ val spans = spannable.getSpans(start, end, type)
136
+ var styleStart: Int? = null
137
+
138
+ for (span in spans) {
139
+ val spanStart = spannable.getSpanStart(span)
140
+ val spanEnd = spannable.getSpanEnd(span)
141
+
142
+ if (start >= spanStart && end <= spanEnd) {
143
+ styleStart = spanStart
144
+ break
145
+ }
146
+ }
147
+
148
+ return styleStart
149
+ }
150
+
151
+ private fun <T>getListStyleStart(type: Class<T>): Int? {
152
+ val (start, end) = getParagraphSelection()
153
+ val spannable = view.text as Spannable
154
+ var styleStart: Int? = null
155
+
156
+ var paragraphStart = start
157
+ val paragraphs = spannable.substring(start, end).split("\n")
158
+ pi@ for (paragraph in paragraphs) {
159
+ val paragraphEnd = paragraphStart + paragraph.length
160
+ val spans = spannable.getSpans(paragraphStart, paragraphEnd, type)
161
+
162
+ for (span in spans) {
163
+ val spanStart = spannable.getSpanStart(span)
164
+ val spanEnd = spannable.getSpanEnd(span)
165
+
166
+ if (spanStart == paragraphStart && spanEnd == paragraphEnd) {
167
+ styleStart = spanStart
168
+ paragraphStart = paragraphEnd + 1
169
+ continue@pi
170
+ }
171
+ }
172
+
173
+ styleStart = null
174
+ break
175
+ }
176
+
177
+ return styleStart
178
+ }
179
+
180
+ private fun <T>getParametrizedStyleStart(type: Class<T>): Int? {
181
+ val (start, end) = getInlineSelection()
182
+ val spannable = view.text as Spannable
183
+ val spans = spannable.getSpans(start, end, type)
184
+ val isLinkType = type == EnrichedLinkSpan::class.java
185
+ val isMentionType = type == EnrichedMentionSpan::class.java
186
+
187
+ if (isLinkType && spans.isEmpty()) {
188
+ emitLinkDetectedEvent(spannable, null, start, end)
189
+ return null
190
+ }
191
+
192
+ if (isMentionType && spans.isEmpty()) {
193
+ emitMentionDetectedEvent(spannable, null, start, end)
194
+ return null
195
+ }
196
+
197
+ for (span in spans) {
198
+ val spanStart = spannable.getSpanStart(span)
199
+ val spanEnd = spannable.getSpanEnd(span)
200
+
201
+ if (start >= spanStart && end <= spanEnd) {
202
+ if (isLinkType && span is EnrichedLinkSpan) {
203
+ emitLinkDetectedEvent(spannable, span, spanStart, spanEnd)
204
+ } else if (isMentionType && span is EnrichedMentionSpan) {
205
+ emitMentionDetectedEvent(spannable, span, spanStart, spanEnd)
206
+ }
207
+
208
+ return spanStart
209
+ }
210
+ }
211
+
212
+ return null
213
+ }
214
+
215
+ private fun emitSelectionChangeEvent(editable: Editable?, start: Int, end: Int) {
216
+ if (editable == null) return
217
+
218
+ val context = view.context as ReactContext
219
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
220
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
221
+
222
+ val text = editable.substring(start, end)
223
+ dispatcher?.dispatchEvent(OnChangeSelectionEvent(
224
+ surfaceId,
225
+ view.id,
226
+ text,
227
+ start ,
228
+ end,
229
+ view.experimentalSynchronousEvents,
230
+ ))
231
+ }
232
+
233
+ private fun emitLinkDetectedEvent(spannable: Spannable, span: EnrichedLinkSpan?, start: Int, end: Int) {
234
+ val text = spannable.substring(start, end)
235
+ val url = span?.getUrl() ?: ""
236
+
237
+ // Prevents emitting unnecessary events
238
+ if (text == previousLinkDetectedEvent["text"] && url == previousLinkDetectedEvent["url"]) return
239
+
240
+ previousLinkDetectedEvent.put("text", text)
241
+ previousLinkDetectedEvent.put("url", url)
242
+
243
+ val context = view.context as ReactContext
244
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
245
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
246
+ dispatcher?.dispatchEvent(OnLinkDetectedEvent(
247
+ surfaceId,
248
+ view.id,
249
+ text,
250
+ url,
251
+ start,
252
+ end,
253
+ view.experimentalSynchronousEvents,
254
+ ))
255
+ }
256
+
257
+ private fun emitMentionDetectedEvent(spannable: Spannable, span: EnrichedMentionSpan?, start: Int, end: Int) {
258
+ val text = spannable.substring(start, end)
259
+ val attributes = span?.getAttributes() ?: emptyMap()
260
+ val indicator = span?.getIndicator() ?: ""
261
+ val payload = JSONObject(attributes).toString()
262
+
263
+ val previousText = previousMentionDetectedEvent["text"] ?: ""
264
+ val previousPayload = previousMentionDetectedEvent["payload"] ?: ""
265
+ val previousIndicator = previousMentionDetectedEvent["indicator"] ?: ""
266
+
267
+ if (text == previousText && payload == previousPayload && indicator == previousIndicator) return
268
+
269
+ previousMentionDetectedEvent.put("text", text)
270
+ previousMentionDetectedEvent.put("payload", payload)
271
+ previousMentionDetectedEvent.put("indicator", indicator)
272
+
273
+ val context = view.context as ReactContext
274
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
275
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
276
+ dispatcher?.dispatchEvent(OnMentionDetectedEvent(
277
+ surfaceId,
278
+ view.id,
279
+ text,
280
+ indicator,
281
+ payload,
282
+ view.experimentalSynchronousEvents,
283
+ ))
284
+ }
285
+ }
@@ -0,0 +1,204 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReactContext
5
+ import com.facebook.react.bridge.WritableMap
6
+ import com.facebook.react.uimanager.UIManagerHelper
7
+ import com.swmansion.enriched.EnrichedTextInputView
8
+ import com.swmansion.enriched.events.OnChangeStateEvent
9
+ import com.swmansion.enriched.spans.EnrichedSpans
10
+
11
+ class EnrichedSpanState(private val view: EnrichedTextInputView) {
12
+ private var previousPayload: WritableMap? = null
13
+
14
+ var boldStart: Int? = null
15
+ private set
16
+ var italicStart: Int? = null
17
+ private set
18
+ var underlineStart: Int? = null
19
+ private set
20
+ var strikethroughStart: Int? = null
21
+ private set
22
+ var inlineCodeStart: Int? = null
23
+ private set
24
+ var h1Start: Int? = null
25
+ private set
26
+ var h2Start: Int? = null
27
+ private set
28
+ var h3Start: Int? = null
29
+ private set
30
+ var codeBlockStart: Int? = null
31
+ private set
32
+ var blockQuoteStart: Int? = null
33
+ private set
34
+ var orderedListStart: Int? = null
35
+ private set
36
+ var unorderedListStart: Int? = null
37
+ private set
38
+ var linkStart: Int? = null
39
+ private set
40
+ var imageStart: Int? = null
41
+ private set
42
+ var mentionStart: Int? = null
43
+ private set
44
+
45
+ fun setBoldStart(start: Int?) {
46
+ this.boldStart = start
47
+ emitStateChangeEvent()
48
+ }
49
+
50
+ fun setItalicStart(start: Int?) {
51
+ this.italicStart = start
52
+ emitStateChangeEvent()
53
+ }
54
+
55
+ fun setUnderlineStart(start: Int?) {
56
+ this.underlineStart = start
57
+ emitStateChangeEvent()
58
+ }
59
+
60
+ fun setStrikethroughStart(start: Int?) {
61
+ this.strikethroughStart = start
62
+ emitStateChangeEvent()
63
+ }
64
+
65
+ fun setInlineCodeStart(start: Int?) {
66
+ this.inlineCodeStart = start
67
+ emitStateChangeEvent()
68
+ }
69
+
70
+ fun setH1Start(start: Int?) {
71
+ this.h1Start = start
72
+ emitStateChangeEvent()
73
+ }
74
+
75
+ fun setH2Start(start: Int?) {
76
+ this.h2Start = start
77
+ emitStateChangeEvent()
78
+ }
79
+
80
+ fun setH3Start(start: Int?) {
81
+ this.h3Start = start
82
+ emitStateChangeEvent()
83
+ }
84
+
85
+ fun setCodeBlockStart(start: Int?) {
86
+ this.codeBlockStart = start
87
+ emitStateChangeEvent()
88
+ }
89
+
90
+ fun setBlockQuoteStart(start: Int?) {
91
+ this.blockQuoteStart = start
92
+ emitStateChangeEvent()
93
+ }
94
+
95
+ fun setOrderedListStart(start: Int?) {
96
+ this.orderedListStart = start
97
+ emitStateChangeEvent()
98
+ }
99
+
100
+ fun setUnorderedListStart(start: Int?) {
101
+ this.unorderedListStart = start
102
+ emitStateChangeEvent()
103
+ }
104
+
105
+ fun setLinkStart(start: Int?) {
106
+ this.linkStart = start
107
+ emitStateChangeEvent()
108
+ }
109
+
110
+ fun setImageStart(start: Int?) {
111
+ this.imageStart = start
112
+ emitStateChangeEvent()
113
+ }
114
+
115
+ fun setMentionStart(start: Int?) {
116
+ this.mentionStart = start
117
+ emitStateChangeEvent()
118
+ }
119
+
120
+ fun getStart(name: String): Int? {
121
+ val start = when (name) {
122
+ EnrichedSpans.BOLD -> boldStart
123
+ EnrichedSpans.ITALIC -> italicStart
124
+ EnrichedSpans.UNDERLINE -> underlineStart
125
+ EnrichedSpans.STRIKETHROUGH -> strikethroughStart
126
+ EnrichedSpans.INLINE_CODE -> inlineCodeStart
127
+ EnrichedSpans.H1 -> h1Start
128
+ EnrichedSpans.H2 -> h2Start
129
+ EnrichedSpans.H3 -> h3Start
130
+ EnrichedSpans.CODE_BLOCK -> codeBlockStart
131
+ EnrichedSpans.BLOCK_QUOTE -> blockQuoteStart
132
+ EnrichedSpans.ORDERED_LIST -> orderedListStart
133
+ EnrichedSpans.UNORDERED_LIST -> unorderedListStart
134
+ EnrichedSpans.LINK -> linkStart
135
+ EnrichedSpans.IMAGE -> imageStart
136
+ EnrichedSpans.MENTION -> mentionStart
137
+ else -> null
138
+ }
139
+
140
+ return start
141
+ }
142
+
143
+ fun setStart(name: String, start: Int?) {
144
+ when (name) {
145
+ EnrichedSpans.BOLD -> setBoldStart(start)
146
+ EnrichedSpans.ITALIC -> setItalicStart(start)
147
+ EnrichedSpans.UNDERLINE -> setUnderlineStart(start)
148
+ EnrichedSpans.STRIKETHROUGH -> setStrikethroughStart(start)
149
+ EnrichedSpans.INLINE_CODE -> setInlineCodeStart(start)
150
+ EnrichedSpans.H1 -> setH1Start(start)
151
+ EnrichedSpans.H2 -> setH2Start(start)
152
+ EnrichedSpans.H3 -> setH3Start(start)
153
+ EnrichedSpans.CODE_BLOCK -> setCodeBlockStart(start)
154
+ EnrichedSpans.BLOCK_QUOTE -> setBlockQuoteStart(start)
155
+ EnrichedSpans.ORDERED_LIST -> setOrderedListStart(start)
156
+ EnrichedSpans.UNORDERED_LIST -> setUnorderedListStart(start)
157
+ EnrichedSpans.LINK -> setLinkStart(start)
158
+ EnrichedSpans.IMAGE -> setImageStart(start)
159
+ EnrichedSpans.MENTION -> setMentionStart(start)
160
+ }
161
+ }
162
+
163
+ private fun emitStateChangeEvent() {
164
+ val payload: WritableMap = Arguments.createMap()
165
+ payload.putBoolean("isBold", boldStart != null)
166
+ payload.putBoolean("isItalic", italicStart != null)
167
+ payload.putBoolean("isUnderline", underlineStart != null)
168
+ payload.putBoolean("isStrikeThrough", strikethroughStart != null)
169
+ payload.putBoolean("isInlineCode", inlineCodeStart != null)
170
+ payload.putBoolean("isH1", h1Start != null)
171
+ payload.putBoolean("isH2", h2Start != null)
172
+ payload.putBoolean("isH3", h3Start != null)
173
+ payload.putBoolean("isCodeBlock", codeBlockStart != null)
174
+ payload.putBoolean("isBlockQuote", blockQuoteStart != null)
175
+ payload.putBoolean("isOrderedList", orderedListStart != null)
176
+ payload.putBoolean("isUnorderedList", unorderedListStart != null)
177
+ payload.putBoolean("isLink", linkStart != null)
178
+ payload.putBoolean("isImage", imageStart != null)
179
+ payload.putBoolean("isMention", mentionStart != null)
180
+
181
+ // Do not emit event if payload is the same
182
+ if (previousPayload == payload) {
183
+ return
184
+ }
185
+
186
+ previousPayload = Arguments.createMap().apply {
187
+ merge(payload)
188
+ }
189
+
190
+ val context = view.context as ReactContext
191
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
192
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
193
+ dispatcher?.dispatchEvent(OnChangeStateEvent(
194
+ surfaceId,
195
+ view.id,
196
+ payload,
197
+ view.experimentalSynchronousEvents,
198
+ ))
199
+ }
200
+
201
+ companion object {
202
+ const val NAME = "ReactNativeEnrichedView"
203
+ }
204
+ }
@@ -0,0 +1,91 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import android.text.Spannable
4
+ import android.text.SpannableString
5
+ import android.text.SpannableStringBuilder
6
+ import android.util.Log
7
+ import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
8
+ import com.swmansion.enriched.spans.interfaces.EnrichedParagraphSpan
9
+ import org.json.JSONObject
10
+
11
+ fun jsonStringToStringMap(json: String): Map<String, String> {
12
+ val result = mutableMapOf<String, String>()
13
+ try {
14
+ val jsonObject = JSONObject(json)
15
+ for (key in jsonObject.keys()) {
16
+ val value = jsonObject.opt(key)
17
+ if (value is String) {
18
+ result[key] = value
19
+ }
20
+ }
21
+ } catch (e: Exception) {
22
+ Log.w("ReactNativeEnrichedView", "Failed to parse JSON string to Map: $json", e)
23
+ }
24
+
25
+ return result
26
+ }
27
+
28
+ fun Spannable.getSafeSpanBoundaries(start: Int, end: Int): Pair<Int, Int> {
29
+ val safeStart = start.coerceAtMost(end).coerceAtLeast(0)
30
+ val safeEnd = end.coerceAtLeast(start).coerceAtMost(this.length)
31
+
32
+ return Pair(safeStart, safeEnd)
33
+ }
34
+
35
+ fun Spannable.getParagraphBounds(start: Int, end: Int): Pair<Int, Int> {
36
+ var startPosition = start.coerceAtLeast(0).coerceAtMost(this.length)
37
+ var endPosition = end.coerceAtLeast(0).coerceAtMost(this.length)
38
+
39
+ // Find the start of the paragraph
40
+ while (startPosition > 0 && this[startPosition - 1] != '\n') {
41
+ startPosition--
42
+ }
43
+
44
+ // Find the end of the paragraph
45
+ while (endPosition < this.length && this[endPosition] != '\n') {
46
+ endPosition++
47
+ }
48
+
49
+ if (startPosition >= endPosition) {
50
+ // If the start position is equal or greater than the end position, return the same position
51
+ startPosition = endPosition
52
+ }
53
+
54
+ return Pair(startPosition, endPosition)
55
+ }
56
+
57
+ fun Spannable.getParagraphBounds(index: Int): Pair<Int, Int> {
58
+ return this.getParagraphBounds(index, index)
59
+ }
60
+
61
+ fun Spannable.mergeSpannables(start: Int, end: Int, string: String): Spannable {
62
+ return this.mergeSpannables(start, end, SpannableString(string))
63
+ }
64
+
65
+ fun Spannable.mergeSpannables(start: Int, end: Int, spannable: Spannable): Spannable {
66
+ var finalStart = start
67
+ var finalEnd = end
68
+
69
+ val builder = SpannableStringBuilder(this)
70
+ val startBlockSpans = spannable.getSpans(0, 0, EnrichedBlockSpan::class.java)
71
+ val startParagraphSpans = spannable.getSpans(0, 0, EnrichedParagraphSpan::class.java)
72
+ val endBlockSpans = spannable.getSpans(this.length, this.length, EnrichedBlockSpan::class.java)
73
+ val endParagraphSpans = spannable.getSpans(this.length, this.length, EnrichedParagraphSpan::class.java)
74
+ val (paragraphStart, paragraphEnd) = this.getParagraphBounds(start, end)
75
+ val isNewLineStart = startBlockSpans.isNotEmpty() || startParagraphSpans.isNotEmpty()
76
+ val isNewLineEnd = endBlockSpans.isNotEmpty() || endParagraphSpans.isNotEmpty()
77
+
78
+ if (isNewLineStart && start != paragraphStart) {
79
+ builder.insert(start, "\n")
80
+ finalStart = start + 1
81
+ finalEnd = end + 1
82
+ }
83
+
84
+ if (isNewLineEnd && end != paragraphEnd) {
85
+ builder.insert(finalEnd, "\n")
86
+ }
87
+
88
+ builder.replace(finalStart, finalEnd, spannable)
89
+
90
+ return builder
91
+ }
@@ -0,0 +1,73 @@
1
+ package com.swmansion.enriched.watchers
2
+
3
+ import android.text.SpanWatcher
4
+ import android.text.Spannable
5
+ import android.text.style.ParagraphStyle
6
+ import com.facebook.react.bridge.ReactContext
7
+ import com.facebook.react.uimanager.UIManagerHelper
8
+ import com.swmansion.enriched.EnrichedTextInputView
9
+ import com.swmansion.enriched.events.OnChangeHtmlEvent
10
+ import com.swmansion.enriched.spans.EnrichedOrderedListSpan
11
+ import com.swmansion.enriched.spans.interfaces.EnrichedHeadingSpan
12
+ import com.swmansion.enriched.spans.interfaces.EnrichedSpan
13
+ import com.swmansion.enriched.utils.EnrichedParser
14
+ import com.swmansion.enriched.utils.getSafeSpanBoundaries
15
+
16
+ class EnrichedSpanWatcher(private val view: EnrichedTextInputView) : SpanWatcher {
17
+ private var previousHtml: String? = null
18
+
19
+ override fun onSpanAdded(text: Spannable, what: Any, start: Int, end: Int) {
20
+ updateNextLineLayout(what, text, end)
21
+ updateUnorderedListSpans(what, text, end)
22
+ emitEvent(text, what)
23
+ }
24
+
25
+ override fun onSpanRemoved(text: Spannable, what: Any, start: Int, end: Int) {
26
+ updateNextLineLayout(what, text, end)
27
+ updateUnorderedListSpans(what, text, end)
28
+ emitEvent(text, what)
29
+ }
30
+
31
+ override fun onSpanChanged(text: Spannable, what: Any, ostart: Int, oend: Int, nstart: Int, nend: Int) {
32
+ // Do nothing for now
33
+ }
34
+
35
+ private fun updateUnorderedListSpans(what: Any, text: Spannable, end: Int) {
36
+ if (what is EnrichedOrderedListSpan) {
37
+ view.listStyles?.updateOrderedListIndexes(text, end)
38
+ }
39
+ }
40
+
41
+ // After adding/removing heading span, we have to manually set empty paragraph span to the following text
42
+ // This allows us to update the layout (as it's not updated automatically - looks like an Android issue)
43
+ private fun updateNextLineLayout(what: Any, text: Spannable, end: Int) {
44
+ class EmptySpan : ParagraphStyle {}
45
+
46
+ if (what is EnrichedHeadingSpan) {
47
+ val finalStart = (end + 1)
48
+ val finalEnd = text.length
49
+ val (safeStart, safeEnd) = text.getSafeSpanBoundaries(finalStart, finalEnd)
50
+ text.setSpan(EmptySpan(), safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
51
+ }
52
+ }
53
+
54
+ fun emitEvent(s: Spannable, what: Any?) {
55
+ // Emit event only if we change one of ours spans
56
+ if (what != null && what !is EnrichedSpan) return
57
+
58
+ val html = EnrichedParser.toHtml(s)
59
+ if (html == previousHtml) return
60
+
61
+ previousHtml = html
62
+ view.layoutManager.invalidateLayout(view.text)
63
+ val context = view.context as ReactContext
64
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
65
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
66
+ dispatcher?.dispatchEvent(OnChangeHtmlEvent(
67
+ surfaceId,
68
+ view.id,
69
+ html,
70
+ view.experimentalSynchronousEvents,
71
+ ))
72
+ }
73
+ }
@@ -0,0 +1,51 @@
1
+ package com.swmansion.enriched.watchers
2
+
3
+ import android.text.Editable
4
+ import android.text.TextWatcher
5
+ import com.facebook.react.bridge.ReactContext
6
+ import com.facebook.react.uimanager.UIManagerHelper
7
+ import com.swmansion.enriched.EnrichedTextInputView
8
+ import com.swmansion.enriched.events.OnChangeTextEvent
9
+
10
+ class EnrichedTextWatcher(private val view: EnrichedTextInputView) : TextWatcher {
11
+ private var endCursorPosition: Int = 0
12
+ private var previousTextLength: Int = 0
13
+
14
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
15
+ previousTextLength = s?.length ?: 0
16
+ }
17
+
18
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
19
+ endCursorPosition = start + count
20
+ view.layoutManager.measureSize(s ?: "")
21
+ view.isRemovingMany = !view.isSettingValue && before > count + 1
22
+ }
23
+
24
+ override fun afterTextChanged(s: Editable?) {
25
+ if (s == null) return
26
+ emitEvents(s)
27
+
28
+ if (view.isSettingValue) return
29
+ applyStyles(s)
30
+ }
31
+
32
+ private fun applyStyles(s: Editable) {
33
+ view.inlineStyles?.afterTextChanged(s, endCursorPosition)
34
+ view.paragraphStyles?.afterTextChanged(s, endCursorPosition, previousTextLength)
35
+ view.listStyles?.afterTextChanged(s, endCursorPosition, previousTextLength)
36
+ view.parametrizedStyles?.afterTextChanged(s, endCursorPosition)
37
+ }
38
+
39
+ private fun emitEvents(s: Editable) {
40
+ val context = view.context as ReactContext
41
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
42
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
43
+ dispatcher?.dispatchEvent(OnChangeTextEvent(
44
+ surfaceId,
45
+ view.id,
46
+ s,
47
+ view.experimentalSynchronousEvents,
48
+ ))
49
+ view.spanWatcher?.emitEvent(s, null)
50
+ }
51
+ }