From d53e8c2f4cb8ff2193fa9adf29a993006625d40b Mon Sep 17 00:00:00 2001 From: szydlovsky <9szydlowski9@gmail.com> Date: Mon, 11 Aug 2025 10:37:29 +0200 Subject: [PATCH] fix: handle replacement edge cases for links and mentions --- ios/ReactNativeRichTextEditorView.mm | 12 ++++++++++++ ios/styles/LinkStyle.mm | 15 +++++++++++++++ ios/styles/MentionStyle.mm | 15 +++++++++++++++ ios/utils/StyleHeaders.h | 2 ++ ios/utils/TextInsertionUtils.h | 5 ++--- ios/utils/TextInsertionUtils.mm | 16 +++++++++------- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/ios/ReactNativeRichTextEditorView.mm b/ios/ReactNativeRichTextEditorView.mm index 8418bf336..43f0cf62f 100644 --- a/ios/ReactNativeRichTextEditorView.mm +++ b/ios/ReactNativeRichTextEditorView.mm @@ -1083,6 +1083,18 @@ - (bool)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range r rejectTextChanges = rejectTextChanges || removedFirstLineList || addedShortcutList; } + LinkStyle *linkStyle = stylesDict[@([LinkStyle getStyleType])]; + if(linkStyle != nullptr) { + BOOL fixedLeadingAttributes = [linkStyle handleLeadingLinkReplacement:range replacementText:text]; + rejectTextChanges = rejectTextChanges || fixedLeadingAttributes; + } + + MentionStyle *mentionStyle = stylesDict[@([MentionStyle getStyleType])]; + if(mentionStyle != nullptr) { + BOOL fixedLeadingAttributes = [mentionStyle handleLeadingMentionReplacement:range replacementText:text]; + rejectTextChanges = rejectTextChanges || fixedLeadingAttributes; + } + if(rejectTextChanges) { [self anyTextMayHaveBeenModified]; } diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm index ce12f064d..ae518ba0e 100644 --- a/ios/styles/LinkStyle.mm +++ b/ios/styles/LinkStyle.mm @@ -407,6 +407,21 @@ - (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange { [self manageLinkTypingAttributes]; } +// replacing whole input (that starts with a link) with a manually typed letter improperly applies link's attributes to all the following text +- (BOOL)handleLeadingLinkReplacement:(NSRange)range replacementText:(NSString *)text { + // whole textView range gets replaced with a single letter + if(_editor->textView.textStorage.string.length > 0 && NSEqualRanges(range, NSMakeRange(0, _editor->textView.textStorage.string.length)) && text.length == 1) { + // first character detection is enough for the removal to be done + if([self detectStyle:NSMakeRange(0, 1)]) { + [self removeAttributes:NSMakeRange(0, _editor->textView.textStorage.string.length)]; + // do the replacing manually + [TextInsertionUtils replaceText:text inView:_editor->textView at:range additionalAttributes:nullptr editor:_editor]; + return YES; + } + } + return NO; +} + // MARK: - Private non-standard methods // determines whether a given range contains only links pointing to one url diff --git a/ios/styles/MentionStyle.mm b/ios/styles/MentionStyle.mm index 7f74f7fd8..a7eac5061 100644 --- a/ios/styles/MentionStyle.mm +++ b/ios/styles/MentionStyle.mm @@ -390,6 +390,21 @@ - (void)manageMentionTypingAttributes { } } +// replacing whole input (that starts with a mention) with a manually typed letter improperly applies mention's attributes to all the following text +- (BOOL)handleLeadingMentionReplacement:(NSRange)range replacementText:(NSString *)text { + // whole textView range gets replaced with a single letter + if(_editor->textView.textStorage.string.length > 0 && NSEqualRanges(range, NSMakeRange(0, _editor->textView.textStorage.string.length)) && text.length == 1) { + // first character detection is enough for the removal to be done + if([self detectStyle:NSMakeRange(0, 1)]) { + [self removeAttributes:NSMakeRange(0, _editor->textView.textStorage.string.length)]; + // do the replacing manually + [TextInsertionUtils replaceText:text inView:_editor->textView at:range additionalAttributes:nullptr editor:_editor]; + return YES; + } + } + return NO; +} + // returns mention params if it exists - (MentionParams *)getMentionParamsAt:(NSUInteger)location { NSRange mentionRange = NSMakeRange(0, 0); diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h index 70362de01..9ef3db241 100644 --- a/ios/utils/StyleHeaders.h +++ b/ios/utils/StyleHeaders.h @@ -26,6 +26,7 @@ - (void)manageLinkTypingAttributes; - (void)handleAutomaticLinks:(NSString *)word inRange:(NSRange)wordRange; - (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange; +- (BOOL)handleLeadingLinkReplacement:(NSRange)range replacementText:(NSString *)text; @end @interface MentionStyle : NSObject @@ -35,6 +36,7 @@ - (void)handleExistingMentions; - (void)manageMentionEditing; - (void)manageMentionTypingAttributes; +- (BOOL)handleLeadingMentionReplacement:(NSRange)range replacementText:(NSString *)text; - (MentionParams *)getMentionParamsAt:(NSUInteger)location; - (NSRange)getFullMentionRangeAt:(NSUInteger)location; - (NSValue *)getActiveMentionRange; diff --git a/ios/utils/TextInsertionUtils.h b/ios/utils/TextInsertionUtils.h index d9f97914e..463815e42 100644 --- a/ios/utils/TextInsertionUtils.h +++ b/ios/utils/TextInsertionUtils.h @@ -1,7 +1,6 @@ #import -#import "ReactNativeRichTextEditorView.h" @interface TextInsertionUtils : NSObject -+ (void)insertText:(NSString*)text inView:(UITextView*)textView at:(NSInteger)index additionalAttributes:(NSDictionary*)additionalAttrs editor:(ReactNativeRichTextEditorView *)editor; -+ (void)replaceText:(NSString*)text inView:(UITextView*)textView at:(NSRange)range additionalAttributes:(NSDictionary*)additionalAttrs editor:(ReactNativeRichTextEditorView *)editor; ++ (void)insertText:(NSString*)text inView:(UITextView*)textView at:(NSInteger)index additionalAttributes:(NSDictionary*)additionalAttrs editor:(id)editor; ++ (void)replaceText:(NSString*)text inView:(UITextView*)textView at:(NSRange)range additionalAttributes:(NSDictionary*)additionalAttrs editor:(id)editor; @end diff --git a/ios/utils/TextInsertionUtils.mm b/ios/utils/TextInsertionUtils.mm index 5fef5afbf..57d6d5bad 100644 --- a/ios/utils/TextInsertionUtils.mm +++ b/ios/utils/TextInsertionUtils.mm @@ -1,9 +1,9 @@ #import "TextInsertionUtils.h" #import "UIView+React.h" - +#import "ReactNativeRichTextEditorView.h" @implementation TextInsertionUtils -+ (void)insertText:(NSString*)text inView:(UITextView*)textView at:(NSInteger)index additionalAttributes:(NSDictionary*)additionalAttrs editor:(ReactNativeRichTextEditorView *)editor { ++ (void)insertText:(NSString*)text inView:(UITextView*)textView at:(NSInteger)index additionalAttributes:(NSDictionary*)additionalAttrs editor:(id)editor { NSMutableDictionary *copiedAttrs = [textView.typingAttributes mutableCopy]; if(additionalAttrs != nullptr) { [copiedAttrs addEntriesFromDictionary: additionalAttrs]; @@ -15,12 +15,13 @@ + (void)insertText:(NSString*)text inView:(UITextView*)textView at:(NSInteger)in [textView reactFocus]; textView.selectedRange = NSMakeRange(index + text.length, 0); - if(editor != nullptr) { - editor->recentlyChangedRange = NSMakeRange(index, text.length); + ReactNativeRichTextEditorView *typedEditor = (ReactNativeRichTextEditorView *)editor; + if(typedEditor != nullptr) { + typedEditor->recentlyChangedRange = NSMakeRange(index, text.length); } } -+ (void)replaceText:(NSString*)text inView:(UITextView*)textView at:(NSRange)range additionalAttributes:(NSDictionary*)additionalAttrs editor:(ReactNativeRichTextEditorView *)editor { ++ (void)replaceText:(NSString*)text inView:(UITextView*)textView at:(NSRange)range additionalAttributes:(NSDictionary*)additionalAttrs editor:(id)editor { [textView.textStorage replaceCharactersInRange:range withString:text]; if(additionalAttrs != nullptr) { [textView.textStorage addAttributes:additionalAttrs range:NSMakeRange(range.location, [text length])]; @@ -29,8 +30,9 @@ + (void)replaceText:(NSString*)text inView:(UITextView*)textView at:(NSRange)ran [textView reactFocus]; textView.selectedRange = NSMakeRange(range.location + text.length, 0); - if(editor != nullptr) { - editor->recentlyChangedRange = NSMakeRange(range.location, text.length); + ReactNativeRichTextEditorView *typedEditor = (ReactNativeRichTextEditorView *)editor; + if(typedEditor != nullptr) { + typedEditor->recentlyChangedRange = NSMakeRange(range.location, text.length); } } @end