diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt index 42264b4e..e457afa2 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt @@ -436,6 +436,39 @@ class EnrichedTextInputView : setSelection(actualStart, actualEnd) } + fun insertValue( + 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 diff --git a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt index 6b24ac27..f03466d7 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt @@ -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) } diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 66c22512..f28e1741 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -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" @@ -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]; } } @@ -1384,6 +1391,53 @@ - (void)setCustomSelection:(NSInteger)visibleStart end:(NSInteger)visibleEnd { textView.selectedRange = NSMakeRange(actualStart, actualEnd - actualStart); } +- (void)insertValue:(NSString *)value + 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 { diff --git a/src/native/EnrichedTextInput.tsx b/src/native/EnrichedTextInput.tsx index a58756bc..4347d1ca 100644 --- a/src/native/EnrichedTextInput.tsx +++ b/src/native/EnrichedTextInput.tsx @@ -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) => { diff --git a/src/spec/EnrichedTextInputNativeComponent.ts b/src/spec/EnrichedTextInputNativeComponent.ts index 83e557d5..74bef1aa 100644 --- a/src/spec/EnrichedTextInputNativeComponent.ts +++ b/src/spec/EnrichedTextInputNativeComponent.ts @@ -416,6 +416,12 @@ interface NativeCommands { start: Int32, end: Int32 ) => void; + insertValue: ( + viewRef: React.ElementRef, + value: string, + start: Int32, + end: Int32 + ) => void; // Text formatting commands toggleBold: (viewRef: React.ElementRef) => void; @@ -478,6 +484,7 @@ export const Commands: NativeCommands = codegenNativeCommands({ 'blur', 'setValue', 'setSelection', + 'insertValue', // Text formatting commands 'toggleBold', diff --git a/src/types.ts b/src/types.ts index aeee6e93..bd782ad2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -224,6 +224,7 @@ export interface EnrichedTextInputInstance extends NativeMethods { setValue: (value: string) => void; setSelection: (start: number, end: number) => void; getHTML: () => Promise; + insertValue: (value: string, start: number, end: number) => void; // Text formatting commands toggleBold: () => void; diff --git a/src/web/EnrichedTextInput.tsx b/src/web/EnrichedTextInput.tsx index 89e81d37..2b57a31b 100644 --- a/src/web/EnrichedTextInput.tsx +++ b/src/web/EnrichedTextInput.tsx @@ -126,6 +126,7 @@ export const EnrichedTextInput = ({ measureInWindow: () => {}, measureLayout: () => {}, setNativeProps: () => {}, + insertValue: () => {}, }) );