react-native-enriched 0.1.5 → 0.2.0
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 +3 -9
- 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/Props.cpp +5 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -45
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +53 -12
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +19 -22
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewPackage.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +36 -1
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedImageSpan.kt +132 -11
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +65 -46
- 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 +2 -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 +110 -3
- package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +75 -32
- package/android/src/main/java/com/swmansion/enriched/utils/AsyncDrawable.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +38 -15
- 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 -1
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
- package/android/src/main/res/drawable/broken_image.xml +10 -0
- package/ios/EnrichedTextInputView.h +3 -1
- package/ios/EnrichedTextInputView.mm +167 -68
- package/ios/config/InputConfig.h +6 -0
- package/ios/config/InputConfig.mm +32 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -45
- package/ios/generated/RNEnrichedTextInputViewSpec/RCTComponentViewHelpers.h +20 -4
- package/ios/inputParser/InputParser.mm +179 -31
- package/ios/inputTextView/InputTextView.mm +3 -5
- package/ios/internals/EnrichedTextInputViewShadowNode.h +1 -0
- package/ios/internals/EnrichedTextInputViewShadowNode.mm +29 -17
- package/ios/styles/BlockQuoteStyle.mm +5 -26
- package/ios/styles/BoldStyle.mm +2 -0
- package/ios/styles/CodeBlockStyle.mm +228 -0
- package/ios/styles/H1Style.mm +1 -0
- package/ios/styles/H2Style.mm +1 -0
- package/ios/styles/H3Style.mm +1 -0
- package/ios/styles/ImageStyle.mm +158 -0
- package/ios/styles/InlineCodeStyle.mm +2 -0
- package/ios/styles/ItalicStyle.mm +2 -0
- package/ios/styles/LinkStyle.mm +15 -7
- package/ios/styles/MentionStyle.mm +133 -36
- package/ios/styles/OrderedListStyle.mm +5 -8
- package/ios/styles/StrikethroughStyle.mm +2 -0
- package/ios/styles/UnderlineStyle.mm +2 -0
- package/ios/styles/UnorderedListStyle.mm +5 -8
- package/ios/utils/BaseStyleProtocol.h +1 -0
- package/ios/utils/ImageData.h +10 -0
- package/ios/utils/ImageData.mm +4 -0
- package/ios/utils/LayoutManagerExtension.mm +118 -3
- package/ios/utils/OccurenceUtils.h +4 -0
- package/ios/utils/OccurenceUtils.mm +47 -0
- package/ios/utils/ParagraphAttributesUtils.h +1 -0
- package/ios/utils/ParagraphAttributesUtils.mm +87 -20
- package/ios/utils/StringExtension.h +1 -1
- package/ios/utils/StringExtension.mm +17 -8
- package/ios/utils/StyleHeaders.h +12 -0
- package/ios/utils/ZeroWidthSpaceUtils.mm +22 -10
- package/lib/module/EnrichedTextInput.js +4 -2
- package/lib/module/EnrichedTextInput.js.map +1 -1
- package/lib/module/EnrichedTextInputNativeComponent.ts +7 -5
- package/lib/module/normalizeHtmlStyle.js +0 -4
- package/lib/module/normalizeHtmlStyle.js.map +1 -1
- package/lib/typescript/src/EnrichedTextInput.d.ts +3 -6
- package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +2 -5
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/normalizeHtmlStyle.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedTextInput.tsx +6 -7
- package/src/EnrichedTextInputNativeComponent.ts +7 -5
- package/src/normalizeHtmlStyle.ts +0 -4
|
@@ -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,18 +99,34 @@ 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
|
-
private fun
|
|
105
|
+
private fun canLinkBeApplied(): Boolean {
|
|
106
|
+
val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return true
|
|
107
|
+
val conflictingStyles = mergingConfig.conflictingStyles
|
|
108
|
+
val blockingStyles = mergingConfig.blockingStyles
|
|
109
|
+
|
|
110
|
+
for (style in blockingStyles) {
|
|
111
|
+
if (view.spanState?.getStart(style) != null) return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (style in conflictingStyles) {
|
|
115
|
+
if (view.spanState?.getStart(style) != null) return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun afterTextChangedLinks(result: TextRange) {
|
|
108
122
|
// Do not detect link if it's applied manually
|
|
109
|
-
if (isSettingLinkSpan) return
|
|
123
|
+
if (isSettingLinkSpan || !canLinkBeApplied()) return
|
|
124
|
+
|
|
110
125
|
val spannable = view.text as Spannable
|
|
111
126
|
val (word, start, end) = result
|
|
112
127
|
|
|
113
128
|
// TODO: Consider using more reliable regex, this one matches almost anything
|
|
114
129
|
val urlPattern = android.util.Patterns.WEB_URL.matcher(word)
|
|
115
|
-
|
|
116
130
|
val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java)
|
|
117
131
|
for (span in spans) {
|
|
118
132
|
spannable.removeSpan(span)
|
|
@@ -125,55 +139,80 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
|
|
|
125
139
|
}
|
|
126
140
|
}
|
|
127
141
|
|
|
128
|
-
private fun afterTextChangedMentions(
|
|
142
|
+
private fun afterTextChangedMentions(currentWord: TextRange) {
|
|
129
143
|
val mentionHandler = view.mentionHandler ?: return
|
|
130
144
|
val spannable = view.text as Spannable
|
|
131
|
-
val (word, start, end) = result
|
|
132
145
|
|
|
133
146
|
val indicatorsPattern = mentionIndicators.joinToString("|") { Regex.escape(it) }
|
|
134
147
|
val mentionIndicatorRegex = Regex("^($indicatorsPattern)")
|
|
135
148
|
val mentionRegex= Regex("^($indicatorsPattern)\\w*")
|
|
136
149
|
|
|
137
|
-
val spans = spannable.getSpans(start, end, EnrichedMentionSpan::class.java)
|
|
150
|
+
val spans = spannable.getSpans(currentWord.start, currentWord.end, EnrichedMentionSpan::class.java)
|
|
138
151
|
for (span in spans) {
|
|
139
152
|
spannable.removeSpan(span)
|
|
140
153
|
}
|
|
141
154
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)
|
|
145
162
|
|
|
146
|
-
//
|
|
147
|
-
if (
|
|
148
|
-
|
|
163
|
+
// No previous word -> no mention to be detected
|
|
164
|
+
if (previousWord == null) {
|
|
165
|
+
mentionHandler.endMention()
|
|
166
|
+
return
|
|
149
167
|
}
|
|
150
168
|
|
|
151
|
-
|
|
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 ?: ""
|
|
152
178
|
} else {
|
|
153
|
-
|
|
179
|
+
// Current word is a mention -> use it
|
|
180
|
+
finalStart = currentWord.start
|
|
181
|
+
indicator = mentionIndicatorRegex.find(currentWord.text)?.value ?: ""
|
|
154
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
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
mentionHandler.onMention(indicator, text)
|
|
155
193
|
}
|
|
156
194
|
|
|
157
|
-
fun setImageSpan(src: String) {
|
|
195
|
+
fun setImageSpan(src: String, width: Float, height: Float) {
|
|
158
196
|
if (view.selection == null) return
|
|
159
|
-
|
|
160
197
|
val spannable = view.text as SpannableStringBuilder
|
|
161
|
-
|
|
162
|
-
val spans = spannable.getSpans(start, end, EnrichedImageSpan::class.java)
|
|
198
|
+
val (start, originalEnd) = view.selection.getInlineSelection()
|
|
163
199
|
|
|
164
|
-
|
|
165
|
-
spannable.removeSpan(s)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (start == end) {
|
|
200
|
+
if (start == originalEnd) {
|
|
169
201
|
spannable.insert(start, "\uFFFC")
|
|
170
|
-
|
|
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")
|
|
171
209
|
}
|
|
172
210
|
|
|
173
|
-
val
|
|
174
|
-
val span = EnrichedImageSpan(
|
|
175
|
-
|
|
176
|
-
|
|
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)
|
|
177
216
|
}
|
|
178
217
|
|
|
179
218
|
fun startMention(indicator: String) {
|
|
@@ -229,4 +268,8 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
|
|
|
229
268
|
val spannable = view.text as Spannable
|
|
230
269
|
return removeSpansForRange(spannable, start, end, config.clazz)
|
|
231
270
|
}
|
|
271
|
+
|
|
272
|
+
companion object {
|
|
273
|
+
data class TextRange(val text: String, val start: Int, val end: Int)
|
|
274
|
+
}
|
|
232
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
|
+
}
|
|
@@ -275,7 +275,16 @@ public class EnrichedParser {
|
|
|
275
275
|
if (style[j] instanceof EnrichedImageSpan) {
|
|
276
276
|
out.append("<img src=\"");
|
|
277
277
|
out.append(((EnrichedImageSpan) style[j]).getSource());
|
|
278
|
-
out.append("\"
|
|
278
|
+
out.append("\"");
|
|
279
|
+
|
|
280
|
+
out.append(" width=\"");
|
|
281
|
+
out.append(((EnrichedImageSpan) style[j]).getWidth());
|
|
282
|
+
out.append("\"");
|
|
283
|
+
|
|
284
|
+
out.append(" height=\"");
|
|
285
|
+
out.append(((EnrichedImageSpan) style[j]).getHeight());
|
|
286
|
+
|
|
287
|
+
out.append("\"/>");
|
|
279
288
|
// Don't output the placeholder character underlying the image.
|
|
280
289
|
i = next;
|
|
281
290
|
}
|
|
@@ -350,6 +359,7 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
350
359
|
private final EnrichedParser.ImageGetter mImageGetter;
|
|
351
360
|
private static Integer currentOrderedListItemIndex = 0;
|
|
352
361
|
private static Boolean isInOrderedList = false;
|
|
362
|
+
private static Boolean isEmptyTag = false;
|
|
353
363
|
|
|
354
364
|
public HtmlToSpannedConverter(String source, HtmlStyle style, EnrichedParser.ImageGetter imageGetter, Parser parser) {
|
|
355
365
|
mStyle = style;
|
|
@@ -396,9 +406,15 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
396
406
|
for (EnrichedZeroWidthSpaceSpan zeroWidthSpaceSpan : zeroWidthSpaceSpans) {
|
|
397
407
|
int start = mSpannableStringBuilder.getSpanStart(zeroWidthSpaceSpan);
|
|
398
408
|
int end = mSpannableStringBuilder.getSpanEnd(zeroWidthSpaceSpan);
|
|
399
|
-
|
|
409
|
+
|
|
410
|
+
if (mSpannableStringBuilder.charAt(start) != '\u200B') {
|
|
411
|
+
// Insert zero-width space character at the start if it's not already present.
|
|
412
|
+
mSpannableStringBuilder.insert(start, "\u200B");
|
|
413
|
+
end++; // Adjust end position due to insertion.
|
|
414
|
+
}
|
|
415
|
+
|
|
400
416
|
mSpannableStringBuilder.removeSpan(zeroWidthSpaceSpan);
|
|
401
|
-
mSpannableStringBuilder.setSpan(zeroWidthSpaceSpan, start, end
|
|
417
|
+
mSpannableStringBuilder.setSpan(zeroWidthSpaceSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
402
418
|
}
|
|
403
419
|
|
|
404
420
|
return mSpannableStringBuilder;
|
|
@@ -409,6 +425,7 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
409
425
|
// We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
|
|
410
426
|
// so we can safely emit the linebreaks when we handle the close tag.
|
|
411
427
|
} else if (tag.equalsIgnoreCase("p")) {
|
|
428
|
+
isEmptyTag = true;
|
|
412
429
|
startBlockElement(mSpannableStringBuilder);
|
|
413
430
|
} else if (tag.equalsIgnoreCase("ul")) {
|
|
414
431
|
isInOrderedList = false;
|
|
@@ -418,14 +435,17 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
418
435
|
currentOrderedListItemIndex = 0;
|
|
419
436
|
startBlockElement(mSpannableStringBuilder);
|
|
420
437
|
} else if (tag.equalsIgnoreCase("li")) {
|
|
438
|
+
isEmptyTag = true;
|
|
421
439
|
startLi(mSpannableStringBuilder);
|
|
422
440
|
} else if (tag.equalsIgnoreCase("b")) {
|
|
423
441
|
start(mSpannableStringBuilder, new Bold());
|
|
424
442
|
} else if (tag.equalsIgnoreCase("i")) {
|
|
425
443
|
start(mSpannableStringBuilder, new Italic());
|
|
426
444
|
} else if (tag.equalsIgnoreCase("blockquote")) {
|
|
445
|
+
isEmptyTag = true;
|
|
427
446
|
startBlockquote(mSpannableStringBuilder);
|
|
428
447
|
} else if (tag.equalsIgnoreCase("codeblock")) {
|
|
448
|
+
isEmptyTag = true;
|
|
429
449
|
startCodeBlock(mSpannableStringBuilder);
|
|
430
450
|
} else if (tag.equalsIgnoreCase("a")) {
|
|
431
451
|
startA(mSpannableStringBuilder, attributes);
|
|
@@ -442,7 +462,7 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
442
462
|
} else if (tag.equalsIgnoreCase("h3")) {
|
|
443
463
|
startHeading(mSpannableStringBuilder, 3);
|
|
444
464
|
} else if (tag.equalsIgnoreCase("img")) {
|
|
445
|
-
startImg(mSpannableStringBuilder, attributes, mImageGetter
|
|
465
|
+
startImg(mSpannableStringBuilder, attributes, mImageGetter);
|
|
446
466
|
} else if (tag.equalsIgnoreCase("code")) {
|
|
447
467
|
start(mSpannableStringBuilder, new Code());
|
|
448
468
|
} else if (tag.equalsIgnoreCase("mention")) {
|
|
@@ -632,11 +652,17 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
632
652
|
}
|
|
633
653
|
}
|
|
634
654
|
|
|
635
|
-
private static void setParagraphSpanFromMark(
|
|
655
|
+
private static void setParagraphSpanFromMark(Editable text, Object mark, Object... spans) {
|
|
636
656
|
int where = text.getSpanStart(mark);
|
|
637
657
|
text.removeSpan(mark);
|
|
638
658
|
int len = text.length();
|
|
639
659
|
|
|
660
|
+
// Block spans require at least one character to be applied.
|
|
661
|
+
if (isEmptyTag) {
|
|
662
|
+
text.append("\u200B");
|
|
663
|
+
len++;
|
|
664
|
+
}
|
|
665
|
+
|
|
640
666
|
// Adjust the end position to exclude the newline character, if present
|
|
641
667
|
if (len > 0 && text.charAt(len - 1) == '\n') {
|
|
642
668
|
len--;
|
|
@@ -661,20 +687,15 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
661
687
|
}
|
|
662
688
|
}
|
|
663
689
|
|
|
664
|
-
private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img
|
|
690
|
+
private static void startImg(Editable text, Attributes attributes, EnrichedParser.ImageGetter img) {
|
|
665
691
|
String src = attributes.getValue("", "src");
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
d = img.getDrawable(src);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (d == null) {
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
692
|
+
String width = attributes.getValue("", "width");
|
|
693
|
+
String height = attributes.getValue("", "height");
|
|
674
694
|
|
|
675
695
|
int len = text.length();
|
|
696
|
+
EnrichedImageSpan span = EnrichedImageSpan.Companion.createEnrichedImageSpan(src, Integer.parseInt(width), Integer.parseInt(height));
|
|
676
697
|
text.append("");
|
|
677
|
-
text.setSpan(
|
|
698
|
+
text.setSpan(span, len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
678
699
|
}
|
|
679
700
|
|
|
680
701
|
private static void startA(Editable text, Attributes attributes) {
|
|
@@ -741,6 +762,8 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
741
762
|
|
|
742
763
|
public void characters(char[] ch, int start, int length) {
|
|
743
764
|
StringBuilder sb = new StringBuilder();
|
|
765
|
+
if (length > 0) isEmptyTag = false;
|
|
766
|
+
|
|
744
767
|
/*
|
|
745
768
|
* Ignore whitespace that immediately follows other whitespace;
|
|
746
769
|
* newlines count as spaces.
|
|
@@ -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
|
|
|
@@ -59,7 +62,6 @@ class EnrichedSpanWatcher(private val view: EnrichedTextInputView) : SpanWatcher
|
|
|
59
62
|
if (html == previousHtml) return
|
|
60
63
|
|
|
61
64
|
previousHtml = html
|
|
62
|
-
view.layoutManager.invalidateLayout(view.text)
|
|
63
65
|
val context = view.context as ReactContext
|
|
64
66
|
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
65
67
|
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
@@ -17,7 +17,7 @@ class EnrichedTextWatcher(private val view: EnrichedTextInputView) : TextWatcher
|
|
|
17
17
|
|
|
18
18
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
19
19
|
endCursorPosition = start + count
|
|
20
|
-
view.layoutManager.
|
|
20
|
+
view.layoutManager.invalidateLayout()
|
|
21
21
|
view.isRemovingMany = !view.isDuringTransaction && before > count + 1
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#include "EnrichedTextInputMeasurementManager.h"
|
|
2
|
+
#include "conversions.h"
|
|
2
3
|
|
|
3
4
|
#include <fbjni/fbjni.h>
|
|
4
5
|
#include <react/jni/ReadableNativeMap.h>
|
|
@@ -10,6 +11,7 @@ namespace facebook::react {
|
|
|
10
11
|
|
|
11
12
|
Size EnrichedTextInputMeasurementManager::measure(
|
|
12
13
|
SurfaceId surfaceId,
|
|
14
|
+
int viewTag,
|
|
13
15
|
const EnrichedTextInputViewProps& props,
|
|
14
16
|
LayoutConstraints layoutConstraints) const {
|
|
15
17
|
const jni::global_ref<jobject>& fabricUIManager =
|
|
@@ -33,12 +35,23 @@ namespace facebook::react {
|
|
|
33
35
|
|
|
34
36
|
local_ref<JString> componentName = make_jstring("EnrichedTextInputView");
|
|
35
37
|
|
|
38
|
+
// Prepare extraData map with viewTag
|
|
39
|
+
folly::dynamic extraData = folly::dynamic::object();
|
|
40
|
+
extraData["viewTag"] = viewTag;
|
|
41
|
+
local_ref<ReadableNativeMap::javaobject> extraDataRNM = ReadableNativeMap::newObjectCxxArgs(extraData);
|
|
42
|
+
local_ref<ReadableMap::javaobject> extraDataRM = make_local(reinterpret_cast<ReadableMap::javaobject>(extraDataRNM.get()));
|
|
43
|
+
|
|
44
|
+
// Prepare layout metrics affecting props
|
|
45
|
+
auto serializedProps = toDynamic(props);
|
|
46
|
+
local_ref<ReadableNativeMap::javaobject> propsRNM = ReadableNativeMap::newObjectCxxArgs(serializedProps);
|
|
47
|
+
local_ref<ReadableMap::javaobject> propsRM = make_local(reinterpret_cast<ReadableMap::javaobject>(propsRNM.get()));
|
|
48
|
+
|
|
36
49
|
auto measurement = yogaMeassureToSize(measure(
|
|
37
50
|
fabricUIManager,
|
|
38
51
|
surfaceId,
|
|
39
52
|
componentName.get(),
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
extraDataRM.get(),
|
|
54
|
+
propsRM.get(),
|
|
42
55
|
nullptr,
|
|
43
56
|
minimumSize.width,
|
|
44
57
|
maximumSize.width,
|
|
@@ -27,8 +27,7 @@ extern const char EnrichedTextInputComponentName[] = "EnrichedTextInputView";
|
|
|
27
27
|
Size EnrichedTextInputShadowNode::measureContent(
|
|
28
28
|
const LayoutContext &layoutContext,
|
|
29
29
|
const LayoutConstraints &layoutConstraints) const {
|
|
30
|
-
|
|
31
|
-
return measurementsManager_->measure(getSurfaceId(), getConcreteProps(), layoutConstraints);
|
|
30
|
+
return measurementsManager_->measure(getSurfaceId(), getTag(), getConcreteProps(), layoutConstraints);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
} // namespace facebook::react
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <folly/dynamic.h>
|
|
4
|
+
#include <react/renderer/components/FBReactNativeSpec/Props.h>
|
|
5
|
+
#include <react/renderer/core/propsConversions.h>
|
|
6
|
+
#include <react/renderer/components/RNEnrichedTextInputViewSpec/Props.h>
|
|
7
|
+
|
|
8
|
+
namespace facebook::react {
|
|
9
|
+
|
|
10
|
+
#ifdef RN_SERIALIZABLE_STATE
|
|
11
|
+
inline folly::dynamic toDynamic(const EnrichedTextInputViewProps &props)
|
|
12
|
+
{
|
|
13
|
+
// Serialize only metrics affecting props
|
|
14
|
+
folly::dynamic serializedProps = folly::dynamic::object();
|
|
15
|
+
serializedProps["defaultValue"] = props.defaultValue;
|
|
16
|
+
serializedProps["placeholder"] = props.placeholder;
|
|
17
|
+
serializedProps["fontSize"] = props.fontSize;
|
|
18
|
+
serializedProps["fontWeight"] = props.fontWeight;
|
|
19
|
+
serializedProps["fontStyle"] = props.fontStyle;
|
|
20
|
+
serializedProps["fontFamily"] = props.fontFamily;
|
|
21
|
+
serializedProps["htmlStyle"] = toDynamic(props.htmlStyle);
|
|
22
|
+
|
|
23
|
+
return serializedProps;
|
|
24
|
+
}
|
|
25
|
+
#endif
|
|
26
|
+
|
|
27
|
+
} // namespace facebook::react
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="24dp"
|
|
3
|
+
android:height="24dp"
|
|
4
|
+
android:viewportWidth="960"
|
|
5
|
+
android:viewportHeight="960"
|
|
6
|
+
android:tint="?attr/colorControlNormal">
|
|
7
|
+
<path
|
|
8
|
+
android:fillColor="@android:color/white"
|
|
9
|
+
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM240,503L400,343L560,503L720,343L760,383L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,463L240,503ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,496L720,456L560,616L400,456L240,616L200,576L200,760Q200,760 200,760Q200,760 200,760ZM200,760L200,760Q200,760 200,760Q200,760 200,760L200,496L200,576L200,463L200,383L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,463L200,463L200,576L200,576L200,760Q200,760 200,760Q200,760 200,760Z"/>
|
|
10
|
+
</vector>
|
|
@@ -18,14 +18,16 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
18
18
|
@public InputParser *parser;
|
|
19
19
|
@public NSMutableDictionary<NSAttributedStringKey, id> *defaultTypingAttributes;
|
|
20
20
|
@public NSDictionary<NSNumber *, id<BaseStyleProtocol>> *stylesDict;
|
|
21
|
+
NSDictionary<NSNumber *, NSArray<NSNumber *> *> *conflictingStyles;
|
|
22
|
+
NSDictionary<NSNumber *, NSArray<NSNumber *> *> *blockingStyles;
|
|
21
23
|
@public BOOL blockEmitting;
|
|
22
|
-
@public BOOL emitHtml;
|
|
23
24
|
}
|
|
24
25
|
- (CGSize)measureSize:(CGFloat)maxWidth;
|
|
25
26
|
- (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url range:(NSRange)range;
|
|
26
27
|
- (void)emitOnMentionEvent:(NSString *)indicator text:(nullable NSString *)text;
|
|
27
28
|
- (void)anyTextMayHaveBeenModified;
|
|
28
29
|
- (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range;
|
|
30
|
+
- (NSArray<NSNumber *> *)getPresentStyleTypesFrom:(NSArray<NSNumber *> *)types range:(NSRange)range;
|
|
29
31
|
@end
|
|
30
32
|
|
|
31
33
|
NS_ASSUME_NONNULL_END
|