diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 93f0e8cb3..6026089b5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2690,7 +2690,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 77a7129540c0e06ab6be9a2bcb887d3d2e594431 ReactCodegen: b62625187ce853918021a7a9178cc406e9820d56 ReactCommon: d07170f92e0e853091a70b2741b7c43f5dfdea73 - ReactNativeEnriched: b11d66700889cd36c9938a825736de1929349b24 + ReactNativeEnriched: ce7a893fdefce993826a3177da6811b513e26526 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 6af5d1e0290903c82b3e0cb6836a5f898c1c4634 diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm index f2eb77601..6f97def79 100644 --- a/ios/styles/BlockQuoteStyle.mm +++ b/ios/styles/BlockQuoteStyle.mm @@ -118,16 +118,33 @@ - (void)removeTypingAttributes { [self removeAttributes:_input->textView.selectedRange]; } -// removing first quote line by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange -// so we try guessing that a point should be deleted here - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if([self detectStyle:_input->textView.selectedRange] && - NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) && - [text isEqualToString:@""] + if( + [self detectStyle:_input->textView.selectedRange] && + NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) && + [text isEqualToString:@""] ) { + // removing first quote line by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange + // so we try guessing that a line should be deleted here NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:_input->textView.selectedRange]; [self removeAttributes:paragraphRange]; return YES; + } else if( + [self detectStyle:_input->textView.selectedRange] && + [text isEqualToString:@""] + ) { + // other case; make sure removing all the (non newline) text from a quto line also removes the line itself + NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:range]; + NSValue *nonNewlineVal = [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:paragraphRange].firstObject; + if(nonNewlineVal == nullptr) { + return NO; + } + NSRange nonNewlineRange = [nonNewlineVal rangeValue]; + if(NSEqualRanges(range, nonNewlineRange)) { + [self removeAttributes:range]; + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:_input withSelection:YES]; + return YES; + } } return NO; } @@ -145,22 +162,11 @@ - (BOOL)detectStyle:(NSRange)range { } ]; } else { - NSInteger searchLocation = range.location; - if(searchLocation == _input->textView.textStorage.length) { - NSParagraphStyle *pStyle = _input->textView.typingAttributes[NSParagraphStyleAttributeName]; - return [self styleCondition:pStyle :NSMakeRange(0, 0)]; - } - - NSRange paragraphRange = NSMakeRange(0, 0); - NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length); - NSParagraphStyle *paragraph = [_input->textView.textStorage - attribute:NSParagraphStyleAttributeName - atIndex:searchLocation - longestEffectiveRange: ¶graphRange - inRange:inputRange + return [OccurenceUtils detect:NSParagraphStyleAttributeName withInput:_input atIndex:range.location checkPrevious:YES + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } ]; - - return [self styleCondition:paragraph :NSMakeRange(0, 0)]; } } diff --git a/ios/styles/BoldStyle.mm b/ios/styles/BoldStyle.mm index 075c856a9..2dd507396 100644 --- a/ios/styles/BoldStyle.mm +++ b/ios/styles/BoldStyle.mm @@ -98,8 +98,11 @@ - (BOOL)detectStyle:(NSRange)range { } ]; } else { - UIFont *currentFontAttr = (UIFont *)_input->textView.typingAttributes[NSFontAttributeName]; - return [self styleCondition:currentFontAttr :range]; + return [OccurenceUtils detect:NSFontAttributeName withInput:_input atIndex:range.location checkPrevious:NO + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; } } diff --git a/ios/styles/HeadingStyleBase.mm b/ios/styles/HeadingStyleBase.mm index cf6289626..4d3beacab 100644 --- a/ios/styles/HeadingStyleBase.mm +++ b/ios/styles/HeadingStyleBase.mm @@ -117,11 +117,11 @@ - (BOOL)detectStyle:(NSRange)range { } ]; } else { - UIFont *currentFontAttr = (UIFont *)[self typedInput]->textView.typingAttributes[NSFontAttributeName]; - if(currentFontAttr == nullptr) { - return false; - } - return currentFontAttr.pointSize == [self getHeadingFontSize]; + return [OccurenceUtils detect:NSFontAttributeName withInput:[self typedInput] atIndex:range.location checkPrevious:YES + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; } } diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm index 949f99ccf..94d8ad00b 100644 --- a/ios/styles/InlineCodeStyle.mm +++ b/ios/styles/InlineCodeStyle.mm @@ -140,8 +140,11 @@ - (BOOL)detectStyle:(NSRange)range { return detected; } else { - UIColor *currentBgColorAttr = (UIColor *)_input->textView.typingAttributes[NSBackgroundColorAttributeName]; - return [self styleCondition:currentBgColorAttr :range]; + return [OccurenceUtils detect:NSBackgroundColorAttributeName withInput:_input atIndex:range.location checkPrevious:NO + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; } } diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm index 0a642f9e0..6ec4debc4 100644 --- a/ios/styles/ItalicStyle.mm +++ b/ios/styles/ItalicStyle.mm @@ -83,11 +83,11 @@ - (BOOL)detectStyle:(NSRange)range { } ]; } else { - UIFont *currentFontAttr = (UIFont *)_input->textView.typingAttributes[NSFontAttributeName]; - if(currentFontAttr == nullptr) { - return false; - } - return [currentFontAttr isItalic]; + return [OccurenceUtils detect:NSFontAttributeName withInput:_input atIndex:range.location checkPrevious:NO + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; } } diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm index 832166476..2963b0a06 100644 --- a/ios/styles/OrderedListStyle.mm +++ b/ios/styles/OrderedListStyle.mm @@ -130,16 +130,33 @@ - (void)removeTypingAttributes { [self removeAttributes:_input->textView.selectedRange]; } -// removing first list point by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange -// so we try guessing that a point should be deleted here - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if([self detectStyle:_input->textView.selectedRange] && - NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) && - [text isEqualToString:@""] + if( + [self detectStyle:_input->textView.selectedRange] && + NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) && + [text isEqualToString:@""] ) { + // removing first list point by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange + // so we try guessing that a point should be deleted here NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:_input->textView.selectedRange]; [self removeAttributes:paragraphRange]; return YES; + } else if( + [self detectStyle:_input->textView.selectedRange] && + [text isEqualToString:@""] + ) { + // other case; make sure removing all the (non newline) text from a list item also removes the item itself + NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:range]; + NSValue *nonNewlineVal = [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:paragraphRange].firstObject; + if(nonNewlineVal == nullptr) { + return NO; + } + NSRange nonNewlineRange = [nonNewlineVal rangeValue]; + if(NSEqualRanges(range, nonNewlineRange)) { + [self removeAttributes:range]; + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:_input withSelection:YES]; + return YES; + } } return NO; } @@ -187,22 +204,11 @@ - (BOOL)detectStyle:(NSRange)range { } ]; } else { - NSInteger searchLocation = range.location; - if(searchLocation == _input->textView.textStorage.length) { - NSParagraphStyle *pStyle = _input->textView.typingAttributes[NSParagraphStyleAttributeName]; - return [self styleCondition:pStyle :NSMakeRange(0, 0)]; - } - - NSRange paragraphRange = NSMakeRange(0, 0); - NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length); - NSParagraphStyle *paragraph = [_input->textView.textStorage - attribute:NSParagraphStyleAttributeName - atIndex:searchLocation - longestEffectiveRange: ¶graphRange - inRange:inputRange + return [OccurenceUtils detect:NSParagraphStyleAttributeName withInput:_input atIndex:range.location checkPrevious:YES + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } ]; - - return [self styleCondition:paragraph :NSMakeRange(0, 0)]; } } diff --git a/ios/styles/StrikethroughStyle.mm b/ios/styles/StrikethroughStyle.mm index 6a6088da8..82932384c 100644 --- a/ios/styles/StrikethroughStyle.mm +++ b/ios/styles/StrikethroughStyle.mm @@ -56,8 +56,11 @@ - (BOOL)detectStyle:(NSRange)range { } ]; } else { - NSNumber *currenStrikethroughAttr = (NSNumber *)_input->textView.typingAttributes[NSStrikethroughStyleAttributeName]; - return currenStrikethroughAttr != nullptr; + return [OccurenceUtils detect:NSStrikethroughStyleAttributeName withInput:_input atIndex:range.location checkPrevious:NO + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; } } diff --git a/ios/styles/UnderlineStyle.mm b/ios/styles/UnderlineStyle.mm index d602b73c7..f36aa0899 100644 --- a/ios/styles/UnderlineStyle.mm +++ b/ios/styles/UnderlineStyle.mm @@ -88,8 +88,11 @@ - (BOOL)detectStyle:(NSRange)range { } ]; } else { - NSNumber *currentUnderlineAttr = (NSNumber *)_input->textView.typingAttributes[NSUnderlineStyleAttributeName]; - return [self styleCondition:currentUnderlineAttr :range]; + return [OccurenceUtils detect:NSUnderlineStyleAttributeName withInput:_input atIndex:range.location checkPrevious:NO + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } + ]; } } diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm index bab837362..6cec6bdd9 100644 --- a/ios/styles/UnorderedListStyle.mm +++ b/ios/styles/UnorderedListStyle.mm @@ -130,16 +130,33 @@ - (void)removeTypingAttributes { [self removeAttributes:_input->textView.selectedRange]; } -// removing first list point by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange -// so we try guessing that a point should be deleted here - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text { - if([self detectStyle:_input->textView.selectedRange] && - NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) && - [text isEqualToString:@""] + if( + [self detectStyle:_input->textView.selectedRange] && + NSEqualRanges(_input->textView.selectedRange, NSMakeRange(0, 0)) && + [text isEqualToString:@""] ) { + // removing first list point by backspacing doesn't remove typing attributes because it doesn't run textViewDidChange + // so we try guessing that a point should be deleted here NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:_input->textView.selectedRange]; [self removeAttributes:paragraphRange]; return YES; + } else if( + [self detectStyle:_input->textView.selectedRange] && + [text isEqualToString:@""] + ) { + // other case; make sure removing all the (non newline) text from a list item also removes the item itself + NSRange paragraphRange = [_input->textView.textStorage.string paragraphRangeForRange:range]; + NSValue *nonNewlineVal = [ParagraphsUtils getNonNewlineRangesIn:_input->textView range:paragraphRange].firstObject; + if(nonNewlineVal == nullptr) { + return NO; + } + NSRange nonNewlineRange = [nonNewlineVal rangeValue]; + if(NSEqualRanges(range, nonNewlineRange)) { + [self removeAttributes:range]; + [TextInsertionUtils replaceText:text at:range additionalAttributes:nullptr input:_input withSelection:YES]; + return YES; + } } return NO; } @@ -187,22 +204,11 @@ - (BOOL)detectStyle:(NSRange)range { } ]; } else { - NSInteger searchLocation = range.location; - if(searchLocation == _input->textView.textStorage.length) { - NSParagraphStyle *pStyle = _input->textView.typingAttributes[NSParagraphStyleAttributeName]; - return [self styleCondition:pStyle :NSMakeRange(0, 0)]; - } - - NSRange paragraphRange = NSMakeRange(0, 0); - NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length); - NSParagraphStyle *paragraph = [_input->textView.textStorage - attribute:NSParagraphStyleAttributeName - atIndex:searchLocation - longestEffectiveRange: ¶graphRange - inRange:inputRange + return [OccurenceUtils detect:NSParagraphStyleAttributeName withInput:_input atIndex:range.location checkPrevious:YES + withCondition:^BOOL(id _Nullable value, NSRange range) { + return [self styleCondition:value :range]; + } ]; - - return [self styleCondition:paragraph :NSMakeRange(0, 0)]; } } diff --git a/ios/utils/OccurenceUtils.h b/ios/utils/OccurenceUtils.h index dc6b501cd..885cb2121 100644 --- a/ios/utils/OccurenceUtils.h +++ b/ios/utils/OccurenceUtils.h @@ -9,6 +9,12 @@ withInput:(EnrichedTextInputView* _Nonnull)input inRange:(NSRange)range withCondition:(BOOL (NS_NOESCAPE ^_Nonnull)(id _Nullable value, NSRange range))condition; ++ (BOOL)detect + :(NSAttributedStringKey _Nonnull)key + withInput:(EnrichedTextInputView* _Nonnull)input + atIndex:(NSUInteger)index + checkPrevious:(BOOL)check + withCondition:(BOOL (NS_NOESCAPE ^_Nonnull)(id _Nullable value, NSRange range))condition; + (BOOL)detectMultiple :(NSArray *_Nonnull)keys withInput:(EnrichedTextInputView* _Nonnull)input diff --git a/ios/utils/OccurenceUtils.mm b/ios/utils/OccurenceUtils.mm index 6248c735c..7c5b707bb 100644 --- a/ios/utils/OccurenceUtils.mm +++ b/ios/utils/OccurenceUtils.mm @@ -19,6 +19,37 @@ + (BOOL)detect return totalLength == range.length; } +// checkPrevious flag is used for styles like lists or blockquotes +// it means that first character of paragraph will be checked instead if the detection is not in input's selected range and at the end of the input ++ (BOOL)detect + :(NSAttributedStringKey _Nonnull)key + withInput:(EnrichedTextInputView* _Nonnull)input + atIndex:(NSUInteger)index + checkPrevious:(BOOL)checkPrev + withCondition:(BOOL (NS_NOESCAPE ^_Nonnull)(id _Nullable value, NSRange range))condition +{ + NSRange detectionRange = NSMakeRange(index, 0); + id attrValue; + if(NSEqualRanges(input->textView.selectedRange, detectionRange)) { + attrValue = input->textView.typingAttributes[key]; + } else if(index == input->textView.textStorage.string.length) { + if(checkPrev) { + NSRange paragraphRange = [input->textView.textStorage.string paragraphRangeForRange:detectionRange]; + if(paragraphRange.location == detectionRange.location) { + return NO; + } else { + return [self detect:key withInput:input inRange:NSMakeRange(paragraphRange.location, 1) withCondition:condition]; + } + } else { + return NO; + } + } else { + NSRange attrRange = NSMakeRange(0, 0); + attrValue = [input->textView.textStorage attribute:key atIndex:index effectiveRange:&attrRange]; + } + return condition(attrValue, NSMakeRange(index, 0)); +} + + (BOOL)detectMultiple :(NSArray *_Nonnull)keys withInput:(EnrichedTextInputView* _Nonnull)input