react-native-enriched 0.1.5 → 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 +34 -10
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
- 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/EnrichedSpans.kt +61 -46
- package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +18 -2
- 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 +71 -40
- 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 +1 -1
- package/src/EnrichedTextInput.tsx +3 -0
- package/src/EnrichedTextInputNativeComponent.ts +1 -0
|
@@ -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,28 +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];
|
|
1107
|
+
// update drawing - schedule debounced relayout
|
|
1108
|
+
[self scheduleRelayoutIfNeeded];
|
|
1109
|
+
}
|
|
1094
1110
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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];
|
|
1103
1137
|
}
|
|
1104
1138
|
|
|
1105
1139
|
// MARK: - UITextView delegate methods
|
|
@@ -1184,9 +1218,6 @@ Class<RCTComponentViewProtocol> EnrichedTextInputViewCls(void) {
|
|
|
1184
1218
|
|
|
1185
1219
|
// manage selection changes
|
|
1186
1220
|
[self manageSelectionBasedChanges];
|
|
1187
|
-
|
|
1188
|
-
// update active styles
|
|
1189
|
-
[self tryUpdatingActiveStyles];
|
|
1190
1221
|
}
|
|
1191
1222
|
|
|
1192
1223
|
// this function isn't called always when some text changes (for example setting link or starting mention with indicator doesn't fire it)
|
|
@@ -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
|
}
|
|
@@ -559,6 +559,7 @@ class EnrichedTextInputViewProps final : public ViewProps {
|
|
|
559
559
|
SharedColor selectionColor{};
|
|
560
560
|
std::string autoCapitalize{};
|
|
561
561
|
EnrichedTextInputViewHtmlStyleStruct htmlStyle{};
|
|
562
|
+
bool scrollEnabled{false};
|
|
562
563
|
SharedColor color{};
|
|
563
564
|
Float fontSize{0.0};
|
|
564
565
|
std::string fontFamily{};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#import "StyleHeaders.h"
|
|
4
4
|
#import "UIView+React.h"
|
|
5
5
|
#import "TextInsertionUtils.h"
|
|
6
|
+
#import "StringExtension.h"
|
|
6
7
|
|
|
7
8
|
@implementation InputParser {
|
|
8
9
|
EnrichedTextInputView *_input;
|
|
@@ -204,8 +205,8 @@
|
|
|
204
205
|
[result appendString: [NSString stringWithFormat:@"<%@>", tagContent]];
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
// append the letter
|
|
208
|
-
[result appendString:currentCharacterStr];
|
|
208
|
+
// append the letter and escape it if needed
|
|
209
|
+
[result appendString: [NSString stringByEscapingHtml:currentCharacterStr]];
|
|
209
210
|
|
|
210
211
|
// save current styles for next character's checks
|
|
211
212
|
previousActiveStyles = currentActiveStyles;
|
|
@@ -490,6 +491,7 @@
|
|
|
490
491
|
BOOL closingTag = NO;
|
|
491
492
|
NSMutableString *currentTagName = [[NSMutableString alloc] initWithString:@""];
|
|
492
493
|
NSMutableString *currentTagParams = [[NSMutableString alloc] initWithString:@""];
|
|
494
|
+
NSDictionary *htmlEntitiesDict = [NSString getEscapedCharactersInfoFrom:fixedHtml];
|
|
493
495
|
|
|
494
496
|
// firstly, extract text and initially processed tags
|
|
495
497
|
for(int i = 0; i < fixedHtml.length; i++) {
|
|
@@ -511,7 +513,7 @@
|
|
|
511
513
|
} else if(!closingTag) {
|
|
512
514
|
// we finish opening tag - get its location and optionally params and put them under tag name key in ongoingTags
|
|
513
515
|
NSMutableArray *tagArr = [[NSMutableArray alloc] init];
|
|
514
|
-
[tagArr addObject:[NSNumber
|
|
516
|
+
[tagArr addObject:[NSNumber numberWithInteger:plainText.length]];
|
|
515
517
|
if(currentTagParams.length > 0) {
|
|
516
518
|
[tagArr addObject:[currentTagParams copy]];
|
|
517
519
|
}
|
|
@@ -550,8 +552,19 @@
|
|
|
550
552
|
currentTagParams = [[NSMutableString alloc] initWithString:@""];
|
|
551
553
|
} else {
|
|
552
554
|
if(!insideTag) {
|
|
553
|
-
// no tags logic - just append text
|
|
554
|
-
|
|
555
|
+
// no tags logic - just append the right text
|
|
556
|
+
|
|
557
|
+
// html entity on the index; use unescaped character and forward iterator accordingly
|
|
558
|
+
NSArray *entityInfo = htmlEntitiesDict[@(i)];
|
|
559
|
+
if(entityInfo != nullptr) {
|
|
560
|
+
NSString *escaped = entityInfo[0];
|
|
561
|
+
NSString *unescaped = entityInfo[1];
|
|
562
|
+
[plainText appendString:unescaped];
|
|
563
|
+
// the iterator will forward by 1 itself
|
|
564
|
+
i += escaped.length - 1;
|
|
565
|
+
} else {
|
|
566
|
+
[plainText appendString:currentCharacterStr];
|
|
567
|
+
}
|
|
555
568
|
} else {
|
|
556
569
|
if(gettingTagName) {
|
|
557
570
|
if(currentCharacterChar == ' ') {
|
|
@@ -597,9 +610,21 @@
|
|
|
597
610
|
} else if([tagName isEqualToString:@"code"]) {
|
|
598
611
|
[styleArr addObject:@([InlineCodeStyle getStyleType])];
|
|
599
612
|
} else if([tagName isEqualToString:@"a"]) {
|
|
613
|
+
NSRegularExpression *hrefRegex = [NSRegularExpression regularExpressionWithPattern:@"href=\".+\""
|
|
614
|
+
options:0
|
|
615
|
+
error:nullptr
|
|
616
|
+
];
|
|
617
|
+
NSTextCheckingResult* match = [hrefRegex firstMatchInString:params options:0 range: NSMakeRange(0, params.length)];
|
|
618
|
+
|
|
619
|
+
if(match == nullptr) {
|
|
620
|
+
// same as on Android, no href (or empty href) equals no link style
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
NSRange hrefRange = match.range;
|
|
600
625
|
[styleArr addObject:@([LinkStyle getStyleType])];
|
|
601
626
|
// cut only the url from the href="..." string
|
|
602
|
-
NSString *url = [params substringWithRange:NSMakeRange(6,
|
|
627
|
+
NSString *url = [params substringWithRange:NSMakeRange(hrefRange.location + 6, hrefRange.length - 7)];
|
|
603
628
|
stylePair.styleValue = url;
|
|
604
629
|
} else if([tagName isEqualToString:@"mention"]) {
|
|
605
630
|
[styleArr addObject:@([MentionStyle getStyleType])];
|
|
@@ -644,7 +669,7 @@
|
|
|
644
669
|
} else if([tagName isEqualToString:@"blockquote"]) {
|
|
645
670
|
[styleArr addObject:@([BlockQuoteStyle getStyleType])];
|
|
646
671
|
} else {
|
|
647
|
-
|
|
672
|
+
// some other external tags like span just don't get put into the processed styles
|
|
648
673
|
continue;
|
|
649
674
|
}
|
|
650
675
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
NSString *plainText = [typedInput->textView.textStorage.string substringWithRange:typedInput->textView.selectedRange];
|
|
15
15
|
NSString *fixedPlainText = [plainText stringByReplacingOccurrencesOfString:@"\u200B" withString:@""];
|
|
16
16
|
|
|
17
|
-
NSString *
|
|
17
|
+
NSString *parsedHtml = [typedInput->parser parseToHtmlFromRange:typedInput->textView.selectedRange];
|
|
18
18
|
|
|
19
19
|
NSMutableAttributedString *attrStr = [[typedInput->textView.textStorage attributedSubstringFromRange:typedInput->textView.selectedRange] mutableCopy];
|
|
20
20
|
NSRange fullAttrStrRange = NSMakeRange(0, attrStr.length);
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
|
|
29
29
|
[pasteboard setItems:@[@{
|
|
30
30
|
UTTypeUTF8PlainText.identifier : fixedPlainText,
|
|
31
|
-
UTTypeHTML.identifier :
|
|
31
|
+
UTTypeHTML.identifier : parsedHtml,
|
|
32
32
|
UTTypeRTF.identifier : rtfData
|
|
33
33
|
}]];
|
|
34
34
|
}
|
|
@@ -53,9 +53,7 @@
|
|
|
53
53
|
htmlString = htmlValue;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
htmlString = [NSString stringByUnescapingHtml:htmlString];
|
|
58
|
-
// validate it
|
|
56
|
+
// validate the html
|
|
59
57
|
NSString *initiallyProcessedHtml = [typedInput->parser initiallyProcessHtml:htmlString];
|
|
60
58
|
|
|
61
59
|
if(initiallyProcessedHtml != nullptr) {
|
package/ios/styles/LinkStyle.mm
CHANGED
|
@@ -167,7 +167,6 @@ static NSString *const AutomaticLinkAttributeName = @"AutomaticLinkAttributeName
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
[self manageLinkTypingAttributes];
|
|
170
|
-
[_input anyTextMayHaveBeenModified];
|
|
171
170
|
}
|
|
172
171
|
|
|
173
172
|
// get exact link data at the given location if it exists
|
|
@@ -224,20 +223,22 @@ static NSString *const AutomaticLinkAttributeName = @"AutomaticLinkAttributeName
|
|
|
224
223
|
}
|
|
225
224
|
}
|
|
226
225
|
|
|
227
|
-
[_input->textView.textStorage
|
|
226
|
+
NSString *manualLink = [_input->textView.textStorage
|
|
228
227
|
attribute:ManualLinkAttributeName
|
|
229
228
|
atIndex:searchLocation
|
|
230
229
|
longestEffectiveRange: &manualLinkRange
|
|
231
230
|
inRange:inputRange
|
|
232
231
|
];
|
|
233
|
-
[_input->textView.textStorage
|
|
232
|
+
NSString *automaticLink = [_input->textView.textStorage
|
|
234
233
|
attribute:AutomaticLinkAttributeName
|
|
235
234
|
atIndex:searchLocation
|
|
236
235
|
longestEffectiveRange: &automaticLinkRange
|
|
237
236
|
inRange:inputRange
|
|
238
237
|
];
|
|
239
238
|
|
|
240
|
-
return
|
|
239
|
+
return manualLink == nullptr
|
|
240
|
+
? automaticLink == nullptr ? NSMakeRange(0, 0) : automaticLinkRange
|
|
241
|
+
: manualLinkRange;
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
- (void)manageLinkTypingAttributes {
|
|
@@ -349,10 +350,9 @@ static NSString *const AutomaticLinkAttributeName = @"AutomaticLinkAttributeName
|
|
|
349
350
|
}
|
|
350
351
|
if(addStyle) {
|
|
351
352
|
[self addLink:word url:regexPassedUrl range:wordRange manual:NO];
|
|
353
|
+
// emit onLinkDetected if style was added
|
|
354
|
+
[_input emitOnLinkDetectedEvent:word url:regexPassedUrl range:wordRange];
|
|
352
355
|
}
|
|
353
|
-
|
|
354
|
-
// emit onLinkDetected
|
|
355
|
-
[_input emitOnLinkDetectedEvent:word url:regexPassedUrl range:wordRange];
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
@@ -158,18 +158,13 @@
|
|
|
158
158
|
if(charBefore == '1') {
|
|
159
159
|
// we got a match - add a list if possible
|
|
160
160
|
if([_input handleStyleBlocksAndConflicts:[[self class] getStyleType] range:paragraphRange]) {
|
|
161
|
-
// don't emit
|
|
162
|
-
|
|
163
|
-
if(prevEmitHtml) {
|
|
164
|
-
_input->emitHtml = NO;
|
|
165
|
-
}
|
|
161
|
+
// don't emit during the replacing
|
|
162
|
+
_input->blockEmitting = YES;
|
|
166
163
|
|
|
167
164
|
// remove the number
|
|
168
165
|
[TextInsertionUtils replaceText:@"" at:NSMakeRange(paragraphRange.location, 1) additionalAttributes:nullptr input:_input withSelection:YES];
|
|
169
166
|
|
|
170
|
-
|
|
171
|
-
_input->emitHtml = YES;
|
|
172
|
-
}
|
|
167
|
+
_input->blockEmitting = NO;
|
|
173
168
|
|
|
174
169
|
// add attributes on the paragraph
|
|
175
170
|
[self addAttributes:NSMakeRange(paragraphRange.location, paragraphRange.length - 1)];
|