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.
- package/README.md +4 -14
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +4 -1
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +2 -1
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.cpp +10 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/EventEmitters.h +7 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +0 -45
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +111 -2
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +9 -3
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/events/MentionHandler.kt +1 -1
- package/android/src/main/java/com/swmansion/enriched/events/OnRequestHtmlResultEvent.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBoldSpan.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +42 -1
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH1Span.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH2Span.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedH3Span.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +135 -9
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedItalicSpan.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedLinkSpan.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedMentionSpan.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +13 -3
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedStrikeThroughSpan.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnderlineSpan.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/spans/interfaces/EnrichedSpan.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/spans/utils/ForceRedrawSpan.kt +13 -0
- package/android/src/main/java/com/swmansion/enriched/styles/HtmlStyle.kt +80 -9
- package/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt +188 -5
- package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +57 -30
- package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +24 -13
- package/android/src/main/java/com/swmansion/enriched/utils/ResourceManager.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +3 -0
- package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.cpp +6 -6
- package/android/src/main/new_arch/RNEnrichedTextInputViewSpec.h +6 -6
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputComponentDescriptor.h +19 -19
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +40 -51
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +13 -15
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +23 -21
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.h +35 -36
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.cpp +4 -4
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputState.h +13 -14
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +12 -13
- package/android/src/main/res/drawable/broken_image.xml +10 -0
- package/ios/EnrichedTextInputView.h +27 -12
- package/ios/EnrichedTextInputView.mm +906 -547
- package/ios/attachments/ImageAttachment.h +10 -0
- package/ios/attachments/ImageAttachment.mm +34 -0
- package/ios/attachments/MediaAttachment.h +23 -0
- package/ios/attachments/MediaAttachment.mm +31 -0
- package/ios/config/InputConfig.h +12 -6
- package/ios/config/InputConfig.mm +71 -33
- package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.cpp +10 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/EventEmitters.h +7 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +0 -45
- package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +41 -4
- package/ios/inputParser/InputParser.h +5 -5
- package/ios/inputParser/InputParser.mm +867 -333
- package/ios/inputTextView/InputTextView.h +1 -1
- package/ios/inputTextView/InputTextView.mm +100 -59
- package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +11 -9
- package/ios/internals/EnrichedTextInputViewShadowNode.h +28 -24
- package/ios/internals/EnrichedTextInputViewShadowNode.mm +64 -47
- package/ios/internals/EnrichedTextInputViewState.h +3 -1
- package/ios/styles/BlockQuoteStyle.mm +192 -142
- package/ios/styles/BoldStyle.mm +96 -62
- package/ios/styles/CodeBlockStyle.mm +304 -0
- package/ios/styles/H1Style.mm +10 -3
- package/ios/styles/H2Style.mm +10 -3
- package/ios/styles/H3Style.mm +10 -3
- package/ios/styles/HeadingStyleBase.mm +129 -84
- package/ios/styles/ImageStyle.mm +160 -0
- package/ios/styles/InlineCodeStyle.mm +149 -84
- package/ios/styles/ItalicStyle.mm +77 -51
- package/ios/styles/LinkStyle.mm +353 -224
- package/ios/styles/MentionStyle.mm +434 -220
- package/ios/styles/OrderedListStyle.mm +172 -105
- package/ios/styles/StrikethroughStyle.mm +53 -34
- package/ios/styles/UnderlineStyle.mm +69 -45
- package/ios/styles/UnorderedListStyle.mm +170 -105
- package/ios/utils/BaseStyleProtocol.h +3 -2
- package/ios/utils/ColorExtension.mm +7 -5
- package/ios/utils/FontExtension.mm +42 -27
- package/ios/utils/ImageData.h +10 -0
- package/ios/utils/ImageData.mm +4 -0
- package/ios/utils/LayoutManagerExtension.h +1 -1
- package/ios/utils/LayoutManagerExtension.mm +334 -109
- package/ios/utils/MentionParams.h +0 -1
- package/ios/utils/MentionStyleProps.h +1 -1
- package/ios/utils/MentionStyleProps.mm +27 -20
- package/ios/utils/OccurenceUtils.h +42 -38
- package/ios/utils/OccurenceUtils.mm +177 -107
- package/ios/utils/ParagraphAttributesUtils.h +6 -1
- package/ios/utils/ParagraphAttributesUtils.mm +152 -41
- package/ios/utils/ParagraphsUtils.h +2 -1
- package/ios/utils/ParagraphsUtils.mm +40 -26
- package/ios/utils/StringExtension.h +1 -1
- package/ios/utils/StringExtension.mm +19 -16
- package/ios/utils/StyleHeaders.h +35 -11
- package/ios/utils/TextInsertionUtils.h +13 -2
- package/ios/utils/TextInsertionUtils.mm +38 -20
- package/ios/utils/WordsUtils.h +2 -1
- package/ios/utils/WordsUtils.mm +32 -22
- package/ios/utils/ZeroWidthSpaceUtils.h +3 -1
- package/ios/utils/ZeroWidthSpaceUtils.mm +153 -75
- package/lib/module/EnrichedTextInput.js +41 -3
- package/lib/module/EnrichedTextInput.js.map +1 -1
- package/lib/module/EnrichedTextInputNativeComponent.ts +17 -5
- package/lib/module/normalizeHtmlStyle.js +0 -4
- package/lib/module/normalizeHtmlStyle.js.map +1 -1
- package/lib/typescript/src/EnrichedTextInput.d.ts +2 -5
- package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +7 -5
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
- package/package.json +8 -1
- package/src/EnrichedTextInput.tsx +48 -7
- package/src/EnrichedTextInputNativeComponent.ts +17 -5
- 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
|
|
104
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
//
|
|
163
|
-
if (
|
|
164
|
-
|
|
163
|
+
// No previous word -> no mention to be detected
|
|
164
|
+
if (previousWord == null) {
|
|
165
|
+
mentionHandler.endMention()
|
|
166
|
+
return
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
val spans = spannable.getSpans(start, end, EnrichedImageSpan::class.java)
|
|
198
|
+
val (start, originalEnd) = view.selection.getInlineSelection()
|
|
179
199
|
|
|
180
|
-
|
|
181
|
-
spannable.removeSpan(s)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (start == end) {
|
|
200
|
+
if (start == originalEnd) {
|
|
185
201
|
spannable.insert(start, "\uFFFC")
|
|
186
|
-
|
|
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
|
|
190
|
-
val span = EnrichedImageSpan(
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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
|
|
698
|
+
private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img) {
|
|
683
699
|
String src = attributes.getValue("", "src");
|
|
684
|
-
|
|
685
|
-
|
|
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(
|
|
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
|
|
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
|
|
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 ¶ms) {
|
|
16
|
+
std::shared_ptr<TurboModule> RNEnrichedTextInputViewSpec_ModuleProvider(
|
|
17
|
+
const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) {
|
|
18
18
|
|
|
19
19
|
return nullptr;
|
|
20
20
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
|
|
2
2
|
/**
|
|
3
|
-
* This code was generated by
|
|
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
|
|
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(
|
|
23
|
+
std::shared_ptr<TurboModule> RNEnrichedTextInputViewSpec_ModuleProvider(
|
|
24
|
+
const std::string &moduleName, const JavaTurboModule::InitParams ¶ms);
|
|
25
25
|
|
|
26
26
|
} // namespace facebook::react
|