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
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<img src="https://github.com/user-attachments/assets/
|
|
1
|
+
<img src="https://github.com/user-attachments/assets/b010571e-e4a3-4d92-a409-4f9fe614025d" alt="react-native-enriched by Software Mansion" width="100%">
|
|
2
2
|
|
|
3
3
|
# react-native-enriched
|
|
4
4
|
|
|
@@ -101,11 +101,11 @@ export default function App() {
|
|
|
101
101
|
<View style={styles.container}>
|
|
102
102
|
<EnrichedTextInput
|
|
103
103
|
ref={ref}
|
|
104
|
-
onChangeState={
|
|
104
|
+
onChangeState={e => setStylesState(e.nativeEvent)}
|
|
105
105
|
style={styles.input}
|
|
106
106
|
/>
|
|
107
107
|
<Button
|
|
108
|
-
title=
|
|
108
|
+
title={stylesState?.isBold ? 'Unbold' : 'Bold'}
|
|
109
109
|
color={stylesState?.isBold ? 'green' : 'gray'}
|
|
110
110
|
onPress={() => ref.current?.toggleBold()}
|
|
111
111
|
/>
|
|
@@ -152,9 +152,6 @@ Supported styles:
|
|
|
152
152
|
- ordered list
|
|
153
153
|
- unordered list
|
|
154
154
|
|
|
155
|
-
> [!NOTE]
|
|
156
|
-
> The iOS doesn't support codeblocks just yet, but it's planned in the near future!
|
|
157
|
-
|
|
158
155
|
Each of the styles can be toggled the same way as in the example from [usage section](#usage); call a proper `toggle` function on the component ref.
|
|
159
156
|
|
|
160
157
|
Each call toggles the style within the current text selection. We can still divide styles into two categories based on how they treat the selection:
|
|
@@ -212,9 +209,6 @@ You can insert an image into the input using [setImage](docs/API_REFERENCE.md#se
|
|
|
212
209
|
|
|
213
210
|
The image will be put into a single line in the input and will affect the line's height as well as input's height. Keep in mind, that image will replace currently selected text or insert into the cursor position if there is no text selection.
|
|
214
211
|
|
|
215
|
-
> [!NOTE]
|
|
216
|
-
> The iOS doesn't support inline images just yet, but it's planned in the near future!
|
|
217
|
-
|
|
218
212
|
## Style Detection
|
|
219
213
|
|
|
220
214
|
All of the above styles can be detected with the use of [onChangeState](docs/API_REFERENCE.md#onchangestate) event payload.
|
|
@@ -55,6 +55,9 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
|
|
|
55
55
|
case "htmlStyle":
|
|
56
56
|
mViewManager.setHtmlStyle(view, (ReadableMap) value);
|
|
57
57
|
break;
|
|
58
|
+
case "scrollEnabled":
|
|
59
|
+
mViewManager.setScrollEnabled(view, value == null ? false : (boolean) value);
|
|
60
|
+
break;
|
|
58
61
|
case "color":
|
|
59
62
|
mViewManager.setColor(view, ColorPropConverter.getColor(value, view.getContext()));
|
|
60
63
|
break;
|
|
@@ -133,7 +136,7 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
|
|
|
133
136
|
mViewManager.addLink(view, args.getInt(0), args.getInt(1), args.getString(2), args.getString(3));
|
|
134
137
|
break;
|
|
135
138
|
case "addImage":
|
|
136
|
-
mViewManager.addImage(view, args.getString(0));
|
|
139
|
+
mViewManager.addImage(view, args.getString(0), (float) args.getDouble(1), (float) args.getDouble(2));
|
|
137
140
|
break;
|
|
138
141
|
case "startMention":
|
|
139
142
|
mViewManager.startMention(view, args.getString(0));
|
|
@@ -26,6 +26,7 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
|
|
|
26
26
|
void setSelectionColor(T view, @Nullable Integer value);
|
|
27
27
|
void setAutoCapitalize(T view, @Nullable String value);
|
|
28
28
|
void setHtmlStyle(T view, @Nullable ReadableMap value);
|
|
29
|
+
void setScrollEnabled(T view, boolean value);
|
|
29
30
|
void setColor(T view, @Nullable Integer value);
|
|
30
31
|
void setFontSize(T view, float value);
|
|
31
32
|
void setFontFamily(T view, @Nullable String value);
|
|
@@ -49,7 +50,7 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
|
|
|
49
50
|
void toggleOrderedList(T view);
|
|
50
51
|
void toggleUnorderedList(T view);
|
|
51
52
|
void addLink(T view, int start, int end, String text, String url);
|
|
52
|
-
void addImage(T view, String uri);
|
|
53
|
+
void addImage(T view, String uri, float width, float height);
|
|
53
54
|
void startMention(T view, String indicator);
|
|
54
55
|
void addMention(T view, String indicator, String text, String payload);
|
|
55
56
|
}
|
package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp
CHANGED
|
@@ -30,6 +30,7 @@ EnrichedTextInputViewProps::EnrichedTextInputViewProps(
|
|
|
30
30
|
selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
|
|
31
31
|
autoCapitalize(convertRawProp(context, rawProps, "autoCapitalize", sourceProps.autoCapitalize, {})),
|
|
32
32
|
htmlStyle(convertRawProp(context, rawProps, "htmlStyle", sourceProps.htmlStyle, {})),
|
|
33
|
+
scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {false})),
|
|
33
34
|
color(convertRawProp(context, rawProps, "color", sourceProps.color, {})),
|
|
34
35
|
fontSize(convertRawProp(context, rawProps, "fontSize", sourceProps.fontSize, {0.0})),
|
|
35
36
|
fontFamily(convertRawProp(context, rawProps, "fontFamily", sourceProps.fontFamily, {})),
|
|
@@ -94,6 +95,10 @@ folly::dynamic EnrichedTextInputViewProps::getDiffProps(
|
|
|
94
95
|
result["htmlStyle"] = toDynamic(htmlStyle);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
if (scrollEnabled != oldProps->scrollEnabled) {
|
|
99
|
+
result["scrollEnabled"] = scrollEnabled;
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
if (color != oldProps->color) {
|
|
98
103
|
result["color"] = *color;
|
|
99
104
|
}
|
|
@@ -309,45 +309,6 @@ static inline folly::dynamic toDynamic(const EnrichedTextInputViewHtmlStyleAStru
|
|
|
309
309
|
}
|
|
310
310
|
#endif
|
|
311
311
|
|
|
312
|
-
struct EnrichedTextInputViewHtmlStyleImgStruct {
|
|
313
|
-
Float width{0.0};
|
|
314
|
-
Float height{0.0};
|
|
315
|
-
|
|
316
|
-
#ifdef RN_SERIALIZABLE_STATE
|
|
317
|
-
bool operator==(const EnrichedTextInputViewHtmlStyleImgStruct&) const = default;
|
|
318
|
-
|
|
319
|
-
folly::dynamic toDynamic() const {
|
|
320
|
-
folly::dynamic result = folly::dynamic::object();
|
|
321
|
-
result["width"] = width;
|
|
322
|
-
result["height"] = height;
|
|
323
|
-
return result;
|
|
324
|
-
}
|
|
325
|
-
#endif
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
static inline void fromRawValue(const PropsParserContext& context, const RawValue &value, EnrichedTextInputViewHtmlStyleImgStruct &result) {
|
|
329
|
-
auto map = (std::unordered_map<std::string, RawValue>)value;
|
|
330
|
-
|
|
331
|
-
auto tmp_width = map.find("width");
|
|
332
|
-
if (tmp_width != map.end()) {
|
|
333
|
-
fromRawValue(context, tmp_width->second, result.width);
|
|
334
|
-
}
|
|
335
|
-
auto tmp_height = map.find("height");
|
|
336
|
-
if (tmp_height != map.end()) {
|
|
337
|
-
fromRawValue(context, tmp_height->second, result.height);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
static inline std::string toString(const EnrichedTextInputViewHtmlStyleImgStruct &value) {
|
|
342
|
-
return "[Object EnrichedTextInputViewHtmlStyleImgStruct]";
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
#ifdef RN_SERIALIZABLE_STATE
|
|
346
|
-
static inline folly::dynamic toDynamic(const EnrichedTextInputViewHtmlStyleImgStruct &value) {
|
|
347
|
-
return value.toDynamic();
|
|
348
|
-
}
|
|
349
|
-
#endif
|
|
350
|
-
|
|
351
312
|
struct EnrichedTextInputViewHtmlStyleOlStruct {
|
|
352
313
|
Float gapWidth{0.0};
|
|
353
314
|
Float marginLeft{0.0};
|
|
@@ -459,7 +420,6 @@ struct EnrichedTextInputViewHtmlStyleStruct {
|
|
|
459
420
|
EnrichedTextInputViewHtmlStyleCodeStruct code{};
|
|
460
421
|
EnrichedTextInputViewHtmlStyleAStruct a{};
|
|
461
422
|
folly::dynamic mention{};
|
|
462
|
-
EnrichedTextInputViewHtmlStyleImgStruct img{};
|
|
463
423
|
EnrichedTextInputViewHtmlStyleOlStruct ol{};
|
|
464
424
|
EnrichedTextInputViewHtmlStyleUlStruct ul{};
|
|
465
425
|
|
|
@@ -476,7 +436,6 @@ struct EnrichedTextInputViewHtmlStyleStruct {
|
|
|
476
436
|
result["code"] = ::facebook::react::toDynamic(code);
|
|
477
437
|
result["a"] = ::facebook::react::toDynamic(a);
|
|
478
438
|
result["mention"] = mention;
|
|
479
|
-
result["img"] = ::facebook::react::toDynamic(img);
|
|
480
439
|
result["ol"] = ::facebook::react::toDynamic(ol);
|
|
481
440
|
result["ul"] = ::facebook::react::toDynamic(ul);
|
|
482
441
|
return result;
|
|
@@ -519,10 +478,6 @@ static inline void fromRawValue(const PropsParserContext& context, const RawValu
|
|
|
519
478
|
if (tmp_mention != map.end()) {
|
|
520
479
|
fromRawValue(context, tmp_mention->second, result.mention);
|
|
521
480
|
}
|
|
522
|
-
auto tmp_img = map.find("img");
|
|
523
|
-
if (tmp_img != map.end()) {
|
|
524
|
-
fromRawValue(context, tmp_img->second, result.img);
|
|
525
|
-
}
|
|
526
481
|
auto tmp_ol = map.find("ol");
|
|
527
482
|
if (tmp_ol != map.end()) {
|
|
528
483
|
fromRawValue(context, tmp_ol->second, result.ol);
|
|
@@ -559,6 +514,7 @@ class EnrichedTextInputViewProps final : public ViewProps {
|
|
|
559
514
|
SharedColor selectionColor{};
|
|
560
515
|
std::string autoCapitalize{};
|
|
561
516
|
EnrichedTextInputViewHtmlStyleStruct htmlStyle{};
|
|
517
|
+
bool scrollEnabled{false};
|
|
562
518
|
SharedColor color{};
|
|
563
519
|
Float fontSize{0.0};
|
|
564
520
|
std::string fontFamily{};
|
|
@@ -29,6 +29,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
|
|
|
29
29
|
import com.swmansion.enriched.events.MentionHandler
|
|
30
30
|
import com.swmansion.enriched.events.OnInputBlurEvent
|
|
31
31
|
import com.swmansion.enriched.events.OnInputFocusEvent
|
|
32
|
+
import com.swmansion.enriched.spans.EnrichedImageSpan
|
|
32
33
|
import com.swmansion.enriched.spans.EnrichedSpans
|
|
33
34
|
import com.swmansion.enriched.styles.InlineStyles
|
|
34
35
|
import com.swmansion.enriched.styles.ListStyles
|
|
@@ -54,12 +55,14 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
54
55
|
val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
|
|
55
56
|
var isDuringTransaction: Boolean = false
|
|
56
57
|
var isRemovingMany: Boolean = false
|
|
58
|
+
var scrollEnabled: Boolean = true
|
|
57
59
|
|
|
58
60
|
val mentionHandler: MentionHandler? = MentionHandler(this)
|
|
59
61
|
var htmlStyle: HtmlStyle = HtmlStyle(this, null)
|
|
60
62
|
var spanWatcher: EnrichedSpanWatcher? = null
|
|
61
63
|
var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
|
|
62
64
|
|
|
65
|
+
var shouldEmitHtml: Boolean = true
|
|
63
66
|
var experimentalSynchronousEvents: Boolean = false
|
|
64
67
|
|
|
65
68
|
var fontSize: Float? = null
|
|
@@ -70,6 +73,8 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
70
73
|
private var fontFamily: String? = null
|
|
71
74
|
private var fontStyle: Int = ReactConstants.UNSET
|
|
72
75
|
private var fontWeight: Int = ReactConstants.UNSET
|
|
76
|
+
private var defaultValue: CharSequence? = null
|
|
77
|
+
private var defaultValueDirty: Boolean = false
|
|
73
78
|
|
|
74
79
|
private var inputMethodManager: InputMethodManager? = null
|
|
75
80
|
|
|
@@ -137,6 +142,14 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
137
142
|
return super.onTouchEvent(ev)
|
|
138
143
|
}
|
|
139
144
|
|
|
145
|
+
override fun canScrollVertically(direction: Int): Boolean {
|
|
146
|
+
return scrollEnabled
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override fun canScrollHorizontally(direction: Int): Boolean {
|
|
150
|
+
return scrollEnabled
|
|
151
|
+
}
|
|
152
|
+
|
|
140
153
|
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
|
141
154
|
super.onSelectionChanged(selStart, selEnd)
|
|
142
155
|
selection?.onSelection(selStart, selEnd)
|
|
@@ -243,6 +256,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
243
256
|
val newText = parseText(value)
|
|
244
257
|
setText(newText)
|
|
245
258
|
|
|
259
|
+
observeAsyncImages()
|
|
246
260
|
// Assign SpanWatcher one more time as our previous spannable has been replaced
|
|
247
261
|
addSpanWatcher(EnrichedSpanWatcher(this))
|
|
248
262
|
|
|
@@ -251,6 +265,20 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
251
265
|
}
|
|
252
266
|
}
|
|
253
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Finds all async images in the current text and sets up listeners
|
|
270
|
+
* to redraw the text layout when they finish downloading.
|
|
271
|
+
*/
|
|
272
|
+
private fun observeAsyncImages() {
|
|
273
|
+
val liveText = text ?: return
|
|
274
|
+
|
|
275
|
+
val spans = liveText.getSpans(0, liveText.length, EnrichedImageSpan::class.java)
|
|
276
|
+
|
|
277
|
+
for (span in spans) {
|
|
278
|
+
span.observeAsyncDrawableLoaded(liveText)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
254
282
|
fun setAutoFocus(autoFocus: Boolean) {
|
|
255
283
|
this.autoFocus = autoFocus
|
|
256
284
|
}
|
|
@@ -306,7 +334,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
306
334
|
|
|
307
335
|
// This ensured that newly created spans will take the new font size into account
|
|
308
336
|
htmlStyle.invalidateStyles()
|
|
309
|
-
layoutManager.invalidateLayout(
|
|
337
|
+
layoutManager.invalidateLayout()
|
|
310
338
|
}
|
|
311
339
|
|
|
312
340
|
fun setFontFamily(family: String?) {
|
|
@@ -360,7 +388,24 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
360
388
|
return false
|
|
361
389
|
}
|
|
362
390
|
|
|
363
|
-
fun
|
|
391
|
+
fun afterUpdateTransaction() {
|
|
392
|
+
updateTypeface()
|
|
393
|
+
updateDefaultValue()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fun setDefaultValue(value: CharSequence?) {
|
|
397
|
+
defaultValue = value
|
|
398
|
+
defaultValueDirty = true
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private fun updateDefaultValue() {
|
|
402
|
+
if (!defaultValueDirty) return
|
|
403
|
+
|
|
404
|
+
defaultValueDirty = false
|
|
405
|
+
setValue(defaultValue ?: "")
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private fun updateTypeface() {
|
|
364
409
|
if (!typefaceDirty) return
|
|
365
410
|
typefaceDirty = false
|
|
366
411
|
|
|
@@ -368,7 +413,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
368
413
|
typeface = newTypeface
|
|
369
414
|
paint.typeface = newTypeface
|
|
370
415
|
|
|
371
|
-
layoutManager.invalidateLayout(
|
|
416
|
+
layoutManager.invalidateLayout()
|
|
372
417
|
}
|
|
373
418
|
|
|
374
419
|
private fun toggleStyle(name: String) {
|
|
@@ -388,7 +433,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
388
433
|
else -> Log.w("EnrichedTextInputView", "Unknown style: $name")
|
|
389
434
|
}
|
|
390
435
|
|
|
391
|
-
layoutManager.invalidateLayout(
|
|
436
|
+
layoutManager.invalidateLayout()
|
|
392
437
|
}
|
|
393
438
|
|
|
394
439
|
private fun removeStyle(name: String, start: Int, end: Int): Boolean {
|
|
@@ -438,7 +483,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
438
483
|
}
|
|
439
484
|
|
|
440
485
|
private fun verifyStyle(name: String): Boolean {
|
|
441
|
-
val mergingConfig = EnrichedSpans.
|
|
486
|
+
val mergingConfig = EnrichedSpans.getMergingConfigForStyle(name, htmlStyle) ?: return true
|
|
442
487
|
val conflictingStyles = mergingConfig.conflictingStyles
|
|
443
488
|
val blockingStyles = mergingConfig.blockingStyles
|
|
444
489
|
val isEnabling = spanState?.getStart(name) == null
|
|
@@ -499,11 +544,12 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
499
544
|
parametrizedStyles?.setLinkSpan(start, end, text, url)
|
|
500
545
|
}
|
|
501
546
|
|
|
502
|
-
fun addImage(src: String) {
|
|
547
|
+
fun addImage(src: String, width: Float, height: Float) {
|
|
503
548
|
val isValid = verifyStyle(EnrichedSpans.IMAGE)
|
|
504
549
|
if (!isValid) return
|
|
505
550
|
|
|
506
|
-
parametrizedStyles?.setImageSpan(src)
|
|
551
|
+
parametrizedStyles?.setImageSpan(src, width, height)
|
|
552
|
+
layoutManager.invalidateLayout()
|
|
507
553
|
}
|
|
508
554
|
|
|
509
555
|
fun startMention(indicator: String) {
|
|
@@ -542,11 +588,6 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
542
588
|
didAttachToWindow = true
|
|
543
589
|
}
|
|
544
590
|
|
|
545
|
-
override fun onDetachedFromWindow() {
|
|
546
|
-
layoutManager.cleanup()
|
|
547
|
-
super.onDetachedFromWindow()
|
|
548
|
-
}
|
|
549
|
-
|
|
550
591
|
companion object {
|
|
551
592
|
const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
|
|
552
593
|
}
|
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
package com.swmansion.enriched
|
|
2
2
|
|
|
3
|
-
import android.graphics.text.LineBreaker
|
|
4
|
-
import android.os.Build
|
|
5
|
-
import android.text.Editable
|
|
6
|
-
import android.text.StaticLayout
|
|
7
3
|
import com.facebook.react.bridge.Arguments
|
|
8
|
-
import com.facebook.react.uimanager.PixelUtil
|
|
9
4
|
|
|
10
5
|
class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView) {
|
|
11
|
-
private var cachedSize: Pair<Float, Float> = Pair(0f, 0f)
|
|
12
|
-
private var cachedYogaWidth: Float = 0f
|
|
13
6
|
private var forceHeightRecalculationCounter: Int = 0
|
|
14
7
|
|
|
15
|
-
fun
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
fun invalidateLayout() {
|
|
9
|
+
val text = view.text
|
|
10
|
+
val paint = view.paint
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
measureSize(text ?: "")
|
|
12
|
+
val needUpdate = MeasurementStore.store(view.id, text, paint)
|
|
13
|
+
if (!needUpdate) return
|
|
22
14
|
|
|
23
15
|
val counter = forceHeightRecalculationCounter
|
|
24
16
|
forceHeightRecalculationCounter++
|
|
@@ -27,48 +19,7 @@ class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView
|
|
|
27
19
|
view.stateWrapper?.updateState(state)
|
|
28
20
|
}
|
|
29
21
|
|
|
30
|
-
fun
|
|
31
|
-
|
|
32
|
-
return cachedSize
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
val text = view.text ?: ""
|
|
36
|
-
val result = measureAndCacheSize(text, maxWidth)
|
|
37
|
-
cachedYogaWidth = maxWidth
|
|
38
|
-
return result
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
fun measureSize(text: CharSequence): Pair<Float, Float> {
|
|
42
|
-
return measureAndCacheSize(text, cachedYogaWidth)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private fun measureAndCacheSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
|
|
46
|
-
val result = measureSize(text, maxWidth)
|
|
47
|
-
cachedSize = result
|
|
48
|
-
return result
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private fun measureSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
|
|
52
|
-
val paint = view.paint
|
|
53
|
-
val textLength = text.length
|
|
54
|
-
|
|
55
|
-
val builder = StaticLayout.Builder
|
|
56
|
-
.obtain(text, 0, textLength, paint, maxWidth.toInt())
|
|
57
|
-
.setIncludePad(true)
|
|
58
|
-
.setLineSpacing(0f, 1f)
|
|
59
|
-
|
|
60
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
61
|
-
builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
65
|
-
builder.setUseLineSpacingFromFallbacks(true)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
val staticLayout = builder.build()
|
|
69
|
-
val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
|
|
70
|
-
val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
|
|
71
|
-
|
|
72
|
-
return Pair(widthInSP, heightInSP)
|
|
22
|
+
fun releaseMeasurementStore() {
|
|
23
|
+
MeasurementStore.release(view.id)
|
|
73
24
|
}
|
|
74
25
|
}
|
|
@@ -15,7 +15,6 @@ import com.facebook.react.uimanager.annotations.ReactProp
|
|
|
15
15
|
import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerDelegate
|
|
16
16
|
import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerInterface
|
|
17
17
|
import com.facebook.yoga.YogaMeasureMode
|
|
18
|
-
import com.facebook.yoga.YogaMeasureOutput
|
|
19
18
|
import com.swmansion.enriched.events.OnInputBlurEvent
|
|
20
19
|
import com.swmansion.enriched.events.OnChangeHtmlEvent
|
|
21
20
|
import com.swmansion.enriched.events.OnChangeSelectionEvent
|
|
@@ -32,12 +31,8 @@ import com.swmansion.enriched.utils.jsonStringToStringMap
|
|
|
32
31
|
@ReactModule(name = EnrichedTextInputViewManager.NAME)
|
|
33
32
|
class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
34
33
|
EnrichedTextInputViewManagerInterface<EnrichedTextInputView> {
|
|
35
|
-
private val mDelegate: ViewManagerDelegate<EnrichedTextInputView>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
init {
|
|
39
|
-
mDelegate = EnrichedTextInputViewManagerDelegate(this)
|
|
40
|
-
}
|
|
34
|
+
private val mDelegate: ViewManagerDelegate<EnrichedTextInputView> =
|
|
35
|
+
EnrichedTextInputViewManagerDelegate(this)
|
|
41
36
|
|
|
42
37
|
override fun getDelegate(): ViewManagerDelegate<EnrichedTextInputView>? {
|
|
43
38
|
return mDelegate
|
|
@@ -48,10 +43,12 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
public override fun createViewInstance(context: ThemedReactContext): EnrichedTextInputView {
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
return EnrichedTextInputView(context)
|
|
47
|
+
}
|
|
53
48
|
|
|
54
|
-
|
|
49
|
+
override fun onDropViewInstance(view: EnrichedTextInputView) {
|
|
50
|
+
super.onDropViewInstance(view)
|
|
51
|
+
view.layoutManager.releaseMeasurementStore()
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
override fun updateState(
|
|
@@ -80,7 +77,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
80
77
|
|
|
81
78
|
@ReactProp(name = "defaultValue")
|
|
82
79
|
override fun setDefaultValue(view: EnrichedTextInputView?, value: String?) {
|
|
83
|
-
view?.
|
|
80
|
+
view?.setDefaultValue(value)
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
@ReactProp(name = "placeholder")
|
|
@@ -157,9 +154,14 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
157
154
|
view?.setFontStyle(style)
|
|
158
155
|
}
|
|
159
156
|
|
|
157
|
+
@ReactProp(name = "scrollEnabled")
|
|
158
|
+
override fun setScrollEnabled(view: EnrichedTextInputView, scrollEnabled: Boolean) {
|
|
159
|
+
view.scrollEnabled = scrollEnabled
|
|
160
|
+
}
|
|
161
|
+
|
|
160
162
|
override fun onAfterUpdateTransaction(view: EnrichedTextInputView) {
|
|
161
163
|
super.onAfterUpdateTransaction(view)
|
|
162
|
-
view.
|
|
164
|
+
view.afterUpdateTransaction()
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
override fun setPadding(
|
|
@@ -175,7 +177,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
override fun setIsOnChangeHtmlSet(view: EnrichedTextInputView?, value: Boolean) {
|
|
178
|
-
|
|
180
|
+
view?.shouldEmitHtml = value
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
override fun setAutoCapitalize(view: EnrichedTextInputView?, flag: String?) {
|
|
@@ -253,8 +255,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
253
255
|
view?.addLink(start, end, text, url)
|
|
254
256
|
}
|
|
255
257
|
|
|
256
|
-
override fun addImage(view: EnrichedTextInputView?, src: String) {
|
|
257
|
-
view?.addImage(src)
|
|
258
|
+
override fun addImage(view: EnrichedTextInputView?, src: String, width: Float, height: Float) {
|
|
259
|
+
view?.addImage(src, width, height)
|
|
258
260
|
}
|
|
259
261
|
|
|
260
262
|
override fun startMention(view: EnrichedTextInputView?, indicator: String) {
|
|
@@ -277,13 +279,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
277
279
|
heightMode: YogaMeasureMode?,
|
|
278
280
|
attachmentsPositions: FloatArray?
|
|
279
281
|
): Long {
|
|
280
|
-
val
|
|
281
|
-
|
|
282
|
-
if (size != null) {
|
|
283
|
-
return YogaMeasureOutput.make(size.first, size.second)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return YogaMeasureOutput.make(0, 0)
|
|
282
|
+
val id = localData?.getInt("viewTag")
|
|
283
|
+
return MeasurementStore.getMeasureById(context, id, width, props)
|
|
287
284
|
}
|
|
288
285
|
|
|
289
286
|
companion object {
|
|
@@ -4,10 +4,12 @@ import com.facebook.react.ReactPackage
|
|
|
4
4
|
import com.facebook.react.bridge.NativeModule
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
6
|
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
import com.swmansion.enriched.utils.ResourceManager
|
|
7
8
|
import java.util.ArrayList
|
|
8
9
|
|
|
9
10
|
class EnrichedTextInputViewPackage : ReactPackage {
|
|
10
11
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
12
|
+
ResourceManager.init(reactContext.applicationContext)
|
|
11
13
|
val viewManagers: MutableList<ViewManager<*, *>> = ArrayList()
|
|
12
14
|
viewManagers.add(EnrichedTextInputViewManager())
|
|
13
15
|
return viewManagers
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
package com.swmansion.enriched
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Typeface
|
|
5
|
+
import android.graphics.text.LineBreaker
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.text.Spannable
|
|
8
|
+
import android.text.StaticLayout
|
|
9
|
+
import android.text.TextPaint
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import com.facebook.react.bridge.ReadableMap
|
|
12
|
+
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
|
|
16
|
+
import com.facebook.yoga.YogaMeasureOutput
|
|
17
|
+
import com.swmansion.enriched.styles.HtmlStyle
|
|
18
|
+
import com.swmansion.enriched.utils.EnrichedParser
|
|
19
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
20
|
+
import kotlin.math.ceil
|
|
21
|
+
|
|
22
|
+
object MeasurementStore {
|
|
23
|
+
data class PaintParams(
|
|
24
|
+
val typeface: Typeface,
|
|
25
|
+
val fontSize: Float,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
data class MeasurementParams(
|
|
29
|
+
val initialized: Boolean,
|
|
30
|
+
|
|
31
|
+
val cachedWidth: Float,
|
|
32
|
+
val cachedSize: Long,
|
|
33
|
+
|
|
34
|
+
val spannable: CharSequence?,
|
|
35
|
+
val paintParams: PaintParams,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
private val data = ConcurrentHashMap<Int, MeasurementParams>()
|
|
39
|
+
|
|
40
|
+
fun store(id: Int, spannable: Spannable?, paint: TextPaint): Boolean {
|
|
41
|
+
val cachedWidth = data[id]?.cachedWidth ?: 0f
|
|
42
|
+
val cachedSize = data[id]?.cachedSize ?: 0L
|
|
43
|
+
val initialized = data[id]?.initialized ?: true
|
|
44
|
+
|
|
45
|
+
val size = measure(cachedWidth, spannable, paint)
|
|
46
|
+
val paintParams = PaintParams(paint.typeface, paint.textSize)
|
|
47
|
+
|
|
48
|
+
data[id] = MeasurementParams(initialized, cachedWidth, size, spannable, paintParams)
|
|
49
|
+
return cachedSize != size
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun release(id: Int) {
|
|
53
|
+
data.remove(id)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun measure(maxWidth: Float, spannable: CharSequence?, paintParams: PaintParams): Long {
|
|
57
|
+
val paint = TextPaint().apply {
|
|
58
|
+
typeface = paintParams.typeface
|
|
59
|
+
textSize = paintParams.fontSize
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return measure(maxWidth, spannable, paint)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private fun measure(maxWidth: Float, spannable: CharSequence?, paint: TextPaint): Long {
|
|
66
|
+
val text = spannable ?: ""
|
|
67
|
+
val textLength = text.length
|
|
68
|
+
val builder = StaticLayout.Builder
|
|
69
|
+
.obtain(text, 0, textLength, paint, maxWidth.toInt())
|
|
70
|
+
.setIncludePad(true)
|
|
71
|
+
.setLineSpacing(0f, 1f)
|
|
72
|
+
|
|
73
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
74
|
+
builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
78
|
+
builder.setUseLineSpacingFromFallbacks(true)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
val staticLayout = builder.build()
|
|
82
|
+
val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
|
|
83
|
+
val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
|
|
84
|
+
return YogaMeasureOutput.make(widthInSP, heightInSP)
|
|
85
|
+
}
|
|
86
|
+
|
|
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)
|
|
144
|
+
|
|
145
|
+
if (width == value.cachedWidth) {
|
|
146
|
+
return value.cachedSize
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
val paint = TextPaint().apply {
|
|
150
|
+
typeface = value.paintParams.typeface
|
|
151
|
+
textSize = value.paintParams.fontSize
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
val size = measure(width, value.spannable, paint)
|
|
155
|
+
data[id] = MeasurementParams(true, width, size, value.spannable, value.paintParams)
|
|
156
|
+
return size
|
|
157
|
+
}
|
|
158
|
+
}
|