2828import com .facebook .infer .annotation .Assertions ;
2929import com .facebook .react .bridge .Dynamic ;
3030import com .facebook .react .bridge .JSApplicationIllegalArgumentException ;
31+ import com .facebook .react .bridge .JavaOnlyArray ;
32+ import com .facebook .react .bridge .JavaOnlyMap ;
3133import com .facebook .react .bridge .ReactContext ;
3234import com .facebook .react .bridge .ReadableArray ;
3335import com .facebook .react .bridge .ReadableMap ;
3436import com .facebook .react .bridge .ReadableNativeMap ;
3537import 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 ;
3642import com .facebook .react .common .MapBuilder ;
3743import com .facebook .react .module .annotations .ReactModule ;
3844import com .facebook .react .uimanager .BaseViewManager ;
@@ -225,7 +231,8 @@ public void receiveCommand(
225231
226232 // TODO: construct a ReactTextUpdate and use that with maybeSetText
227233 // 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 ));
229236 }
230237 break ;
231238 }
@@ -257,7 +264,7 @@ public void updateExtraData(ReactEditText view, Object extraData) {
257264 Spannable spannable = update .getText ();
258265 TextInlineImageSpan .possiblyUpdateInlineImageSpans (spannable , view );
259266 }
260- view .maybeSetText (update );
267+ view .maybeSetTextFromState (update );
261268 if (update .getSelectionStart () != UNSET && update .getSelectionEnd () != UNSET )
262269 view .setSelection (update .getSelectionStart (), update .getSelectionEnd ());
263270 }
@@ -842,6 +849,10 @@ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
842849
843850 @ Override
844851 public void onTextChanged (CharSequence s , int start , int before , int count ) {
852+ if (mEditText .mDisableTextDiffing ) {
853+ return ;
854+ }
855+
845856 // Rearranging the text (i.e. changing between singleline and multiline attributes) can
846857 // also trigger onTextChanged, call the event in JS only when the text actually changed
847858 if (count == 0 && before == 0 ) {
@@ -856,6 +867,92 @@ public void onTextChanged(CharSequence s, int start, int before, int count) {
856867 return ;
857868 }
858869
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+
859956 // The event that contains the event counter and updates it must be sent first.
860957 // TODO: t7936714 merge these events
861958 mEventDispatcher .dispatchEvent (
@@ -1116,12 +1213,13 @@ public Object updateState(
11161213
11171214 view .mStateWrapper = stateWrapper ;
11181215
1119- return new ReactTextUpdate (
1216+ return ReactTextUpdate . buildReactTextUpdateFromState (
11201217 spanned ,
11211218 state .getInt ("mostRecentEventCount" ),
11221219 false , // TODO add this into local Data
11231220 textViewProps .getTextAlign (),
11241221 textBreakStrategy ,
1125- justificationMode );
1222+ justificationMode ,
1223+ attributedString );
11261224 }
11271225}
0 commit comments