Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2449,7 +2449,7 @@ PODS:
- React-perflogger (= 0.83.0)
- React-utils (= 0.83.0)
- SocketRocket
- ReactNativeTypeRich (2.2.2):
- ReactNativeTypeRich (2.2.3):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2802,7 +2802,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568
ReactCodegen: 11c08ff43a62009d48c71de000352e4515918801
ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f
ReactNativeTypeRich: 2c69126078c3ca6836c6ceed4a0c32a0115fe7d7
ReactNativeTypeRich: cc40c4c030b5c3855373576e0fa09b19f7550000
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e

Expand Down
147 changes: 71 additions & 76 deletions ios/modules/commands/TypeRichTextInputCommands.mm
Original file line number Diff line number Diff line change
Expand Up @@ -60,82 +60,77 @@ - (void)blur
/// setText(text) - diff-based with proper cursor tracking, non-undoable
- (void)setText:(NSString *)text
{
UITextView *tv = _textView;
TypeRichTextInputView *owner = _owner;
if (!tv || !owner) return;

dispatch_async(dispatch_get_main_queue(), ^{
// Never interrupt IME composition
if (tv.markedTextRange) return;

NSString *newText = text ?: @"";
NSString *currentText = tv.text ?: @"";

// commented for now as causing issues when textinput is blank
// if ([currentText isEqualToString:newText]) {
// return;
// }

owner.blockEmitting = YES;

NSRange oldSelection = tv.selectedRange;

// Calculate minimal diff range
NSRange diffRange = [self calculateDiffRange:currentText newText:newText];

// Calculate what text will be inserted
NSInteger newLength = newText.length - (currentText.length - diffRange.length);
NSString *replacementText = @"";

if (newLength > 0) {
replacementText = [newText substringWithRange:NSMakeRange(diffRange.location, newLength)];
}

// Convert NSRange to UITextRange
UITextPosition *start = [tv positionFromPosition:tv.beginningOfDocument
offset:diffRange.location];
UITextPosition *end = [tv positionFromPosition:tv.beginningOfDocument
offset:NSMaxRange(diffRange)];

if (start && end) {
UITextRange *range = [tv textRangeFromPosition:start toPosition:end];
[tv replaceRange:range withText:replacementText];

// Calculate cursor adjustment
NSInteger delta = replacementText.length - diffRange.length;
NSInteger newCursorPos = oldSelection.location;

// If change happened before cursor, adjust cursor position
if (diffRange.location <= oldSelection.location) {
newCursorPos = oldSelection.location + delta;
}

// Clamp to valid range
newCursorPos = MAX(0, MIN(newCursorPos, newText.length));

// Restore cursor at adjusted position
tv.selectedRange = NSMakeRange(newCursorPos, 0);

} else {
// Fallback: full replace with cursor clamping
tv.text = newText;
NSInteger safeLoc = MIN(oldSelection.location, newText.length);
tv.selectedRange = NSMakeRange(safeLoc, 0);
}

owner.blockEmitting = NO;

[owner updatePlaceholderVisibilityFromCommand];

if (tv.scrollEnabled) {
[owner invalidateTextLayoutFromCommand];

// scroll to cursor
[tv scrollRangeToVisible:tv.selectedRange];
}

[owner dispatchSelectionChangeIfNeeded];
});
UITextView *tv = _textView;
TypeRichTextInputView *owner = _owner;
if (!tv || !owner) return;

dispatch_async(dispatch_get_main_queue(), ^{
if (tv.markedTextRange) return;

NSString *newText = text ?: @"";
NSString *currentText = tv.text ?: @"";

// if text is same, do nothing.
if ([currentText isEqualToString:newText]) {
return;
}

owner.blockEmitting = YES;

NSRange oldSelection = tv.selectedRange;
NSRange diffRange = [self calculateDiffRange:currentText newText:newText];

NSInteger newLength = newText.length - (currentText.length - diffRange.length);
NSString *replacementText = (newLength > 0)
? [newText substringWithRange:NSMakeRange(diffRange.location, newLength)]
: @"";

UITextPosition *start = [tv positionFromPosition:tv.beginningOfDocument offset:diffRange.location];
UITextPosition *end = [tv positionFromPosition:tv.beginningOfDocument offset:NSMaxRange(diffRange)];

if (start && end) {
UITextRange *range = [tv textRangeFromPosition:start toPosition:end];

// text replacement
[tv replaceRange:range withText:replacementText];

// if the user is typing at the end of the selection,
// and we just inserted text that matches what the system expected,
// we should only jump the cursor if the JS-provided state differs
// from the current internal state.

NSInteger delta = replacementText.length - diffRange.length;
BOOL cursorWasAtEnd = (oldSelection.location == currentText.length);

if (cursorWasAtEnd) {
tv.selectedRange = NSMakeRange(newText.length, 0);
} else {
// If the change happened at or before the cursor, shift it
if (diffRange.location <= oldSelection.location) {
NSInteger newPos = oldSelection.location + delta;
newPos = MAX(0, MIN(newPos, newText.length));

// IMPORTANT: Only set the range if it's actually different.
// Setting selectedRange unnecessarily resets the keyboard's internal state.
if (tv.selectedRange.location != newPos) {
tv.selectedRange = NSMakeRange(newPos, 0);
}
}
}
} else {
tv.text = newText;
}

owner.blockEmitting = NO;
[owner updatePlaceholderVisibilityFromCommand];

if (tv.scrollEnabled) {
[owner invalidateTextLayoutFromCommand];
[tv scrollRangeToVisible:tv.selectedRange];
}

[owner dispatchSelectionChangeIfNeeded];
});
}

// Helper: Calculate minimal diff range between two strings
Expand Down
Loading