Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,39 @@ class EnrichedTextInputView :
setSelection(actualStart, actualEnd)
}

fun insertValue(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I tested it once again and on Android we need to fix merging spans first before merging this PR:

Screen.Recording.2026-04-02.at.09.14.41.1.mov

value: String,
start: Int,
end: Int,
) {
val currentText = text as Editable
val textLength = currentText.length

if (textLength == 0) {
setValue(value)
return
}

val actualStart = getActualIndex(start)
val actualEnd = getActualIndex(end)

// Use coerceIn to ensure indices are within [0, textLength] and that start <= end
val safeStart = actualStart.coerceIn(0, textLength)
val safeEnd = actualEnd.coerceIn(safeStart, textLength)

runAsATransaction {
val newText = parseText(value) as Spannable

val finalText = currentText.mergeSpannables(safeStart, safeEnd, newText)
setValue(finalText, false)

// replacement-safe: oldLength - removed + inserted
val insertedLength = finalText.length - (textLength - (safeEnd - safeStart))
val insertedEnd = (safeStart + insertedLength).coerceIn(0, finalText.length)
setSelection(insertedEnd)
}
}

// Helper: Walks through the string skipping ZWSPs to find the Nth visible character
private fun getActualIndex(visibleIndex: Int): Int {
val currentText = text as Spannable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,15 @@ class EnrichedTextInputViewManager :
view?.setCustomSelection(start, end)
}

override fun insertValue(
view: EnrichedTextInputView?,
value: String,
start: Int,
end: Int,
) {
view?.insertValue(value, start, end)
}

override fun toggleBold(view: EnrichedTextInputView?) {
view?.verifyAndToggleStyle(EnrichedSpans.BOLD)
}
Expand Down
54 changes: 54 additions & 0 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import "StringExtension.h"
#import "StyleHeaders.h"
#import "TextBlockTapGestureRecognizer.h"
#import "TextInsertionUtils.h"
#import "UIView+React.h"
#import "WordsUtils.h"
#import "ZeroWidthSpaceUtils.h"
Expand Down Expand Up @@ -1339,6 +1340,12 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
} else if ([commandName isEqualToString:@"requestHTML"]) {
NSInteger requestId = [((NSNumber *)args[0]) integerValue];
[self requestHTML:requestId];
} else if ([commandName isEqualToString:@"insertValue"]) {
NSString *value = (NSString *)args[0];
NSInteger start = [((NSNumber *)args[1]) integerValue];
NSInteger end = [((NSNumber *)args[2]) integerValue];

[self insertValue:value start:start end:end];
}
}

Expand Down Expand Up @@ -1384,6 +1391,53 @@ - (void)setCustomSelection:(NSInteger)visibleStart end:(NSInteger)visibleEnd {
textView.selectedRange = NSMakeRange(actualStart, actualEnd - actualStart);
}

- (void)insertValue:(NSString *)value
Comment thread
kacperzolkiewski marked this conversation as resolved.
start:(NSInteger)visibleStart
end:(NSInteger)visibleEnd {
if (value == nil) {
return;
}

NSString *currentText = textView.text;
NSInteger textLength = currentText.length;

if (textLength == 0) {
[self setValue:value];

return;
}

// Use MIN/MAX to ensure indices are within [0, textLength]
// and that start <= end.
NSUInteger start = MIN(MAX(0, (NSUInteger)visibleStart), textLength);
NSUInteger end = MIN(MAX(start, (NSUInteger)visibleEnd), textLength);
NSRange range = NSMakeRange(start, end - start);

NSString *initiallyProcessedHtml = [parser initiallyProcessHtml:value];
if (initiallyProcessedHtml == nullptr) {
// just plain text
range.length > 0 ? [TextInsertionUtils replaceText:value
at:range
additionalAttributes:nil
input:self
withSelection:YES]
: [TextInsertionUtils insertText:value
at:range.location
additionalAttributes:nil
input:self
withSelection:YES];
} else {
// we've got some seemingly proper html
range.length > 0
? [parser replaceFromHtml:initiallyProcessedHtml range:range]
: [parser insertFromHtml:initiallyProcessedHtml
location:range.location];
}

// set recentlyChangedRange and check for changes
[self anyTextMayHaveBeenModified];
}

// Helper: Walks through the string skipping ZWSPs to find the Nth visible
// character
- (NSUInteger)getActualIndex:(NSInteger)visibleIndex text:(NSString *)text {
Expand Down
3 changes: 3 additions & 0 deletions src/native/EnrichedTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,9 @@ export const EnrichedTextInput = ({
setSelection: (start: number, end: number) => {
Commands.setSelection(nullthrows(nativeRef.current), start, end);
},
insertValue: (value: string, start: number, end: number) => {
Commands.insertValue(nullthrows(nativeRef.current), value, start, end);
},
}));

const handleMentionEvent = (e: NativeSyntheticEvent<OnMentionEvent>) => {
Expand Down
7 changes: 7 additions & 0 deletions src/spec/EnrichedTextInputNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ interface NativeCommands {
start: Int32,
end: Int32
) => void;
insertValue: (
viewRef: React.ElementRef<ComponentType>,
value: string,
start: Int32,
end: Int32
) => void;

// Text formatting commands
toggleBold: (viewRef: React.ElementRef<ComponentType>) => void;
Expand Down Expand Up @@ -478,6 +484,7 @@ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
'blur',
'setValue',
'setSelection',
'insertValue',

// Text formatting commands
'toggleBold',
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export interface EnrichedTextInputInstance extends NativeMethods {
setValue: (value: string) => void;
setSelection: (start: number, end: number) => void;
getHTML: () => Promise<string>;
insertValue: (value: string, start: number, end: number) => void;

// Text formatting commands
toggleBold: () => void;
Expand Down
1 change: 1 addition & 0 deletions src/web/EnrichedTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const EnrichedTextInput = ({
measureInWindow: () => {},
measureLayout: () => {},
setNativeProps: () => {},
insertValue: () => {},
})
);

Expand Down
Loading