Skip to content

Commit 978fbde

Browse files
Merge branch 'main' into add-codeblock
2 parents 42dcf41 + 0db5cfe commit 978fbde

File tree

12 files changed

+161
-49
lines changed

12 files changed

+161
-49
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,11 @@ export default function App() {
101101
<View style={styles.container}>
102102
<EnrichedTextInput
103103
ref={ref}
104-
onChangeState={(e) => setStylesState(e.nativeEvent)}
104+
onChangeState={e => setStylesState(e.nativeEvent)}
105105
style={styles.input}
106106
/>
107107
<Button
108-
title="Toggle bold"
108+
title={stylesState?.isBold ? 'Unbold' : 'Bold'}
109109
color={stylesState?.isBold ? 'green' : 'gray'}
110110
onPress={() => ref.current?.toggleBold()}
111111
/>

android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
279279
heightMode: YogaMeasureMode?,
280280
attachmentsPositions: FloatArray?
281281
): Long {
282-
return MeasurementStore.getMeasureById(localData?.getInt("viewTag"), width)
282+
val id = localData?.getInt("viewTag")
283+
return MeasurementStore.getMeasureById(context, id, width, props)
283284
}
284285

285286
companion object {

android/src/main/java/com/swmansion/enriched/MeasurementStore.kt

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
package com.swmansion.enriched
22

3+
import android.content.Context
34
import android.graphics.Typeface
45
import android.graphics.text.LineBreaker
56
import android.os.Build
67
import android.text.Spannable
78
import android.text.StaticLayout
89
import android.text.TextPaint
10+
import android.util.Log
11+
import com.facebook.react.bridge.ReadableMap
912
import com.facebook.react.uimanager.PixelUtil
13+
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
14+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
15+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
1016
import com.facebook.yoga.YogaMeasureOutput
17+
import com.swmansion.enriched.styles.HtmlStyle
18+
import com.swmansion.enriched.utils.EnrichedParser
1119
import java.util.concurrent.ConcurrentHashMap
20+
import kotlin.math.ceil
1221

1322
object MeasurementStore {
1423
data class PaintParams(
@@ -17,10 +26,12 @@ object MeasurementStore {
1726
)
1827

1928
data class MeasurementParams(
29+
val initialized: Boolean,
30+
2031
val cachedWidth: Float,
2132
val cachedSize: Long,
2233

23-
val spannable: Spannable?,
34+
val spannable: CharSequence?,
2435
val paintParams: PaintParams,
2536
)
2637

@@ -29,18 +40,20 @@ object MeasurementStore {
2940
fun store(id: Int, spannable: Spannable?, paint: TextPaint): Boolean {
3041
val cachedWidth = data[id]?.cachedWidth ?: 0f
3142
val cachedSize = data[id]?.cachedSize ?: 0L
43+
val initialized = data[id]?.initialized ?: true
44+
3245
val size = measure(cachedWidth, spannable, paint)
3346
val paintParams = PaintParams(paint.typeface, paint.textSize)
3447

35-
data[id] = MeasurementParams(cachedWidth, size, spannable, paintParams)
48+
data[id] = MeasurementParams(initialized, cachedWidth, size, spannable, paintParams)
3649
return cachedSize != size
3750
}
3851

3952
fun release(id: Int) {
4053
data.remove(id)
4154
}
4255

43-
fun measure(maxWidth: Float, spannable: Spannable?, paintParams: PaintParams): Long {
56+
private fun measure(maxWidth: Float, spannable: CharSequence?, paintParams: PaintParams): Long {
4457
val paint = TextPaint().apply {
4558
typeface = paintParams.typeface
4659
textSize = paintParams.fontSize
@@ -49,7 +62,7 @@ object MeasurementStore {
4962
return measure(maxWidth, spannable, paint)
5063
}
5164

52-
fun measure(maxWidth: Float, spannable: Spannable?, paint: TextPaint): Long {
65+
private fun measure(maxWidth: Float, spannable: CharSequence?, paint: TextPaint): Long {
5366
val text = spannable ?: ""
5467
val textLength = text.length
5568
val builder = StaticLayout.Builder
@@ -71,9 +84,63 @@ object MeasurementStore {
7184
return YogaMeasureOutput.make(widthInSP, heightInSP)
7285
}
7386

74-
fun getMeasureById(id: Int?, width: Float): Long {
75-
val id = id ?: return YogaMeasureOutput.make(0, 0)
76-
val value = data[id] ?: return YogaMeasureOutput.make(0, 0)
87+
// Returns either: Spannable parsed from HTML defaultValue, or plain text defaultValue, or "I" if no defaultValue
88+
private fun getInitialText(defaultView: EnrichedTextInputView, props: ReadableMap?): CharSequence {
89+
val defaultValue = props?.getString("defaultValue")
90+
91+
// If there is no default value, assume text is one line, "I" is a good approximation of height
92+
if (defaultValue == null) return "I"
93+
94+
val isHtml = defaultValue.startsWith("<html>") && defaultValue.endsWith("</html>")
95+
if (!isHtml) return defaultValue
96+
97+
try {
98+
val htmlStyle = HtmlStyle(defaultView, props.getMap("htmlStyle"))
99+
val parsed = EnrichedParser.fromHtml(defaultValue, htmlStyle, null)
100+
return parsed.trimEnd('\n')
101+
} catch (e: Exception) {
102+
Log.w("MeasurementStore", "Error parsing initial HTML text: ${e.message}")
103+
return defaultValue
104+
}
105+
}
106+
107+
private fun getInitialFontSize(defaultView: EnrichedTextInputView, props: ReadableMap?): Float {
108+
val propsFontSize = props?.getDouble("fontSize")?.toFloat()
109+
if (propsFontSize == null) return defaultView.textSize
110+
111+
return ceil(PixelUtil.toPixelFromSP(propsFontSize))
112+
}
113+
114+
// Called when view measurements are not available in the store
115+
// Most likely first measurement, we can use defaultValue, as no native state is set yet
116+
private fun initialMeasure(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
117+
val defaultView = EnrichedTextInputView(context)
118+
119+
val text = getInitialText(defaultView, props)
120+
val fontSize = getInitialFontSize(defaultView, props)
121+
122+
val fontFamily = props?.getString("fontFamily")
123+
val fontStyle = parseFontStyle(props?.getString("fontStyle"))
124+
val fontWeight = parseFontWeight(props?.getString("fontWeight"))
125+
126+
val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
127+
val paintParams = PaintParams(typeface, fontSize)
128+
val size = measure(width, text, PaintParams(typeface, fontSize))
129+
130+
if (id != null) {
131+
data[id] = MeasurementParams(true, width, size, text, paintParams)
132+
}
133+
134+
return size
135+
}
136+
137+
fun getMeasureById(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
138+
val id = id ?: return initialMeasure(context, id, width, props)
139+
val value = data[id] ?: return initialMeasure(context, id, width, props)
140+
141+
// First measure has to be done using initialMeasure
142+
// That way it's free of any side effects and async initializations
143+
if (!value.initialized) return initialMeasure(context, id, width, props)
77144

78145
if (width == value.cachedWidth) {
79146
return value.cachedSize
@@ -83,8 +150,9 @@ object MeasurementStore {
83150
typeface = value.paintParams.typeface
84151
textSize = value.paintParams.fontSize
85152
}
153+
86154
val size = measure(width, value.spannable, paint)
87-
data[id] = MeasurementParams(width, size, value.spannable, value.paintParams)
155+
data[id] = MeasurementParams(true, width, size, value.spannable, value.paintParams)
88156
return size
89157
}
90158
}

android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "EnrichedTextInputMeasurementManager.h"
2+
#include "conversions.h"
23

34
#include <fbjni/fbjni.h>
45
#include <react/jni/ReadableNativeMap.h>
@@ -11,6 +12,7 @@ namespace facebook::react {
1112
Size EnrichedTextInputMeasurementManager::measure(
1213
SurfaceId surfaceId,
1314
int viewTag,
15+
const EnrichedTextInputViewProps& props,
1416
LayoutConstraints layoutConstraints) const {
1517
const jni::global_ref<jobject>& fabricUIManager =
1618
contextContainer_->at<jni::global_ref<jobject>>("FabricUIManager");
@@ -33,17 +35,23 @@ namespace facebook::react {
3335

3436
local_ref<JString> componentName = make_jstring("EnrichedTextInputView");
3537

36-
folly::dynamic extra = folly::dynamic::object();
37-
extra["viewTag"] = viewTag;
38-
local_ref<ReadableNativeMap::javaobject> extraData = ReadableNativeMap::newObjectCxxArgs(extra);
39-
local_ref<ReadableMap::javaobject> extraDataRM = make_local(reinterpret_cast<ReadableMap::javaobject>(extraData.get()));
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()));
4048

4149
auto measurement = yogaMeassureToSize(measure(
4250
fabricUIManager,
4351
surfaceId,
4452
componentName.get(),
4553
extraDataRM.get(),
46-
nullptr,
54+
propsRM.get(),
4755
nullptr,
4856
minimumSize.width,
4957
maximumSize.width,

android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace facebook::react {
1717
Size measure(
1818
SurfaceId surfaceId,
1919
int viewTag,
20+
const EnrichedTextInputViewProps& props,
2021
LayoutConstraints layoutConstraints) const;
2122

2223
private:

android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extern const char EnrichedTextInputComponentName[] = "EnrichedTextInputView";
2727
Size EnrichedTextInputShadowNode::measureContent(
2828
const LayoutContext &layoutContext,
2929
const LayoutConstraints &layoutConstraints) const {
30-
return measurementsManager_->measure(getSurfaceId(), getTag(), layoutConstraints);
30+
return measurementsManager_->measure(getSurfaceId(), getTag(), getConcreteProps(), layoutConstraints);
3131
}
3232

3333
} // namespace facebook::react
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2349,7 +2349,7 @@ PODS:
23492349
- React-perflogger (= 0.82.1)
23502350
- React-utils (= 0.82.1)
23512351
- SocketRocket
2352-
- ReactNativeEnriched (0.1.5):
2352+
- ReactNativeEnriched (0.1.6):
23532353
- boost
23542354
- DoubleConversion
23552355
- fast_float
@@ -2690,7 +2690,7 @@ SPEC CHECKSUMS:
26902690
ReactAppDependencyProvider: cc2795efe30a023c3a505676b9c748b664b9c0a1
26912691
ReactCodegen: 897bad2d2f722ff4dc46fc144f9cc018db0e2ce4
26922692
ReactCommon: c5803af00bd3737dc1631749b1f1da5beba5b049
2693-
ReactNativeEnriched: 9b9a2496bd301eca5df9a5c1ea208033d4d25b55
2693+
ReactNativeEnriched: c96634c617190d2cd94f9a474d68430dfc8d6dc0
26942694
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
26952695
Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb
26962696

ios/EnrichedTextInputView.mm

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ - (void)setDefaults {
7373
_recentlyActiveMentionRange = NSMakeRange(0, 0);
7474
recentlyChangedRange = NSMakeRange(0, 0);
7575
_recentlyEmittedString = @"";
76-
_recentlyEmittedHtml = @"";
76+
_recentlyEmittedHtml = @"<html>\n<p></p>\n</html>";
7777
_emitHtml = NO;
7878
blockEmitting = NO;
7979
_emitFocusBlur = YES;
@@ -731,9 +731,6 @@ - (void)tryUpdatingActiveStyles {
731731
if(detectedLinkData != nullptr) {
732732
// emit onLinkeDetected event
733733
[self emitOnLinkDetectedEvent:detectedLinkData.text url:detectedLinkData.url range:detectedLinkRange];
734-
735-
_recentlyActiveLinkData = detectedLinkData;
736-
_recentlyActiveLinkRange = detectedLinkRange;
737734
}
738735

739736
if(detectedMentionParams != nullptr) {
@@ -834,6 +831,13 @@ - (void)setValue:(NSString *)value {
834831
- (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url range:(NSRange)range {
835832
auto emitter = [self getEventEmitter];
836833
if(emitter != nullptr) {
834+
// update recently active link info
835+
LinkData *newLinkData = [[LinkData alloc] init];
836+
newLinkData.text = text;
837+
newLinkData.url = url;
838+
_recentlyActiveLinkData = newLinkData;
839+
_recentlyActiveLinkRange = range;
840+
837841
emitter->onLinkDetected({
838842
.text = [text toCppString],
839843
.url = [url toCppString],
@@ -1081,10 +1085,10 @@ - (void)anyTextMayHaveBeenModified {
10811085
[h3Style handleImproperHeadings];
10821086
}
10831087

1084-
// manage mention editing also here
1085-
MentionStyle *mentionStyle = stylesDict[@([MentionStyle getStyleType])];
1086-
if(mentionStyle != nullptr) {
1087-
[mentionStyle manageMentionEditing];
1088+
// mentions removal management
1089+
MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
1090+
if(mentionStyleClass != nullptr) {
1091+
[mentionStyleClass handleExistingMentions];
10881092
}
10891093

10901094
// placholder management
@@ -1095,12 +1099,6 @@ - (void)anyTextMayHaveBeenModified {
10951099
}
10961100

10971101
if(![textView.textStorage.string isEqualToString:_recentlyEmittedString]) {
1098-
// mentions removal management
1099-
MentionStyle *mentionStyleClass = (MentionStyle *)stylesDict[@([MentionStyle getStyleType])];
1100-
if(mentionStyleClass != nullptr) {
1101-
[mentionStyleClass handleExistingMentions];
1102-
}
1103-
11041102
// modified words handling
11051103
NSArray *modifiedWords = [WordsUtils getAffectedWordsFromText:textView.textStorage.string modificationRange:recentlyChangedRange];
11061104
if(modifiedWords != nullptr) {
@@ -1122,13 +1120,13 @@ - (void)anyTextMayHaveBeenModified {
11221120
// emit onChangeText event
11231121
auto emitter = [self getEventEmitter];
11241122
if(emitter != nullptr) {
1123+
// set the recently emitted string only if the emitter is defined
1124+
_recentlyEmittedString = stringToBeEmitted;
1125+
11251126
emitter->onChangeText({
11261127
.value = [stringToBeEmitted toCppString]
11271128
});
11281129
}
1129-
1130-
// set the recently emitted string
1131-
_recentlyEmittedString = stringToBeEmitted;
11321130
}
11331131

11341132
// update height on each character change

ios/inputParser/InputParser.mm

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -631,9 +631,21 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml {
631631
} else if([tagName isEqualToString:@"code"]) {
632632
[styleArr addObject:@([InlineCodeStyle getStyleType])];
633633
} else if([tagName isEqualToString:@"a"]) {
634+
NSRegularExpression *hrefRegex = [NSRegularExpression regularExpressionWithPattern:@"href=\".+\""
635+
options:0
636+
error:nullptr
637+
];
638+
NSTextCheckingResult* match = [hrefRegex firstMatchInString:params options:0 range: NSMakeRange(0, params.length)];
639+
640+
if(match == nullptr) {
641+
// same as on Android, no href (or empty href) equals no link style
642+
continue;
643+
}
644+
645+
NSRange hrefRange = match.range;
634646
[styleArr addObject:@([LinkStyle getStyleType])];
635647
// cut only the url from the href="..." string
636-
NSString *url = [params substringWithRange:NSMakeRange(6, params.length - 7)];
648+
NSString *url = [params substringWithRange:NSMakeRange(hrefRange.location + 6, hrefRange.length - 7)];
637649
stylePair.styleValue = url;
638650
} else if([tagName isEqualToString:@"mention"]) {
639651
[styleArr addObject:@([MentionStyle getStyleType])];
@@ -680,7 +692,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml {
680692
} else if([tagName isEqualToString:@"codeblock"]) {
681693
[styleArr addObject:@([CodeBlockStyle getStyleType])];
682694
} else {
683-
// some other external tags like span just don't get put into the processed styles
695+
// some other external tags like span just don't get put into the processed styles
684696
continue;
685697
}
686698

0 commit comments

Comments
 (0)