react-native-enriched 0.1.6 → 0.2.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 (123) hide show
  1. package/README.md +4 -14
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +4 -1
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +2 -1
  4. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.cpp +10 -0
  5. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.h +7 -0
  6. package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +0 -45
  7. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +111 -2
  8. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +9 -3
  9. package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
  10. package/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +1 -1
  11. package/android/src/main/java/com/swmansion/enriched/events/OnRequestHtmlResultEvent.kt +33 -0
  12. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +6 -0
  13. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBoldSpan.kt +6 -0
  14. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +42 -1
  15. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH1Span.kt +6 -0
  16. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH2Span.kt +6 -0
  17. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH3Span.kt +6 -0
  18. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +135 -9
  19. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +6 -0
  20. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedItalicSpan.kt +5 -0
  21. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedLinkSpan.kt +6 -0
  22. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedMentionSpan.kt +6 -0
  23. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +6 -0
  24. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +13 -3
  25. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedStrikeThroughSpan.kt +5 -0
  26. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnderlineSpan.kt +5 -0
  27. package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +6 -0
  28. package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedSpan.kt +4 -0
  29. package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +13 -0
  30. package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +80 -9
  31. package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +1 -0
  32. package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +188 -5
  33. package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +57 -30
  34. package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
  35. package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +24 -13
  36. package/android/src/main/java/com/swmansion/enriched/utils/ResourceManager.kt +26 -0
  37. package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +3 -0
  38. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.cpp +6 -6
  39. package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.h +6 -6
  40. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputComponentDescriptor.h +19 -19
  41. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +40 -51
  42. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +13 -15
  43. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +23 -21
  44. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.h +35 -36
  45. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.cpp +4 -4
  46. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.h +13 -14
  47. package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +12 -13
  48. package/android/src/main/res/drawable/broken_image.xml +10 -0
  49. package/ios/EnrichedTextInputView.h +27 -12
  50. package/ios/EnrichedTextInputView.mm +906 -547
  51. package/ios/attachments/ImageAttachment.h +10 -0
  52. package/ios/attachments/ImageAttachment.mm +34 -0
  53. package/ios/attachments/MediaAttachment.h +23 -0
  54. package/ios/attachments/MediaAttachment.mm +31 -0
  55. package/ios/config/InputConfig.h +12 -6
  56. package/ios/config/InputConfig.mm +71 -33
  57. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.cpp +10 -0
  58. package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.h +7 -0
  59. package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +0 -45
  60. package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +41 -4
  61. package/ios/inputParser/InputParser.h +5 -5
  62. package/ios/inputParser/InputParser.mm +867 -333
  63. package/ios/inputTextView/InputTextView.h +1 -1
  64. package/ios/inputTextView/InputTextView.mm +100 -59
  65. package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +11 -9
  66. package/ios/internals/EnrichedTextInputViewShadowNode.h +28 -24
  67. package/ios/internals/EnrichedTextInputViewShadowNode.mm +64 -47
  68. package/ios/internals/EnrichedTextInputViewState.h +3 -1
  69. package/ios/styles/BlockQuoteStyle.mm +192 -142
  70. package/ios/styles/BoldStyle.mm +96 -62
  71. package/ios/styles/CodeBlockStyle.mm +304 -0
  72. package/ios/styles/H1Style.mm +10 -3
  73. package/ios/styles/H2Style.mm +10 -3
  74. package/ios/styles/H3Style.mm +10 -3
  75. package/ios/styles/HeadingStyleBase.mm +129 -84
  76. package/ios/styles/ImageStyle.mm +160 -0
  77. package/ios/styles/InlineCodeStyle.mm +149 -84
  78. package/ios/styles/ItalicStyle.mm +77 -51
  79. package/ios/styles/LinkStyle.mm +353 -224
  80. package/ios/styles/MentionStyle.mm +434 -220
  81. package/ios/styles/OrderedListStyle.mm +172 -105
  82. package/ios/styles/StrikethroughStyle.mm +53 -34
  83. package/ios/styles/UnderlineStyle.mm +69 -45
  84. package/ios/styles/UnorderedListStyle.mm +170 -105
  85. package/ios/utils/BaseStyleProtocol.h +3 -2
  86. package/ios/utils/ColorExtension.mm +7 -5
  87. package/ios/utils/FontExtension.mm +42 -27
  88. package/ios/utils/ImageData.h +10 -0
  89. package/ios/utils/ImageData.mm +4 -0
  90. package/ios/utils/LayoutManagerExtension.h +1 -1
  91. package/ios/utils/LayoutManagerExtension.mm +334 -109
  92. package/ios/utils/MentionParams.h +0 -1
  93. package/ios/utils/MentionStyleProps.h +1 -1
  94. package/ios/utils/MentionStyleProps.mm +27 -20
  95. package/ios/utils/OccurenceUtils.h +42 -38
  96. package/ios/utils/OccurenceUtils.mm +177 -107
  97. package/ios/utils/ParagraphAttributesUtils.h +6 -1
  98. package/ios/utils/ParagraphAttributesUtils.mm +152 -41
  99. package/ios/utils/ParagraphsUtils.h +2 -1
  100. package/ios/utils/ParagraphsUtils.mm +40 -26
  101. package/ios/utils/StringExtension.h +1 -1
  102. package/ios/utils/StringExtension.mm +19 -16
  103. package/ios/utils/StyleHeaders.h +35 -11
  104. package/ios/utils/TextInsertionUtils.h +13 -2
  105. package/ios/utils/TextInsertionUtils.mm +38 -20
  106. package/ios/utils/WordsUtils.h +2 -1
  107. package/ios/utils/WordsUtils.mm +32 -22
  108. package/ios/utils/ZeroWidthSpaceUtils.h +3 -1
  109. package/ios/utils/ZeroWidthSpaceUtils.mm +153 -75
  110. package/lib/module/EnrichedTextInput.js +41 -3
  111. package/lib/module/EnrichedTextInput.js.map +1 -1
  112. package/lib/module/EnrichedTextInputNativeComponent.ts +17 -5
  113. package/lib/module/normalizeHtmlStyle.js +0 -4
  114. package/lib/module/normalizeHtmlStyle.js.map +1 -1
  115. package/lib/typescript/src/EnrichedTextInput.d.ts +2 -5
  116. package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
  117. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +7 -5
  118. package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
  119. package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
  120. package/package.json +8 -1
  121. package/src/EnrichedTextInput.tsx +48 -7
  122. package/src/EnrichedTextInputNativeComponent.ts +17 -5
  123. package/src/normalizeHtmlStyle.ts +0 -4
@@ -3,13 +3,85 @@ package com.swmansion.enriched.styles
3
3
  import android.text.Editable
4
4
  import android.text.Spannable
5
5
  import android.text.SpannableStringBuilder
6
+ import android.util.Log
6
7
  import com.swmansion.enriched.EnrichedTextInputView
7
8
  import com.swmansion.enriched.spans.EnrichedSpans
9
+ import com.swmansion.enriched.spans.interfaces.EnrichedSpan
8
10
  import com.swmansion.enriched.utils.getParagraphBounds
9
11
  import com.swmansion.enriched.utils.getSafeSpanBoundaries
10
12
 
11
13
  class ParagraphStyles(private val view: EnrichedTextInputView) {
14
+ private fun <T>getPreviousParagraphSpan(spannable: Spannable, paragraphStart: Int, type: Class<T>): T? {
15
+ if (paragraphStart <= 0) return null
16
+
17
+ val (previousParagraphStart, previousParagraphEnd) = spannable.getParagraphBounds(paragraphStart - 1)
18
+ val spans = spannable.getSpans(previousParagraphStart, previousParagraphEnd, type)
19
+
20
+ // A paragraph implies a single cohesive style. having multiple spans of the
21
+ // same type (e.g., two codeblock spans) in one paragraph is an invalid state in current library logic
22
+ if (spans.size > 1) {
23
+ Log.w("ParagraphStyles", "getPreviousParagraphSpan(): Found more than one span in the paragraph!")
24
+ }
25
+
26
+ if (spans.isNotEmpty()) {
27
+ return spans.first()
28
+ }
29
+
30
+ return null
31
+ }
32
+
33
+ private fun <T>getNextParagraphSpan(spannable: Spannable, paragraphEnd: Int, type: Class<T>): T? {
34
+ if (paragraphEnd >= spannable.length - 1) return null
35
+
36
+ val (nextParagraphStart, nextParagraphEnd) = spannable.getParagraphBounds(paragraphEnd + 1)
37
+
38
+ val spans = spannable.getSpans(nextParagraphStart, nextParagraphEnd, type)
39
+
40
+ // A paragraph implies a single cohesive style. having multiple spans of the
41
+ // same type (e.g., two codeblock spans) in one paragraph is an invalid state in current library logic
42
+ if (spans.size > 1) {
43
+ Log.w("ParagraphStyles", "getNextParagraphSpan(): Found more than one span in the paragraph!")
44
+ }
45
+
46
+ if (spans.isNotEmpty()) {
47
+ return spans.first()
48
+ }
49
+
50
+ return null
51
+ }
52
+
53
+ /**
54
+ * Applies a continuous span to the specified range.
55
+ * If the new range touches existing continuous spans, they are coalesced into a single span
56
+ */
57
+ private fun <T>setContinuousSpan(spannable: Spannable, start: Int, end: Int, type: Class<T>) {
58
+ val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
59
+ val previousSpan = getPreviousParagraphSpan(spannable, start, type)
60
+ val nextSpan = getNextParagraphSpan(spannable, end, type)
61
+ var newStart = start
62
+ var newEnd = end
63
+
64
+ if (previousSpan != null) {
65
+ newStart = spannable.getSpanStart(previousSpan)
66
+ spannable.removeSpan(previousSpan)
67
+ }
68
+
69
+ if (nextSpan != null && start != end) {
70
+ newEnd = spannable.getSpanEnd(nextSpan)
71
+ spannable.removeSpan(nextSpan)
72
+ }
73
+
74
+ val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(newStart, newEnd)
75
+ spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
76
+ }
77
+
78
+
12
79
  private fun <T>setSpan(spannable: Spannable, type: Class<T>, start: Int, end: Int) {
80
+ if (EnrichedSpans.isTypeContinuous(type)) {
81
+ setContinuousSpan(spannable, start, end, type)
82
+ return
83
+ }
84
+
13
85
  val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
14
86
  val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
15
87
  spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@@ -94,14 +166,116 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
94
166
  return spans.isNotEmpty()
95
167
  }
96
168
 
169
+ private fun <T>mergeAdjacentStyleSpans(s: Editable, endCursorPosition: Int, type: Class<T>) {
170
+ val (start, end) = s.getParagraphBounds(endCursorPosition)
171
+ val currParagraphSpans = s.getSpans(start, end, type)
172
+
173
+ if (currParagraphSpans.isEmpty()) {
174
+ return
175
+ }
176
+
177
+ val currSpan = currParagraphSpans[0]
178
+ val nextSpan = getNextParagraphSpan(s, end, type)
179
+
180
+ if (nextSpan == null) {
181
+ return
182
+ }
183
+
184
+ val newStart = s.getSpanStart(currSpan)
185
+ val newEnd = s.getSpanEnd(nextSpan)
186
+
187
+ s.removeSpan(nextSpan)
188
+ s.removeSpan(currSpan)
189
+
190
+ val (safeStart, safeEnd) = s.getSafeSpanBoundaries(newStart, newEnd)
191
+ val span = type.getDeclaredConstructor(HtmlStyle::class.java).newInstance(view.htmlStyle)
192
+
193
+ s.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
194
+ }
195
+
196
+ private fun handleConflictsDuringNewlineDeletion(s: Editable, style: String, paragraphStart: Int, paragraphEnd: Int): Boolean {
197
+ val spanState = view.spanState ?: return false
198
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(style, view.htmlStyle) ?: return false
199
+ var isConflicting = false
200
+ val stylesToCheck = mergingConfig.blockingStyles + mergingConfig.conflictingStyles
201
+
202
+ for (styleToCheck in stylesToCheck) {
203
+ val conflictingType = EnrichedSpans.allSpans[styleToCheck]?.clazz ?: continue
204
+
205
+ val spans = s.getSpans(paragraphStart, paragraphEnd, conflictingType)
206
+ if (spans.isEmpty()) {
207
+ continue
208
+ }
209
+ isConflicting = true
210
+
211
+ val isParagraphStyle = EnrichedSpans.paragraphSpans[styleToCheck] != null
212
+ if (!isParagraphStyle) {
213
+ continue
214
+ }
215
+
216
+ for (span in spans) {
217
+ extendStyleOnWholeParagraph(s, span as EnrichedSpan, conflictingType, paragraphEnd)
218
+ }
219
+ }
220
+
221
+ if (isConflicting) {
222
+ val styleStart = spanState.getStart(style) ?: return false
223
+ spanState.setStart(style, null)
224
+ removeStyle(style, styleStart, paragraphEnd)
225
+ return true
226
+ }
227
+
228
+ return false
229
+ }
230
+
231
+
232
+ private fun deleteConflictingAndBlockingStyles(s: Editable, style: String, paragraphStart: Int, paragraphEnd: Int) {
233
+ val mergingConfig = EnrichedSpans.getMergingConfigForStyle(style, view.htmlStyle) ?: return
234
+ val stylesToCheck = mergingConfig.blockingStyles + mergingConfig.conflictingStyles
235
+
236
+ for (styleToCheck in stylesToCheck) {
237
+ val conflictingType = EnrichedSpans.allSpans[styleToCheck]?.clazz ?: continue
238
+
239
+ val spans = s.getSpans(paragraphStart, paragraphEnd, conflictingType)
240
+ for (span in spans) {
241
+ s.removeSpan(span)
242
+ }
243
+ }
244
+ }
245
+
246
+ private fun <T>extendStyleOnWholeParagraph(s: Editable, span: EnrichedSpan, type: Class<T>, paragraphEnd: Int) {
247
+ val currStyleStart = s.getSpanStart(span)
248
+ s.removeSpan(span)
249
+ val (safeStart, safeEnd) = s.getSafeSpanBoundaries(currStyleStart, paragraphEnd)
250
+ setSpan(s, type, safeStart, safeEnd)
251
+ }
252
+
97
253
  fun afterTextChanged(s: Editable, endPosition: Int, previousTextLength: Int) {
98
254
  var endCursorPosition = endPosition
99
255
  val isBackspace = s.length < previousTextLength
100
256
  val isNewLine = endCursorPosition == 0 || endCursorPosition > 0 && s[endCursorPosition - 1] == '\n'
257
+ val spanState = view.spanState ?: return
101
258
 
102
259
  for ((style, config) in EnrichedSpans.paragraphSpans) {
103
- val spanState = view.spanState ?: continue
104
- val styleStart = spanState.getStart(style) ?: continue
260
+ val styleStart = spanState.getStart(style)
261
+
262
+ if (styleStart == null) {
263
+ if (isBackspace) {
264
+ val (start, end) = s.getParagraphBounds(endCursorPosition)
265
+ val spans = s.getSpans(start, end, config.clazz)
266
+
267
+ for (span in spans) {
268
+ // handle conflicts when entering paragraph with some paragraph style applied
269
+ deleteConflictingAndBlockingStyles(s, style, start, end)
270
+ extendStyleOnWholeParagraph(s, span as EnrichedSpan, config.clazz, end)
271
+ }
272
+ }
273
+
274
+ if (config.isContinuous) {
275
+ mergeAdjacentStyleSpans(s, endCursorPosition, config.clazz)
276
+ }
277
+ continue
278
+ }
105
279
 
106
280
  if (isNewLine) {
107
281
  if (!config.isContinuous) {
@@ -111,7 +285,7 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
111
285
 
112
286
  if (isBackspace) {
113
287
  endCursorPosition -= 1
114
- view.spanState.setStart(style, null)
288
+ spanState.setStart(style, null)
115
289
  } else {
116
290
  s.insert(endCursorPosition, "\u200B")
117
291
  endCursorPosition += 1
@@ -119,6 +293,15 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
119
293
  }
120
294
 
121
295
  var (start, end) = s.getParagraphBounds(styleStart, endCursorPosition)
296
+
297
+ // handle conflicts when deleting newline from paragraph style (going back to previous line)
298
+ if (isBackspace && styleStart != start) {
299
+ val isConflicting = handleConflictsDuringNewlineDeletion(s, style, start, end)
300
+ if (isConflicting) {
301
+ continue
302
+ }
303
+ }
304
+
122
305
  val isNotEndLineSpan = isSpanEnabledInNextLine(s, end, config.clazz)
123
306
  val spans = s.getSpans(start, end, config.clazz)
124
307
 
@@ -154,8 +337,8 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
154
337
 
155
338
  if (start == end) {
156
339
  spannable.insert(start, "\u200B")
157
- view.spanState?.setStart(name, start + 1)
158
340
  setAndMergeSpans(spannable, type, start, end + 1)
341
+ view.selection.validateStyles()
159
342
 
160
343
  return
161
344
  }
@@ -170,8 +353,8 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
170
353
  currentStart = currentEnd + 1
171
354
  }
172
355
 
173
- view.spanState?.setStart(name, start)
174
356
  setAndMergeSpans(spannable, type, start, currentEnd)
357
+ view.selection.validateStyles()
175
358
  }
176
359
 
177
360
  fun getStyleRange(): Pair<Int, Int> {
@@ -1,6 +1,5 @@
1
1
  package com.swmansion.enriched.styles
2
2
 
3
- import android.net.Uri
4
3
  import android.text.Editable
5
4
  import android.text.Spannable
6
5
  import android.text.SpannableStringBuilder
@@ -11,7 +10,6 @@ import com.swmansion.enriched.spans.EnrichedLinkSpan
11
10
  import com.swmansion.enriched.spans.EnrichedMentionSpan
12
11
  import com.swmansion.enriched.spans.EnrichedSpans
13
12
  import com.swmansion.enriched.utils.getSafeSpanBoundaries
14
- import java.io.File
15
13
 
16
14
  class ParametrizedStyles(private val view: EnrichedTextInputView) {
17
15
  private var mentionStart: Int? = null
@@ -85,7 +83,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
85
83
  }
86
84
  }
87
85
 
88
- private fun getWordAtIndex(s: Editable, index: Int): Triple<String, Int, Int>? {
86
+ private fun getWordAtIndex(s: CharSequence, index: Int): TextRange? {
89
87
  if (index < 0 ) return null
90
88
 
91
89
  var start = index
@@ -101,7 +99,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
101
99
 
102
100
  val result = s.subSequence(start, end).toString()
103
101
 
104
- return Triple(result, start, end)
102
+ return TextRange(result, start, end)
105
103
  }
106
104
 
107
105
  private fun canLinkBeApplied(): Boolean {
@@ -120,7 +118,7 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
120
118
  return true
121
119
  }
122
120
 
123
- private fun afterTextChangedLinks(result: Triple<String, Int, Int>) {
121
+ private fun afterTextChangedLinks(result: TextRange) {
124
122
  // Do not detect link if it's applied manually
125
123
  if (isSettingLinkSpan || !canLinkBeApplied()) return
126
124
 
@@ -141,55 +139,80 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
141
139
  }
142
140
  }
143
141
 
144
- private fun afterTextChangedMentions(result: Triple<String, Int, Int>) {
142
+ private fun afterTextChangedMentions(currentWord: TextRange) {
145
143
  val mentionHandler = view.mentionHandler ?: return
146
144
  val spannable = view.text as Spannable
147
- val (word, start, end) = result
148
145
 
149
146
  val indicatorsPattern = mentionIndicators.joinToString("|") { Regex.escape(it) }
150
147
  val mentionIndicatorRegex = Regex("^($indicatorsPattern)")
151
148
  val mentionRegex= Regex("^($indicatorsPattern)\\w*")
152
149
 
153
- val spans = spannable.getSpans(start, end, EnrichedMentionSpan::class.java)
150
+ val spans = spannable.getSpans(currentWord.start, currentWord.end, EnrichedMentionSpan::class.java)
154
151
  for (span in spans) {
155
152
  spannable.removeSpan(span)
156
153
  }
157
154
 
158
- if (mentionRegex.matches(word)) {
159
- val indicator = mentionIndicatorRegex.find(word)?.value ?: ""
160
- val text = word.replaceFirst(indicator, "")
155
+ var indicator: String
156
+ var finalStart: Int
157
+ val finalEnd = currentWord.end
158
+
159
+ // No mention in the current word, check previous one
160
+ if (!mentionRegex.matches(currentWord.text)) {
161
+ val previousWord = getWordAtIndex(spannable, currentWord.start - 1)
161
162
 
162
- // Means we are starting mention
163
- if (text.isEmpty()) {
164
- mentionStart = start
163
+ // No previous word -> no mention to be detected
164
+ if (previousWord == null) {
165
+ mentionHandler.endMention()
166
+ return
165
167
  }
166
168
 
167
- mentionHandler.onMention(indicator, text)
169
+ // Previous word is not a mention -> end mention
170
+ if (!mentionRegex.matches(previousWord.text)) {
171
+ mentionHandler.endMention()
172
+ return
173
+ }
174
+
175
+ // Previous word is a mention -> use it
176
+ finalStart = previousWord.start
177
+ indicator = mentionIndicatorRegex.find(previousWord.text)?.value ?: ""
168
178
  } else {
169
- mentionHandler.endMention()
179
+ // Current word is a mention -> use it
180
+ finalStart = currentWord.start
181
+ indicator = mentionIndicatorRegex.find(currentWord.text)?.value ?: ""
182
+ }
183
+
184
+ // Extract text without indicator
185
+ val text = spannable.subSequence(finalStart, finalEnd).toString().replaceFirst(indicator, "")
186
+
187
+ // Means we are starting mention
188
+ if (text.isEmpty()) {
189
+ mentionStart = finalStart
170
190
  }
191
+
192
+ mentionHandler.onMention(indicator, text)
171
193
  }
172
194
 
173
- fun setImageSpan(src: String) {
195
+ fun setImageSpan(src: String, width: Float, height: Float) {
174
196
  if (view.selection == null) return
175
-
176
197
  val spannable = view.text as SpannableStringBuilder
177
- var (start, end) = view.selection.getInlineSelection()
178
- val spans = spannable.getSpans(start, end, EnrichedImageSpan::class.java)
198
+ val (start, originalEnd) = view.selection.getInlineSelection()
179
199
 
180
- for (s in spans) {
181
- spannable.removeSpan(s)
182
- }
183
-
184
- if (start == end) {
200
+ if (start == originalEnd) {
185
201
  spannable.insert(start, "\uFFFC")
186
- end++
202
+ } else {
203
+ val spans = spannable.getSpans(start, originalEnd, EnrichedImageSpan::class.java)
204
+ for (s in spans) {
205
+ spannable.removeSpan(s)
206
+ }
207
+
208
+ spannable.replace(start, originalEnd, "\uFFFC")
187
209
  }
188
210
 
189
- val uri = Uri.fromFile(File(src))
190
- val span = EnrichedImageSpan(view.context, uri, view.htmlStyle)
191
- val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
192
- spannable.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
211
+ val (imageStart, imageEnd) = spannable.getSafeSpanBoundaries(start, start + 1)
212
+ val span = EnrichedImageSpan.createEnrichedImageSpan(src, width.toInt(), height.toInt())
213
+ span.observeAsyncDrawableLoaded(view.text)
214
+
215
+ spannable.setSpan(span, imageStart, imageEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
193
216
  }
194
217
 
195
218
  fun startMention(indicator: String) {
@@ -245,4 +268,8 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
245
268
  val spannable = view.text as Spannable
246
269
  return removeSpansForRange(spannable, start, end, config.clazz)
247
270
  }
271
+
272
+ companion object {
273
+ data class TextRange(val text: String, val start: Int, val end: Int)
274
+ }
248
275
  }
@@ -0,0 +1,91 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.res.Resources
5
+ import android.graphics.BitmapFactory
6
+ import android.graphics.Canvas
7
+ import android.graphics.Color
8
+ import android.graphics.ColorFilter
9
+ import android.graphics.PixelFormat
10
+ import android.graphics.drawable.Drawable
11
+ import android.os.Handler
12
+ import android.os.Looper
13
+ import android.util.Log
14
+ import androidx.core.content.res.ResourcesCompat
15
+ import androidx.core.graphics.drawable.DrawableCompat
16
+ import java.net.URL
17
+ import java.util.concurrent.Executors
18
+ import androidx.core.graphics.drawable.toDrawable
19
+ import com.swmansion.enriched.R
20
+
21
+ class AsyncDrawable (
22
+ private val url: String,
23
+ ) : Drawable() {
24
+ private var internalDrawable: Drawable = Color.TRANSPARENT.toDrawable()
25
+ private val mainHandler = Handler(Looper.getMainLooper())
26
+ private val executor = Executors.newSingleThreadExecutor()
27
+ var isLoaded = false
28
+
29
+ init {
30
+ internalDrawable.bounds = bounds
31
+
32
+ load()
33
+ }
34
+
35
+ private fun load() {
36
+ executor.execute {
37
+ try {
38
+ isLoaded = false
39
+ val inputStream = URL(url).openStream()
40
+ val bitmap = BitmapFactory.decodeStream(inputStream)
41
+
42
+ // Switch to Main Thread to update UI
43
+ mainHandler.post {
44
+ if (bitmap != null) {
45
+ val d = bitmap.toDrawable(Resources.getSystem())
46
+
47
+ d.bounds = bounds
48
+ internalDrawable = d
49
+ } else {
50
+ loadPlaceholderImage()
51
+ }
52
+ }
53
+ } catch (e: Exception) {
54
+ Log.e("AsyncDrawable", "Failed to load: $url", e)
55
+
56
+ loadPlaceholderImage()
57
+ } finally {
58
+ isLoaded = true
59
+ onLoaded?.invoke()
60
+ }
61
+ }
62
+ }
63
+
64
+ private fun loadPlaceholderImage() {
65
+ internalDrawable = ResourceManager.getDrawableResource(R.drawable.broken_image)
66
+ }
67
+
68
+ override fun draw(canvas: Canvas) {
69
+ internalDrawable.draw(canvas)
70
+ }
71
+
72
+ override fun setAlpha(alpha: Int) {
73
+ internalDrawable.alpha = alpha
74
+ }
75
+
76
+ override fun setColorFilter(colorFilter: ColorFilter?) {
77
+ internalDrawable.colorFilter = colorFilter
78
+ }
79
+
80
+ @Deprecated("Deprecated in Java")
81
+ override fun getOpacity(): Int {
82
+ return PixelFormat.TRANSLUCENT
83
+ }
84
+
85
+ override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
86
+ super.setBounds(left, top, right, bottom)
87
+ internalDrawable.setBounds(left, top, right, bottom)
88
+ }
89
+
90
+ var onLoaded: (() -> Unit)? = null
91
+ }
@@ -105,6 +105,14 @@ public class EnrichedParser {
105
105
  String normalizedBlockQuote = normalizedCodeBlock.replaceAll("</blockquote>\\n<br>", "</blockquote>");
106
106
  return "<html>\n" + normalizedBlockQuote + "</html>";
107
107
  }
108
+
109
+ public static String toHtmlWithDefault(CharSequence text) {
110
+ if (text instanceof Spanned) {
111
+ return toHtml((Spanned) text);
112
+ }
113
+ return "<html>\n<p></p>\n</html>";
114
+ }
115
+
108
116
  /**
109
117
  * Returns an HTML escaped representation of the given plain text.
110
118
  */
@@ -275,7 +283,16 @@ public class EnrichedParser {
275
283
  if (style[j] instanceof EnrichedImageSpan) {
276
284
  out.append("<img src=\"");
277
285
  out.append(((EnrichedImageSpan) style[j]).getSource());
278
- out.append("\">");
286
+ out.append("\"");
287
+
288
+ out.append(" width=\"");
289
+ out.append(((EnrichedImageSpan) style[j]).getWidth());
290
+ out.append("\"");
291
+
292
+ out.append(" height=\"");
293
+ out.append(((EnrichedImageSpan) style[j]).getHeight());
294
+
295
+ out.append("\"/>");
279
296
  // Don't output the placeholder character underlying the image.
280
297
  i = next;
281
298
  }
@@ -412,7 +429,6 @@ class HtmlToSpannedConverter implements ContentHandler {
412
429
  }
413
430
 
414
431
  private void handleStartTag(String tag, Attributes attributes) {
415
- isEmptyTag = false;
416
432
  if (tag.equalsIgnoreCase("br")) {
417
433
  // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
418
434
  // so we can safely emit the linebreaks when we handle the close tag.
@@ -454,7 +470,7 @@ class HtmlToSpannedConverter implements ContentHandler {
454
470
  } else if (tag.equalsIgnoreCase("h3")) {
455
471
  startHeading(mSpannableStringBuilder, 3);
456
472
  } else if (tag.equalsIgnoreCase("img")) {
457
- startImg(mSpannableStringBuilder, attributes, mImageGetter, mStyle);
473
+ startImg(mSpannableStringBuilder, attributes, mImageGetter);
458
474
  } else if (tag.equalsIgnoreCase("code")) {
459
475
  start(mSpannableStringBuilder, new Code());
460
476
  } else if (tag.equalsIgnoreCase("mention")) {
@@ -679,20 +695,15 @@ class HtmlToSpannedConverter implements ContentHandler {
679
695
  }
680
696
  }
681
697
 
682
- private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img, HtmlStyle style) {
698
+ private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img) {
683
699
  String src = attributes.getValue("", "src");
684
- Drawable d = null;
685
- if (img != null) {
686
- d = img.getDrawable(src);
687
- }
688
-
689
- if (d == null) {
690
- return;
691
- }
700
+ String width = attributes.getValue("", "width");
701
+ String height = attributes.getValue("", "height");
692
702
 
693
703
  int len = text.length();
704
+ EnrichedImageSpan span = EnrichedImageSpan.Companion.createEnrichedImageSpan(src, Integer.parseInt(width), Integer.parseInt(height));
694
705
  text.append("");
695
- text.setSpan(new EnrichedImageSpan(d, src, style), len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
706
+ text.setSpan(span, len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
696
707
  }
697
708
 
698
709
  private static void startA(Editable text, Attributes attributes) {
@@ -0,0 +1,26 @@
1
+ package com.swmansion.enriched.utils
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.content.res.Resources
6
+ import android.graphics.drawable.Drawable
7
+ import androidx.core.content.res.ResourcesCompat
8
+ import androidx.core.graphics.drawable.DrawableCompat
9
+
10
+ object ResourceManager {
11
+ private var appContext: Context? = null
12
+
13
+ fun init(context: Context) {
14
+ this.appContext = context.applicationContext
15
+ }
16
+
17
+ @SuppressLint("UseCompatLoadingForDrawables")
18
+ fun getDrawableResource(id: Int): Drawable {
19
+ val context = appContext ?: throw IllegalStateException("ResourceManager not initialized! Call init() first.")
20
+
21
+ val image = ResourcesCompat.getDrawable(context.resources, id, null)
22
+ val finalImage = image ?: Resources.getSystem().getDrawable(android.R.drawable.ic_menu_report_image)
23
+
24
+ return DrawableCompat.wrap(finalImage)
25
+ }
26
+ }
@@ -52,6 +52,9 @@ class EnrichedSpanWatcher(private val view: EnrichedTextInputView) : SpanWatcher
52
52
  }
53
53
 
54
54
  fun emitEvent(s: Spannable, what: Any?) {
55
+ // Do not parse spannable and emit event if onChangeHtml is not provided
56
+ if (!view.shouldEmitHtml) return
57
+
55
58
  // Emit event only if we change one of ours spans
56
59
  if (what != null && what !is EnrichedSpan) return
57
60
 
@@ -1,9 +1,10 @@
1
1
 
2
2
  /**
3
- * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
3
+ * This code was generated by
4
+ * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
4
5
  *
5
- * Do not edit this file as changes may cause incorrect behavior and will be lost
6
- * once the code is regenerated.
6
+ * Do not edit this file as changes may cause incorrect behavior and will be
7
+ * lost once the code is regenerated.
7
8
  *
8
9
  * @generated by codegen project: GenerateModuleJniCpp.js
9
10
  */
@@ -12,9 +13,8 @@
12
13
 
13
14
  namespace facebook::react {
14
15
 
15
-
16
-
17
- std::shared_ptr<TurboModule> RNEnrichedTextInputViewSpec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams &params) {
16
+ std::shared_ptr<TurboModule> RNEnrichedTextInputViewSpec_ModuleProvider(
17
+ const std::string &moduleName, const JavaTurboModule::InitParams &params) {
18
18
 
19
19
  return nullptr;
20
20
  }
@@ -1,9 +1,10 @@
1
1
 
2
2
  /**
3
- * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
3
+ * This code was generated by
4
+ * [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
4
5
  *
5
- * Do not edit this file as changes may cause incorrect behavior and will be lost
6
- * once the code is regenerated.
6
+ * Do not edit this file as changes may cause incorrect behavior and will be
7
+ * lost once the code is regenerated.
7
8
  *
8
9
  * @generated by codegen project: GenerateModuleJniH.js
9
10
  */
@@ -18,9 +19,8 @@
18
19
 
19
20
  namespace facebook::react {
20
21
 
21
-
22
-
23
22
  JSI_EXPORT
24
- std::shared_ptr<TurboModule> RNEnrichedTextInputViewSpec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams &params);
23
+ std::shared_ptr<TurboModule> RNEnrichedTextInputViewSpec_ModuleProvider(
24
+ const std::string &moduleName, const JavaTurboModule::InitParams &params);
25
25
 
26
26
  } // namespace facebook::react