react-native-enriched 0.1.4 → 0.1.6
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/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +1 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -0
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +65 -28
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -46
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +16 -19
- package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +6 -2
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +9 -5
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +10 -5
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +11 -1
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +61 -46
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +11 -1
- package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +1 -2
- package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +29 -12
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +23 -3
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +0 -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/ios/EnrichedTextInputView.h +0 -1
- package/ios/EnrichedTextInputView.mm +72 -36
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -0
- package/ios/inputParser/InputParser.mm +32 -7
- package/ios/inputTextView/InputTextView.mm +3 -5
- package/ios/styles/LinkStyle.mm +7 -7
- package/ios/styles/OrderedListStyle.mm +3 -8
- package/ios/styles/UnorderedListStyle.mm +3 -8
- package/ios/utils/StringExtension.h +1 -1
- package/ios/utils/StringExtension.mm +17 -8
- package/lib/module/EnrichedTextInput.js +2 -0
- package/lib/module/EnrichedTextInput.js.map +1 -1
- package/lib/module/EnrichedTextInputNativeComponent.ts +1 -0
- package/lib/typescript/src/EnrichedTextInput.d.ts +2 -1
- package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +1 -0
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/EnrichedTextInput.tsx +3 -0
- package/src/EnrichedTextInputNativeComponent.ts +1 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.swmansion.enriched.spans
|
|
2
2
|
|
|
3
|
+
import com.swmansion.enriched.styles.HtmlStyle
|
|
4
|
+
|
|
3
5
|
data class BaseSpanConfig(val clazz: Class<*>)
|
|
4
6
|
data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean)
|
|
5
7
|
data class ListSpanConfig(val clazz: Class<*>, val shortcut: String)
|
|
@@ -62,50 +64,63 @@ object EnrichedSpans {
|
|
|
62
64
|
MENTION to BaseSpanConfig(EnrichedMentionSpan::class.java),
|
|
63
65
|
)
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
67
|
+
fun getMergingConfigForStyle(style: String, htmlStyle: HtmlStyle): StylesMergingConfig? {
|
|
68
|
+
return when (style) {
|
|
69
|
+
BOLD -> {
|
|
70
|
+
val blockingStyles = mutableListOf(CODE_BLOCK)
|
|
71
|
+
if (htmlStyle.h1Bold) blockingStyles.add(H1)
|
|
72
|
+
if (htmlStyle.h2Bold) blockingStyles.add(H2)
|
|
73
|
+
if (htmlStyle.h3Bold) blockingStyles.add(H3)
|
|
74
|
+
StylesMergingConfig(blockingStyles = blockingStyles.toTypedArray())
|
|
75
|
+
}
|
|
76
|
+
ITALIC -> StylesMergingConfig(
|
|
77
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
78
|
+
)
|
|
79
|
+
UNDERLINE -> StylesMergingConfig(
|
|
80
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
81
|
+
)
|
|
82
|
+
STRIKETHROUGH -> StylesMergingConfig(
|
|
83
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
84
|
+
)
|
|
85
|
+
INLINE_CODE -> StylesMergingConfig(
|
|
86
|
+
conflictingStyles = arrayOf(MENTION, LINK),
|
|
87
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
88
|
+
)
|
|
89
|
+
H1 -> {
|
|
90
|
+
val conflictingStyles = mutableListOf(H2, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
|
|
91
|
+
if (htmlStyle.h1Bold) conflictingStyles.add(BOLD)
|
|
92
|
+
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
|
|
93
|
+
}
|
|
94
|
+
H2 -> {
|
|
95
|
+
val conflictingStyles = mutableListOf(H1, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
|
|
96
|
+
if (htmlStyle.h2Bold) conflictingStyles.add(BOLD)
|
|
97
|
+
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
|
|
98
|
+
}
|
|
99
|
+
H3 -> {
|
|
100
|
+
val conflictingStyles = mutableListOf(H1, H2, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
|
|
101
|
+
if (htmlStyle.h3Bold) conflictingStyles.add(BOLD)
|
|
102
|
+
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
|
|
103
|
+
}
|
|
104
|
+
BLOCK_QUOTE -> StylesMergingConfig(
|
|
105
|
+
conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST)
|
|
106
|
+
)
|
|
107
|
+
CODE_BLOCK -> StylesMergingConfig(
|
|
108
|
+
conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE)
|
|
109
|
+
)
|
|
110
|
+
UNORDERED_LIST -> StylesMergingConfig(
|
|
111
|
+
conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE)
|
|
112
|
+
)
|
|
113
|
+
ORDERED_LIST -> StylesMergingConfig(
|
|
114
|
+
conflictingStyles = arrayOf(H1, H2, H3, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
|
|
115
|
+
)
|
|
116
|
+
LINK -> StylesMergingConfig(
|
|
117
|
+
blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, MENTION)
|
|
118
|
+
)
|
|
119
|
+
IMAGE -> StylesMergingConfig()
|
|
120
|
+
MENTION -> StylesMergingConfig(
|
|
121
|
+
blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, LINK)
|
|
122
|
+
)
|
|
123
|
+
else -> null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
111
126
|
}
|
|
@@ -4,12 +4,22 @@ import android.graphics.Canvas
|
|
|
4
4
|
import android.graphics.Paint
|
|
5
5
|
import android.text.Layout
|
|
6
6
|
import android.text.Spanned
|
|
7
|
+
import android.text.TextPaint
|
|
7
8
|
import android.text.style.LeadingMarginSpan
|
|
9
|
+
import android.text.style.MetricAffectingSpan
|
|
8
10
|
import com.swmansion.enriched.spans.interfaces.EnrichedParagraphSpan
|
|
9
11
|
import com.swmansion.enriched.styles.HtmlStyle
|
|
10
12
|
|
|
11
13
|
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/BulletSpan.java
|
|
12
|
-
class EnrichedUnorderedListSpan(private val htmlStyle: HtmlStyle) : LeadingMarginSpan, EnrichedParagraphSpan {
|
|
14
|
+
class EnrichedUnorderedListSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedParagraphSpan {
|
|
15
|
+
override fun updateMeasureState(p0: TextPaint) {
|
|
16
|
+
// Do nothing, but inform layout that this span affects text metrics
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun updateDrawState(p0: TextPaint?) {
|
|
20
|
+
// Do nothing, but inform layout that this span affects text metrics
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
override fun getLeadingMargin(p0: Boolean): Int {
|
|
14
24
|
return htmlStyle.ulBulletSize + htmlStyle.ulGapWidth + htmlStyle.ulMarginLeft
|
|
15
25
|
}
|
|
@@ -58,12 +58,11 @@ class ListStyles(private val view: EnrichedTextInputView) {
|
|
|
58
58
|
val spans = ssb.getSpans(start, end, clazz)
|
|
59
59
|
if (spans.isEmpty()) return false
|
|
60
60
|
|
|
61
|
-
ssb.replace(start, end, ssb.substring(start, end).replace("\u200B", ""))
|
|
62
|
-
|
|
63
61
|
for (span in spans) {
|
|
64
62
|
ssb.removeSpan(span)
|
|
65
63
|
}
|
|
66
64
|
|
|
65
|
+
ssb.replace(start, end, ssb.substring(start, end).replace("\u200B", ""))
|
|
67
66
|
return true
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -104,15 +104,31 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
|
|
|
104
104
|
return Triple(result, start, end)
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
private fun canLinkBeApplied(): Boolean {
|
|
108
|
+
val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return true
|
|
109
|
+
val conflictingStyles = mergingConfig.conflictingStyles
|
|
110
|
+
val blockingStyles = mergingConfig.blockingStyles
|
|
111
|
+
|
|
112
|
+
for (style in blockingStyles) {
|
|
113
|
+
if (view.spanState?.getStart(style) != null) return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (style in conflictingStyles) {
|
|
117
|
+
if (view.spanState?.getStart(style) != null) return false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
|
|
107
123
|
private fun afterTextChangedLinks(result: Triple<String, Int, Int>) {
|
|
108
124
|
// Do not detect link if it's applied manually
|
|
109
|
-
if (isSettingLinkSpan) return
|
|
125
|
+
if (isSettingLinkSpan || !canLinkBeApplied()) return
|
|
126
|
+
|
|
110
127
|
val spannable = view.text as Spannable
|
|
111
128
|
val (word, start, end) = result
|
|
112
129
|
|
|
113
130
|
// TODO: Consider using more reliable regex, this one matches almost anything
|
|
114
131
|
val urlPattern = android.util.Patterns.WEB_URL.matcher(word)
|
|
115
|
-
|
|
116
132
|
val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java)
|
|
117
133
|
for (span in spans) {
|
|
118
134
|
spannable.removeSpan(span)
|
|
@@ -201,20 +217,21 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
|
|
|
201
217
|
}
|
|
202
218
|
|
|
203
219
|
val start = mentionStart ?: return
|
|
204
|
-
view.isDuringTransaction = true
|
|
205
|
-
spannable.replace(start, selectionEnd, text)
|
|
206
220
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
221
|
+
view.runAsATransaction {
|
|
222
|
+
spannable.replace(start, selectionEnd, text)
|
|
223
|
+
|
|
224
|
+
val span = EnrichedMentionSpan(text, indicator, attributes, view.htmlStyle)
|
|
225
|
+
val spanEnd = start + text.length
|
|
226
|
+
val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, spanEnd)
|
|
227
|
+
spannable.setSpan(span, safeStart, safeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
211
228
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
229
|
+
val hasSpaceAtTheEnd = spannable.length > safeEnd && spannable[safeEnd] == ' '
|
|
230
|
+
if (!hasSpaceAtTheEnd) {
|
|
231
|
+
spannable.insert(safeEnd, " ")
|
|
232
|
+
}
|
|
215
233
|
}
|
|
216
234
|
|
|
217
|
-
view.isDuringTransaction = false
|
|
218
235
|
view.mentionHandler?.reset()
|
|
219
236
|
view.selection.validateStyles()
|
|
220
237
|
}
|
|
@@ -350,6 +350,7 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
350
350
|
private final EnrichedParser.ImageGetter mImageGetter;
|
|
351
351
|
private static Integer currentOrderedListItemIndex = 0;
|
|
352
352
|
private static Boolean isInOrderedList = false;
|
|
353
|
+
private static Boolean isEmptyTag = false;
|
|
353
354
|
|
|
354
355
|
public HtmlToSpannedConverter(String source, HtmlStyle style, EnrichedParser.ImageGetter imageGetter, Parser parser) {
|
|
355
356
|
mStyle = style;
|
|
@@ -396,19 +397,27 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
396
397
|
for (EnrichedZeroWidthSpaceSpan zeroWidthSpaceSpan : zeroWidthSpaceSpans) {
|
|
397
398
|
int start = mSpannableStringBuilder.getSpanStart(zeroWidthSpaceSpan);
|
|
398
399
|
int end = mSpannableStringBuilder.getSpanEnd(zeroWidthSpaceSpan);
|
|
399
|
-
|
|
400
|
+
|
|
401
|
+
if (mSpannableStringBuilder.charAt(start) != '\u200B') {
|
|
402
|
+
// Insert zero-width space character at the start if it's not already present.
|
|
403
|
+
mSpannableStringBuilder.insert(start, "\u200B");
|
|
404
|
+
end++; // Adjust end position due to insertion.
|
|
405
|
+
}
|
|
406
|
+
|
|
400
407
|
mSpannableStringBuilder.removeSpan(zeroWidthSpaceSpan);
|
|
401
|
-
mSpannableStringBuilder.setSpan(zeroWidthSpaceSpan, start, end
|
|
408
|
+
mSpannableStringBuilder.setSpan(zeroWidthSpaceSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
402
409
|
}
|
|
403
410
|
|
|
404
411
|
return mSpannableStringBuilder;
|
|
405
412
|
}
|
|
406
413
|
|
|
407
414
|
private void handleStartTag(String tag, Attributes attributes) {
|
|
415
|
+
isEmptyTag = false;
|
|
408
416
|
if (tag.equalsIgnoreCase("br")) {
|
|
409
417
|
// We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
|
|
410
418
|
// so we can safely emit the linebreaks when we handle the close tag.
|
|
411
419
|
} else if (tag.equalsIgnoreCase("p")) {
|
|
420
|
+
isEmptyTag = true;
|
|
412
421
|
startBlockElement(mSpannableStringBuilder);
|
|
413
422
|
} else if (tag.equalsIgnoreCase("ul")) {
|
|
414
423
|
isInOrderedList = false;
|
|
@@ -418,14 +427,17 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
418
427
|
currentOrderedListItemIndex = 0;
|
|
419
428
|
startBlockElement(mSpannableStringBuilder);
|
|
420
429
|
} else if (tag.equalsIgnoreCase("li")) {
|
|
430
|
+
isEmptyTag = true;
|
|
421
431
|
startLi(mSpannableStringBuilder);
|
|
422
432
|
} else if (tag.equalsIgnoreCase("b")) {
|
|
423
433
|
start(mSpannableStringBuilder, new Bold());
|
|
424
434
|
} else if (tag.equalsIgnoreCase("i")) {
|
|
425
435
|
start(mSpannableStringBuilder, new Italic());
|
|
426
436
|
} else if (tag.equalsIgnoreCase("blockquote")) {
|
|
437
|
+
isEmptyTag = true;
|
|
427
438
|
startBlockquote(mSpannableStringBuilder);
|
|
428
439
|
} else if (tag.equalsIgnoreCase("codeblock")) {
|
|
440
|
+
isEmptyTag = true;
|
|
429
441
|
startCodeBlock(mSpannableStringBuilder);
|
|
430
442
|
} else if (tag.equalsIgnoreCase("a")) {
|
|
431
443
|
startA(mSpannableStringBuilder, attributes);
|
|
@@ -632,11 +644,17 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
632
644
|
}
|
|
633
645
|
}
|
|
634
646
|
|
|
635
|
-
private static void setParagraphSpanFromMark(
|
|
647
|
+
private static void setParagraphSpanFromMark(Editable text, Object mark, Object... spans) {
|
|
636
648
|
int where = text.getSpanStart(mark);
|
|
637
649
|
text.removeSpan(mark);
|
|
638
650
|
int len = text.length();
|
|
639
651
|
|
|
652
|
+
// Block spans require at least one character to be applied.
|
|
653
|
+
if (isEmptyTag) {
|
|
654
|
+
text.append("\u200B");
|
|
655
|
+
len++;
|
|
656
|
+
}
|
|
657
|
+
|
|
640
658
|
// Adjust the end position to exclude the newline character, if present
|
|
641
659
|
if (len > 0 && text.charAt(len - 1) == '\n') {
|
|
642
660
|
len--;
|
|
@@ -741,6 +759,8 @@ class HtmlToSpannedConverter implements ContentHandler {
|
|
|
741
759
|
|
|
742
760
|
public void characters(char[] ch, int start, int length) {
|
|
743
761
|
StringBuilder sb = new StringBuilder();
|
|
762
|
+
if (length > 0) isEmptyTag = false;
|
|
763
|
+
|
|
744
764
|
/*
|
|
745
765
|
* Ignore whitespace that immediately follows other whitespace;
|
|
746
766
|
* newlines count as spaces.
|
|
@@ -59,7 +59,6 @@ class EnrichedSpanWatcher(private val view: EnrichedTextInputView) : SpanWatcher
|
|
|
59
59
|
if (html == previousHtml) return
|
|
60
60
|
|
|
61
61
|
previousHtml = html
|
|
62
|
-
view.layoutManager.invalidateLayout(view.text)
|
|
63
62
|
val context = view.context as ReactContext
|
|
64
63
|
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
65
64
|
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
|
|
@@ -19,7 +19,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
19
19
|
@public NSMutableDictionary<NSAttributedStringKey, id> *defaultTypingAttributes;
|
|
20
20
|
@public NSDictionary<NSNumber *, id<BaseStyleProtocol>> *stylesDict;
|
|
21
21
|
@public BOOL blockEmitting;
|
|
22
|
-
@public BOOL emitHtml;
|
|
23
22
|
}
|
|
24
23
|
- (CGSize)measureSize:(CGFloat)maxWidth;
|
|
25
24
|
- (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url range:(NSRange)range;
|
|
@@ -34,6 +34,7 @@ using namespace facebook::react;
|
|
|
34
34
|
MentionParams *_recentlyActiveMentionParams;
|
|
35
35
|
NSRange _recentlyActiveMentionRange;
|
|
36
36
|
NSString *_recentlyEmittedHtml;
|
|
37
|
+
BOOL _emitHtml;
|
|
37
38
|
UILabel *_placeholderLabel;
|
|
38
39
|
UIColor *_placeholderColor;
|
|
39
40
|
BOOL _emitFocusBlur;
|
|
@@ -74,8 +75,8 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
74
75
|
_recentlyActiveMentionRange = NSMakeRange(0, 0);
|
|
75
76
|
recentlyChangedRange = NSMakeRange(0, 0);
|
|
76
77
|
_recentlyEmittedString = @"";
|
|
77
|
-
_recentlyEmittedHtml = @"";
|
|
78
|
-
|
|
78
|
+
_recentlyEmittedHtml = @"<html>\n<p></p>\n</html>";
|
|
79
|
+
_emitHtml = NO;
|
|
79
80
|
blockEmitting = NO;
|
|
80
81
|
_emitFocusBlur = YES;
|
|
81
82
|
|
|
@@ -357,6 +358,10 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
357
358
|
stylePropChanged = YES;
|
|
358
359
|
}
|
|
359
360
|
|
|
361
|
+
if(newViewProps.scrollEnabled != oldViewProps.scrollEnabled || textView.scrollEnabled != newViewProps.scrollEnabled) {
|
|
362
|
+
[textView setScrollEnabled:newViewProps.scrollEnabled];
|
|
363
|
+
}
|
|
364
|
+
|
|
360
365
|
folly::dynamic oldMentionStyle = oldViewProps.htmlStyle.mention;
|
|
361
366
|
folly::dynamic newMentionStyle = newViewProps.htmlStyle.mention;
|
|
362
367
|
if(oldMentionStyle != newMentionStyle) {
|
|
@@ -393,22 +398,17 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
393
398
|
|
|
394
399
|
// now set the new config
|
|
395
400
|
config = newConfig;
|
|
396
|
-
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
emitHtml = NO;
|
|
401
|
-
}
|
|
402
|
-
|
|
401
|
+
|
|
402
|
+
// no emitting during styles reload
|
|
403
|
+
blockEmitting = YES;
|
|
404
|
+
|
|
403
405
|
// make sure everything is sound in the html
|
|
404
406
|
NSString *initiallyProcessedHtml = [parser initiallyProcessHtml:currentHtml];
|
|
405
407
|
if(initiallyProcessedHtml != nullptr) {
|
|
406
408
|
[parser replaceWholeFromHtml:initiallyProcessedHtml];
|
|
407
409
|
}
|
|
408
410
|
|
|
409
|
-
|
|
410
|
-
emitHtml = YES;
|
|
411
|
-
}
|
|
411
|
+
blockEmitting = NO;
|
|
412
412
|
|
|
413
413
|
// fill the typing attributes with style props
|
|
414
414
|
defaultTypingAttributes[NSForegroundColorAttributeName] = [config primaryColor];
|
|
@@ -509,7 +509,7 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
// isOnChangeHtmlSet
|
|
512
|
-
|
|
512
|
+
_emitHtml = newViewProps.isOnChangeHtmlSet;
|
|
513
513
|
|
|
514
514
|
[super updateProps:props oldProps:oldProps];
|
|
515
515
|
// mandatory text and height checks
|
|
@@ -605,6 +605,9 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
605
605
|
// style updates are emitted only if something differs from the previously active styles
|
|
606
606
|
BOOL updateNeeded = NO;
|
|
607
607
|
|
|
608
|
+
// active styles are kept in a separate set until we're sure they can be emitted
|
|
609
|
+
NSMutableSet *newActiveStyles = [_activeStyles mutableCopy];
|
|
610
|
+
|
|
608
611
|
// data for onLinkDetected event
|
|
609
612
|
LinkData *detectedLinkData;
|
|
610
613
|
NSRange detectedLinkRange = NSMakeRange(0, 0);
|
|
@@ -615,14 +618,14 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
615
618
|
|
|
616
619
|
for (NSNumber* type in stylesDict) {
|
|
617
620
|
id<BaseStyleProtocol> style = stylesDict[type];
|
|
618
|
-
BOOL wasActive = [
|
|
621
|
+
BOOL wasActive = [newActiveStyles containsObject: type];
|
|
619
622
|
BOOL isActive = [style detectStyle:textView.selectedRange];
|
|
620
623
|
if(wasActive != isActive) {
|
|
621
624
|
updateNeeded = YES;
|
|
622
625
|
if(isActive) {
|
|
623
|
-
[
|
|
626
|
+
[newActiveStyles addObject:type];
|
|
624
627
|
} else {
|
|
625
|
-
[
|
|
628
|
+
[newActiveStyles removeObject:type];
|
|
626
629
|
}
|
|
627
630
|
}
|
|
628
631
|
|
|
@@ -682,6 +685,9 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
682
685
|
if(updateNeeded) {
|
|
683
686
|
auto emitter = [self getEventEmitter];
|
|
684
687
|
if(emitter != nullptr) {
|
|
688
|
+
// update activeStyles only if emitter is available
|
|
689
|
+
_activeStyles = newActiveStyles;
|
|
690
|
+
|
|
685
691
|
emitter->onChangeState({
|
|
686
692
|
.isBold = [_activeStyles containsObject: @([BoldStyle getStyleType])],
|
|
687
693
|
.isItalic = [_activeStyles containsObject: @([ItalicStyle getStyleType])],
|
|
@@ -705,9 +711,6 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
705
711
|
if(detectedLinkData != nullptr) {
|
|
706
712
|
// emit onLinkeDetected event
|
|
707
713
|
[self emitOnLinkDetectedEvent:detectedLinkData.text url:detectedLinkData.url range:detectedLinkRange];
|
|
708
|
-
|
|
709
|
-
_recentlyActiveLinkData = detectedLinkData;
|
|
710
|
-
_recentlyActiveLinkRange = detectedLinkRange;
|
|
711
714
|
}
|
|
712
715
|
|
|
713
716
|
if(detectedMentionParams != nullptr) {
|
|
@@ -806,6 +809,13 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
806
809
|
- (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url range:(NSRange)range {
|
|
807
810
|
auto emitter = [self getEventEmitter];
|
|
808
811
|
if(emitter != nullptr) {
|
|
812
|
+
// update recently active link info
|
|
813
|
+
LinkData *newLinkData = [[LinkData alloc] init];
|
|
814
|
+
newLinkData.text = text;
|
|
815
|
+
newLinkData.url = url;
|
|
816
|
+
_recentlyActiveLinkData = newLinkData;
|
|
817
|
+
_recentlyActiveLinkRange = range;
|
|
818
|
+
|
|
809
819
|
emitter->onLinkDetected({
|
|
810
820
|
.text = [text toCppString],
|
|
811
821
|
.url = [url toCppString],
|
|
@@ -846,7 +856,7 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
846
856
|
}
|
|
847
857
|
|
|
848
858
|
- (void)tryEmittingOnChangeHtmlEvent {
|
|
849
|
-
if(!
|
|
859
|
+
if(!_emitHtml || textView.markedTextRange != nullptr) {
|
|
850
860
|
return;
|
|
851
861
|
}
|
|
852
862
|
auto emitter = [self getEventEmitter];
|
|
@@ -994,6 +1004,9 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
994
1004
|
textView.typingAttributes = defaultTypingAttributes;
|
|
995
1005
|
}
|
|
996
1006
|
}
|
|
1007
|
+
|
|
1008
|
+
// update active styles as well
|
|
1009
|
+
[self tryUpdatingActiveStyles];
|
|
997
1010
|
}
|
|
998
1011
|
|
|
999
1012
|
- (void)handleWordModificationBasedChanges:(NSString*)word inRange:(NSRange)range {
|
|
@@ -1043,6 +1056,12 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
1043
1056
|
[h3Style handleImproperHeadings];
|
|
1044
1057
|
}
|
|
1045
1058
|
|
|
1059
|
+
// mentions removal management
|
|
1060
|
+
MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
|
|
1061
|
+
if(mentionStyleClass != nullptr) {
|
|
1062
|
+
[mentionStyleClass handleExistingMentions];
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1046
1065
|
// placholder management
|
|
1047
1066
|
if(!_placeholderLabel.hidden && textView.textStorage.string.length > 0) {
|
|
1048
1067
|
[self setPlaceholderLabelShown:NO];
|
|
@@ -1051,12 +1070,6 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
1051
1070
|
}
|
|
1052
1071
|
|
|
1053
1072
|
if(![textView.textStorage.string isEqualToString:_recentlyEmittedString]) {
|
|
1054
|
-
// mentions removal management
|
|
1055
|
-
MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
|
|
1056
|
-
if(mentionStyleClass != nullptr) {
|
|
1057
|
-
[mentionStyleClass handleExistingMentions];
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
1073
|
// modified words handling
|
|
1061
1074
|
NSArray *modifiedWords = [WordsUtils getAffectedWordsFromText:textView.textStorage.string modificationRange:recentlyChangedRange];
|
|
1062
1075
|
if(modifiedWords != nullptr) {
|
|
@@ -1078,23 +1091,49 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
1078
1091
|
// emit onChangeText event
|
|
1079
1092
|
auto emitter = [self getEventEmitter];
|
|
1080
1093
|
if(emitter != nullptr) {
|
|
1094
|
+
// set the recently emitted string only if the emitter is defined
|
|
1095
|
+
_recentlyEmittedString = stringToBeEmitted;
|
|
1096
|
+
|
|
1081
1097
|
emitter->onChangeText({
|
|
1082
1098
|
.value = [stringToBeEmitted toCppString]
|
|
1083
1099
|
});
|
|
1084
1100
|
}
|
|
1085
|
-
|
|
1086
|
-
// set the recently emitted string
|
|
1087
|
-
_recentlyEmittedString = stringToBeEmitted;
|
|
1088
1101
|
}
|
|
1089
1102
|
|
|
1090
1103
|
// update height on each character change
|
|
1091
1104
|
[self tryUpdatingHeight];
|
|
1092
1105
|
// update active styles as well
|
|
1093
1106
|
[self tryUpdatingActiveStyles];
|
|
1094
|
-
// update drawing
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1107
|
+
// update drawing - schedule debounced relayout
|
|
1108
|
+
[self scheduleRelayoutIfNeeded];
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Debounced relayout helper - coalesces multiple requests into one per runloop tick
|
|
1112
|
+
- (void)scheduleRelayoutIfNeeded
|
|
1113
|
+
{
|
|
1114
|
+
// Cancel any previously scheduled invocation to debounce
|
|
1115
|
+
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_performRelayout) object:nil];
|
|
1116
|
+
// Schedule on next runloop cycle
|
|
1117
|
+
[self performSelector:@selector(_performRelayout) withObject:nil afterDelay:0];
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
- (void)_performRelayout
|
|
1121
|
+
{
|
|
1122
|
+
if (!textView) { return; }
|
|
1123
|
+
|
|
1124
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1125
|
+
NSRange wholeRange = NSMakeRange(0, self->textView.textStorage.string.length);
|
|
1126
|
+
NSRange actualRange = NSMakeRange(0, 0);
|
|
1127
|
+
[self->textView.layoutManager invalidateLayoutForCharacterRange:wholeRange actualCharacterRange:&actualRange];
|
|
1128
|
+
[self->textView.layoutManager ensureLayoutForCharacterRange:actualRange];
|
|
1129
|
+
[self->textView.layoutManager invalidateDisplayForCharacterRange:wholeRange];
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
- (void)didMoveToWindow {
|
|
1134
|
+
[super didMoveToWindow];
|
|
1135
|
+
// used to run all lifecycle callbacks
|
|
1136
|
+
[self anyTextMayHaveBeenModified];
|
|
1098
1137
|
}
|
|
1099
1138
|
|
|
1100
1139
|
// MARK: - UITextView delegate methods
|
|
@@ -1179,9 +1218,6 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
1179
1218
|
|
|
1180
1219
|
// manage selection changes
|
|
1181
1220
|
[self manageSelectionBasedChanges];
|
|
1182
|
-
|
|
1183
|
-
// update active styles
|
|
1184
|
-
[self tryUpdatingActiveStyles];
|
|
1185
1221
|
}
|
|
1186
1222
|
|
|
1187
1223
|
// this function isn't called always when some text changes (for example setting link or starting mention with indicator doesn't fire it)
|