From c4aeaae7ed2058ed14ea17bcdb2d97dc2ab7a7c5 Mon Sep 17 00:00:00 2001 From: Sean Murphy Date: Sun, 5 Apr 2026 14:04:47 -0400 Subject: [PATCH] feat: add insertText command for inserting text at cursor or position Adds a new `insertText` command that inserts plain text into the attributed string without replacing existing rich content (images, mentions, etc). Accepts optional start/end position parameters (visible indices, skipping ZWSPs). When omitted, inserts at the current selection. Implemented for both iOS and Android. --- .../textinput/EnrichedTextInputView.kt | 22 +++++++++++++++ .../textinput/EnrichedTextInputViewManager.kt | 9 +++++++ ios/EnrichedTextInputView.mm | 27 +++++++++++++++++++ src/native/EnrichedTextInput.tsx | 5 ++++ src/spec/EnrichedTextInputNativeComponent.ts | 7 +++++ src/types.ts | 1 + src/web/EnrichedTextInput.tsx | 10 +++++++ 7 files changed, 81 insertions(+) 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..1b4593de 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt @@ -426,6 +426,28 @@ class EnrichedTextInputView : layoutManager.invalidateLayout() } + fun insertText( + text: String, + start: Int, + end: Int, + ) { + val editable = this.text ?: return + val replaceStart: Int + val replaceEnd: Int + + if (start < 0) { + // Use current selection + replaceStart = selectionStart + replaceEnd = selectionEnd + } else { + replaceStart = getActualIndex(start) + replaceEnd = getActualIndex(end) + } + + editable.replace(replaceStart, replaceEnd, text) + setSelection(replaceStart + text.length) + } + fun setCustomSelection( visibleStart: Int, visibleEnd: Int, 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..904a4d57 100644 --- a/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt +++ b/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt @@ -322,6 +322,15 @@ class EnrichedTextInputViewManager : view?.setValue(text) } + override fun insertText( + view: EnrichedTextInputView?, + text: String, + start: Int, + end: Int, + ) { + view?.insertText(text, start, end) + } + override fun setSelection( view: EnrichedTextInputView?, start: Int, diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm index 66c22512..290887a0 100644 --- a/ios/EnrichedTextInputView.mm +++ b/ios/EnrichedTextInputView.mm @@ -1332,6 +1332,11 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args { CGFloat imgHeight = [(NSNumber *)args[2] floatValue]; [self addImage:uri width:imgWidth height:imgHeight]; + } else if ([commandName isEqualToString:@"insertText"]) { + NSString *text = (NSString *)args[0]; + NSInteger start = [((NSNumber *)args[1]) integerValue]; + NSInteger end = [((NSNumber *)args[2]) integerValue]; + [self insertText:text start:start end:end]; } else if ([commandName isEqualToString:@"setSelection"]) { NSInteger start = [((NSNumber *)args[0]) integerValue]; NSInteger end = [((NSNumber *)args[1]) integerValue]; @@ -1375,6 +1380,28 @@ - (void)setValue:(NSString *)value { [self anyTextMayHaveBeenModified]; } +- (void)insertText:(NSString *)text start:(NSInteger)start end:(NSInteger)end { + NSString *currentText = textView.textStorage.string; + + NSRange replaceRange; + if (start < 0) { + // Use current selection + replaceRange = textView.selectedRange; + } else { + NSUInteger actualStart = [self getActualIndex:start text:currentText]; + NSUInteger actualEnd = [self getActualIndex:end text:currentText]; + replaceRange = NSMakeRange(actualStart, actualEnd - actualStart); + } + + // Replace the range with the new text, preserving surrounding attributes + [textView.textStorage replaceCharactersInRange:replaceRange withString:text]; + + // Move cursor to end of inserted text + textView.selectedRange = NSMakeRange(replaceRange.location + text.length, 0); + + [self anyTextMayHaveBeenModified]; +} + - (void)setCustomSelection:(NSInteger)visibleStart end:(NSInteger)visibleEnd { NSString *text = textView.textStorage.string; diff --git a/src/native/EnrichedTextInput.tsx b/src/native/EnrichedTextInput.tsx index a58756bc..137cbcd8 100644 --- a/src/native/EnrichedTextInput.tsx +++ b/src/native/EnrichedTextInput.tsx @@ -181,6 +181,11 @@ export const EnrichedTextInput = ({ setValue: (value: string) => { Commands.setValue(nullthrows(nativeRef.current), value); }, + insertText: (text: string, start?: number, end?: number) => { + const s = start ?? -1; + const e = end ?? s; + Commands.insertText(nullthrows(nativeRef.current), text, s, e); + }, getHTML: () => { return new Promise((resolve, reject) => { const requestId = nextHtmlRequestId.current++; diff --git a/src/spec/EnrichedTextInputNativeComponent.ts b/src/spec/EnrichedTextInputNativeComponent.ts index 83e557d5..8baa3c6a 100644 --- a/src/spec/EnrichedTextInputNativeComponent.ts +++ b/src/spec/EnrichedTextInputNativeComponent.ts @@ -411,6 +411,12 @@ interface NativeCommands { focus: (viewRef: React.ElementRef) => void; blur: (viewRef: React.ElementRef) => void; setValue: (viewRef: React.ElementRef, text: string) => void; + insertText: ( + viewRef: React.ElementRef, + text: string, + start: Int32, + end: Int32 + ) => void; setSelection: ( viewRef: React.ElementRef, start: Int32, @@ -477,6 +483,7 @@ export const Commands: NativeCommands = codegenNativeCommands({ 'focus', 'blur', 'setValue', + 'insertText', 'setSelection', // Text formatting commands diff --git a/src/types.ts b/src/types.ts index ada3ceb6..0c72027f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -388,6 +388,7 @@ export interface EnrichedTextInputInstance extends NativeMethods { focus: () => void; blur: () => void; setValue: (value: string) => void; + insertText: (text: string, start?: number, end?: number) => void; setSelection: (start: number, end: number) => void; getHTML: () => Promise; diff --git a/src/web/EnrichedTextInput.tsx b/src/web/EnrichedTextInput.tsx index 5ee21ae0..c8cae9ed 100644 --- a/src/web/EnrichedTextInput.tsx +++ b/src/web/EnrichedTextInput.tsx @@ -102,6 +102,16 @@ export const EnrichedTextInput = ({ }) .run(); }, + insertText: (text: string, start?: number, end?: number) => { + if (start !== undefined && start >= 0) { + const doc = editor.state.doc; + const from = nativePosToTiptapPos(doc, start); + const to = end !== undefined ? nativePosToTiptapPos(doc, end) : from; + editor.chain().focus().insertContentAt({ from, to }, text).run(); + } else { + editor.commands.insertContent(text); + } + }, getHTML: () => Promise.resolve(normalizeHtmlFromTiptap(editor.getHTML())), toggleBold: () => {}, toggleItalic: () => {},