28
28
import com .facebook .infer .annotation .Assertions ;
29
29
import com .facebook .react .bridge .Dynamic ;
30
30
import com .facebook .react .bridge .JSApplicationIllegalArgumentException ;
31
+ import com .facebook .react .bridge .JavaOnlyArray ;
32
+ import com .facebook .react .bridge .JavaOnlyMap ;
31
33
import com .facebook .react .bridge .ReactContext ;
32
34
import com .facebook .react .bridge .ReadableArray ;
33
35
import com .facebook .react .bridge .ReadableMap ;
34
36
import com .facebook .react .bridge .ReadableNativeMap ;
35
37
import com .facebook .react .bridge .ReadableType ;
38
+ import com .facebook .react .bridge .WritableArray ;
39
+ import com .facebook .react .bridge .WritableMap ;
40
+ import com .facebook .react .bridge .WritableNativeArray ;
41
+ import com .facebook .react .bridge .WritableNativeMap ;
36
42
import com .facebook .react .common .MapBuilder ;
37
43
import com .facebook .react .module .annotations .ReactModule ;
38
44
import com .facebook .react .uimanager .BaseViewManager ;
@@ -225,7 +231,8 @@ public void receiveCommand(
225
231
226
232
// TODO: construct a ReactTextUpdate and use that with maybeSetText
227
233
// instead of calling setText, etc directly - doing that will definitely cause bugs.
228
- reactEditText .maybeSetText (getReactTextUpdate (text , mostRecentEventCount , start , end ));
234
+ reactEditText .maybeSetTextFromJS (
235
+ getReactTextUpdate (text , mostRecentEventCount , start , end ));
229
236
}
230
237
break ;
231
238
}
@@ -257,7 +264,7 @@ public void updateExtraData(ReactEditText view, Object extraData) {
257
264
Spannable spannable = update .getText ();
258
265
TextInlineImageSpan .possiblyUpdateInlineImageSpans (spannable , view );
259
266
}
260
- view .maybeSetText (update );
267
+ view .maybeSetTextFromState (update );
261
268
if (update .getSelectionStart () != UNSET && update .getSelectionEnd () != UNSET )
262
269
view .setSelection (update .getSelectionStart (), update .getSelectionEnd ());
263
270
}
@@ -842,6 +849,10 @@ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
842
849
843
850
@ Override
844
851
public void onTextChanged (CharSequence s , int start , int before , int count ) {
852
+ if (mEditText .mDisableTextDiffing ) {
853
+ return ;
854
+ }
855
+
845
856
// Rearranging the text (i.e. changing between singleline and multiline attributes) can
846
857
// also trigger onTextChanged, call the event in JS only when the text actually changed
847
858
if (count == 0 && before == 0 ) {
@@ -856,6 +867,92 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
856
867
return ;
857
868
}
858
869
870
+ // Fabric: update representation of AttributedString
871
+ JavaOnlyMap attributedString = mEditText .mAttributedString ;
872
+ if (attributedString != null && attributedString .hasKey ("fragments" )) {
873
+ String changedText = s .subSequence (start , start + count ).toString ();
874
+
875
+ String completeStr = attributedString .getString ("string" );
876
+ String newCompleteStr =
877
+ completeStr .substring (0 , start )
878
+ + changedText
879
+ + (completeStr .length () > start + before
880
+ ? completeStr .substring (start + before )
881
+ : "" );
882
+ attributedString .putString ("string" , newCompleteStr );
883
+
884
+ // Loop through all fragments and change them in-place
885
+ JavaOnlyArray fragments = (JavaOnlyArray ) attributedString .getArray ("fragments" );
886
+ int positionInAttributedString = 0 ;
887
+ boolean found = false ;
888
+ for (int i = 0 ; i < fragments .size () && !found ; i ++) {
889
+ JavaOnlyMap fragment = (JavaOnlyMap ) fragments .getMap (i );
890
+ String fragmentStr = fragment .getString ("string" );
891
+ int positionBefore = positionInAttributedString ;
892
+ positionInAttributedString += fragmentStr .length ();
893
+ if (positionInAttributedString < start ) {
894
+ continue ;
895
+ }
896
+
897
+ int relativePosition = start - positionBefore ;
898
+ found = true ;
899
+
900
+ // Does the change span multiple Fragments?
901
+ // If so, we put any new text entirely in the first
902
+ // Fragment that we edit. For example, if you select two words
903
+ // across Fragment boundaries, "one | two", and replace them with a
904
+ // character "x", the first Fragment will replace "one " with "x", and the
905
+ // second Fragment will replace "two" with an empty string.
906
+ int remaining = fragmentStr .length () - relativePosition ;
907
+
908
+ String newString =
909
+ fragmentStr .substring (0 , relativePosition )
910
+ + changedText
911
+ + (fragmentStr .substring (relativePosition + Math .min (before , remaining )));
912
+ fragment .putString ("string" , newString );
913
+
914
+ // If we're changing 10 characters (before=10) and remaining=3,
915
+ // we want to remove 3 characters from this fragment (`Math.min(before, remaining)`)
916
+ // and 7 from the next Fragment (`before = 10 - 3`)
917
+ if (remaining < before ) {
918
+ changedText = "" ;
919
+ start += remaining ;
920
+ before = before - remaining ;
921
+ found = false ;
922
+ }
923
+ }
924
+ }
925
+
926
+ // Fabric: communicate to C++ layer that text has changed
927
+ // We need to call `incrementAndGetEventCounter` here explicitly because this
928
+ // update may race with other updates.
929
+ // TODO: currently WritableNativeMaps/WritableNativeArrays cannot be reused so
930
+ // we must recreate these data structures every time. It would be nice to have a
931
+ // reusable data-structure to use for TextInput because constructing these and copying
932
+ // on every keystroke is very expensive.
933
+ if (mEditText .mStateWrapper != null && attributedString != null ) {
934
+ WritableMap map = new WritableNativeMap ();
935
+ WritableMap newAttributedString = new WritableNativeMap ();
936
+
937
+ WritableArray fragments = new WritableNativeArray ();
938
+
939
+ for (int i = 0 ; i < attributedString .getArray ("fragments" ).size (); i ++) {
940
+ ReadableMap readableFragment = attributedString .getArray ("fragments" ).getMap (i );
941
+ WritableMap fragment = new WritableNativeMap ();
942
+ fragment .putDouble ("reactTag" , readableFragment .getInt ("reactTag" ));
943
+ fragment .putString ("string" , readableFragment .getString ("string" ));
944
+ fragments .pushMap (fragment );
945
+ }
946
+
947
+ newAttributedString .putString ("string" , attributedString .getString ("string" ));
948
+ newAttributedString .putArray ("fragments" , fragments );
949
+
950
+ map .putInt ("mostRecentEventCount" , mEditText .incrementAndGetEventCounter ());
951
+ map .putMap ("textChanged" , newAttributedString );
952
+
953
+ mEditText .mStateWrapper .updateState (map );
954
+ }
955
+
859
956
// The event that contains the event counter and updates it must be sent first.
860
957
// TODO: t7936714 merge these events
861
958
mEventDispatcher .dispatchEvent (
@@ -1116,12 +1213,13 @@ public Object updateState(
1116
1213
1117
1214
view .mStateWrapper = stateWrapper ;
1118
1215
1119
- return new ReactTextUpdate (
1216
+ return ReactTextUpdate . buildReactTextUpdateFromState (
1120
1217
spanned ,
1121
1218
state .getInt ("mostRecentEventCount" ),
1122
1219
false , // TODO add this into local Data
1123
1220
textViewProps .getTextAlign (),
1124
1221
textBreakStrategy ,
1125
- justificationMode );
1222
+ justificationMode ,
1223
+ attributedString );
1126
1224
}
1127
1225
}
0 commit comments