Skip to content

Commit 9ea58f7

Browse files
authored
feat: iOS unordered list (#98)
* feat: unordered list * feat: improve interactions between headings and list * feat: unordered list parsing * feat: add unordered list "- " shortcut
1 parent 0e68147 commit 9ea58f7

13 files changed

Lines changed: 367 additions & 37 deletions

ios/ReactNativeRichTextEditorView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
1414
@public EditorConfig *config;
1515
@public NSMutableDictionary<NSAttributedStringKey, id> *defaultTypingAttributes;
1616
@public NSDictionary<NSNumber *, id<BaseStyleProtocol>> *stylesDict;
17+
@public BOOL blockEmitting;
1718
}
1819
- (CGSize)measureSize:(CGFloat)maxWidth;
1920
- (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url;

ios/ReactNativeRichTextEditorView.mm

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ - (void)setDefaults {
7777
_recentlyEmittedString = @"";
7878
_recentlyEmittedHtml = @"";
7979
_emitHtml = NO;
80+
blockEmitting = NO;
8081
_emitFocusBlur = YES;
8182

8283
defaultTypingAttributes = [[NSMutableDictionary<NSAttributedStringKey, id> alloc] init];
@@ -91,7 +92,8 @@ - (void)setDefaults {
9192
@([MentionStyle getStyleType]): [[MentionStyle alloc] initWithEditor:self],
9293
@([H1Style getStyleType]): [[H1Style alloc] initWithEditor:self],
9394
@([H2Style getStyleType]): [[H2Style alloc] initWithEditor:self],
94-
@([H3Style getStyleType]): [[H3Style alloc] initWithEditor:self]
95+
@([H3Style getStyleType]): [[H3Style alloc] initWithEditor:self],
96+
@([UnorderedListStyle getStyleType]): [[UnorderedListStyle alloc] initWithEditor:self]
9597
};
9698

9799
_conflictingStyles = @{
@@ -102,9 +104,10 @@ - (void)setDefaults {
102104
@([InlineCodeStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType])],
103105
@([LinkStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), @([MentionStyle getStyleType])],
104106
@([MentionStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType])],
105-
@([H1Style getStyleType]): @[@([H2Style getStyleType]), @([H3Style getStyleType])],
106-
@([H2Style getStyleType]): @[@([H1Style getStyleType]), @([H3Style getStyleType])],
107-
@([H3Style getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType])]
107+
@([H1Style getStyleType]): @[@([H2Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType])],
108+
@([H2Style getStyleType]): @[@([H1Style getStyleType]), @([H3Style getStyleType]), @([UnorderedListStyle getStyleType])],
109+
@([H3Style getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([UnorderedListStyle getStyleType])],
110+
@([UnorderedListStyle getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType]), @([H3Style getStyleType])],
108111
};
109112

110113
_blockingStyles = @{
@@ -117,7 +120,8 @@ - (void)setDefaults {
117120
@([MentionStyle getStyleType]): @[],
118121
@([H1Style getStyleType]): @[],
119122
@([H2Style getStyleType]): @[],
120-
@([H3Style getStyleType]): @[]
123+
@([H3Style getStyleType]): @[],
124+
@([UnorderedListStyle getStyleType]): @[],
121125
};
122126

123127
_editorParser = [[EditorParser alloc] initWithEditor:self];
@@ -205,6 +209,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
205209
defaultTypingAttributes[NSFontAttributeName] = [config primaryFont];
206210
defaultTypingAttributes[NSUnderlineColorAttributeName] = [config primaryColor];
207211
defaultTypingAttributes[NSStrikethroughColorAttributeName] = [config primaryColor];
212+
defaultTypingAttributes[NSParagraphStyleAttributeName] = [[NSParagraphStyle alloc] init];
208213
textView.typingAttributes = defaultTypingAttributes;
209214

210215
if(stylePropChanged) {
@@ -496,7 +501,7 @@ - (void)tryUpdatingActiveStyles {
496501
.isH3 = [_activeStyles containsObject: @([H3Style getStyleType])],
497502
.isCodeBlock = NO, // [_activeStyles containsObject: @([CodeBlockStyle getStyleType])],
498503
.isBlockQuote = NO, // [_activeStyles containsObject: @([BlockQuoteStyle getStyleType])],
499-
.isUnorderedList = NO, // [_activeStyles containsObject: @([UnorderedListStyle getStyleType])],
504+
.isUnorderedList = [_activeStyles containsObject: @([UnorderedListStyle getStyleType])],
500505
.isOrderedList = NO, // [_activeStyles containsObject: @([OrderedListStyle getStyleType]]],
501506
.isImage = NO // [_activeStyles containsObject: @([ImageStyle getStyleType]]],
502507
});
@@ -560,11 +565,13 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
560565
[self toggleParagraphStyle:[H2Style getStyleType]];
561566
} else if([commandName isEqualToString:@"toggleH3"]) {
562567
[self toggleParagraphStyle:[H3Style getStyleType]];
568+
} else if([commandName isEqualToString:@"toggleUnorderedList"]) {
569+
[self toggleParagraphStyle:[UnorderedListStyle getStyleType]];
563570
}
564571
}
565572

566573
- (std::shared_ptr<ReactNativeRichTextEditorViewEventEmitter>)getEventEmitter {
567-
if(_eventEmitter != nullptr) {
574+
if(_eventEmitter != nullptr && !blockEmitting) {
568575
auto emitter = static_cast<const ReactNativeRichTextEditorViewEventEmitter &>(*_eventEmitter);
569576
return std::make_shared<ReactNativeRichTextEditorViewEventEmitter>(emitter);
570577
} else {
@@ -644,6 +651,7 @@ - (void)toggleRegularStyle:(StyleType)type {
644651

645652
if([self handleStyleBlocksAndConflicts:type range:textView.selectedRange]) {
646653
[styleClass applyStyle:textView.selectedRange];
654+
[self tryUpdatingHeight];
647655
[self tryUpdatingActiveStyles];
648656
}
649657
}
@@ -655,7 +663,6 @@ - (void)toggleParagraphStyle:(StyleType)type {
655663

656664
if([self handleStyleBlocksAndConflicts:type range:paragraphRange]) {
657665
[styleClass applyStyle:paragraphRange];
658-
// height needs to be checked as well with paragraph styles
659666
[self tryUpdatingHeight];
660667
[self tryUpdatingActiveStyles];
661668
}
@@ -669,6 +676,7 @@ - (void)addLinkAt:(NSInteger)start end:(NSInteger)end text:(NSString *)text url:
669676
NSRange linkRange = NSMakeRange(start, end - start);
670677
if([self handleStyleBlocksAndConflicts:[LinkStyle getStyleType] range:linkRange]) {
671678
[linkStyleClass addLink:text url:url range:linkRange manual:YES];
679+
[self tryUpdatingHeight];
672680
[self tryUpdatingActiveStyles];
673681
}
674682
}
@@ -680,6 +688,7 @@ - (void)addMention:(NSString *)indicator text:(NSString *)text attributes:(NSStr
680688

681689
if([self handleStyleBlocksAndConflicts:[MentionStyle getStyleType] range:[[mentionStyleClass getActiveMentionRange] rangeValue]]) {
682690
[mentionStyleClass addMention:indicator text:text attributes:attributes];
691+
[self tryUpdatingHeight];
683692
[self tryUpdatingActiveStyles];
684693
}
685694
}
@@ -690,6 +699,7 @@ - (void)startMentionWithIndicator:(NSString *)indicator {
690699

691700
if([self handleStyleBlocksAndConflicts:[MentionStyle getStyleType] range:[[mentionStyleClass getActiveMentionRange] rangeValue]]) {
692701
[mentionStyleClass startMentionWithIndicator:indicator];
702+
[self tryUpdatingHeight];
693703
[self tryUpdatingActiveStyles];
694704
}
695705
}
@@ -781,14 +791,17 @@ - (void)anyTextMayHaveBeenModified {
781791

782792
// do all the stuff only if the text really changed
783793
if(![textView.textStorage.string isEqualToString:_recentlyEmittedString]) {
794+
// emptying input
795+
if(textView.textStorage.string.length == 0) {
796+
// reset typing attribtues
797+
textView.typingAttributes = defaultTypingAttributes;
798+
}
784799

785800
// placholder management
786801
if(_recentlyEmittedString.length == 0 && textView.textStorage.string.length > 0) {
787802
[self setPlaceholderLabelShown:NO];
788803
} else if(_recentlyEmittedString.length > 0 && textView.textStorage.string.length == 0) {
789804
[self setPlaceholderLabelShown:YES];
790-
// also typing attributes get reset when emptying the input
791-
textView.typingAttributes = defaultTypingAttributes;
792805
}
793806

794807
// modified words handling
@@ -816,12 +829,12 @@ - (void)anyTextMayHaveBeenModified {
816829

817830
// set the recently emitted string
818831
_recentlyEmittedString = [textView.textStorage.string copy];
819-
820-
// update height on each character change
821-
[self tryUpdatingHeight];
822-
// update active styles as well
823-
[self tryUpdatingActiveStyles];
824832
}
833+
834+
// update height on each character change
835+
[self tryUpdatingHeight];
836+
// update active styles as well
837+
[self tryUpdatingActiveStyles];
825838
}
826839

827840
// MARK: - UITextView delegate methods
@@ -855,7 +868,19 @@ - (void)textViewDidEndEditing:(UITextView *)textView {
855868

856869
- (bool)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
857870
_recentlyChangedRange = NSMakeRange(range.location, text.length);
858-
return true;
871+
872+
UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getStyleType])];
873+
if(uStyle != nullptr) {
874+
// removing first line list fix
875+
[uStyle handleBackspaceInRange:range replacementText:text];
876+
// creating unordered list from "- "
877+
if([uStyle tryHandlingListShorcutInRange:range replacementText:text]) {
878+
// we successfully added a list -> so we reject the text change
879+
return NO;
880+
}
881+
}
882+
883+
return YES;
859884
}
860885

861886
- (void)textViewDidChangeSelection:(UITextView *)textView {

ios/parser/EditorParser.mm

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ - (NSString *)parseToHtml {
2222
NSMutableString *result = [[NSMutableString alloc] initWithString: @"<html>"];
2323
NSSet<NSNumber *>*previousActiveStyles = [[NSSet<NSNumber *> alloc]init];
2424
BOOL newLine = YES;
25+
BOOL inUnorderedList = NO;
2526
unichar lastCharacter = 0;
2627

2728
for(int i = 0; i < _editor->textView.textStorage.length; i++) {
@@ -58,7 +59,15 @@ - (NSString *)parseToHtml {
5859
}
5960

6061
// append closing paragraph tag
61-
[result appendString:@"</p>"];
62+
if([previousActiveStyles containsObject:@([UnorderedListStyle getStyleType])] ||
63+
[previousActiveStyles containsObject:@([H1Style getStyleType])] ||
64+
[previousActiveStyles containsObject:@([H2Style getStyleType])] ||
65+
[previousActiveStyles containsObject:@([H3Style getStyleType])]
66+
) {
67+
// do nothing, proper closing paragraph tags have been already appended
68+
} else {
69+
[result appendString:@"</p>"];
70+
}
6271

6372
// clear the previous styles
6473
previousActiveStyles = [[NSSet<NSNumber *> alloc]init];
@@ -69,7 +78,29 @@ - (NSString *)parseToHtml {
6978
// new line - open the paragraph
7079
if(newLine) {
7180
newLine = NO;
72-
[result appendString:@"\n<p>"];
81+
82+
// handle ending lists
83+
if(inUnorderedList && ![currentActiveStyles containsObject:@([UnorderedListStyle getStyleType])]) {
84+
inUnorderedList = NO;
85+
[result appendString:@"\n</ul>"];
86+
}
87+
88+
// handle starting lists
89+
if(!inUnorderedList && [currentActiveStyles containsObject:@([UnorderedListStyle getStyleType])]) {
90+
inUnorderedList = YES;
91+
[result appendString:@"\n<ul>"];
92+
}
93+
94+
// don't add the <p> tag if paragraph styles are present
95+
if([currentActiveStyles containsObject:@([UnorderedListStyle getStyleType])] ||
96+
[currentActiveStyles containsObject:@([H1Style getStyleType])] ||
97+
[currentActiveStyles containsObject:@([H2Style getStyleType])] ||
98+
[currentActiveStyles containsObject:@([H3Style getStyleType])]
99+
) {
100+
[result appendString:@"\n"];
101+
} else {
102+
[result appendString:@"\n<p>"];
103+
}
73104
}
74105

75106
// get styles that have ended: they are sorted in an ascending manner
@@ -117,7 +148,18 @@ - (NSString *)parseToHtml {
117148
}
118149

119150
// finish the paragraph
120-
[result appendString:@"</p>"];
151+
// handle ending paragraph styles
152+
if([previousActiveStyles containsObject:@([UnorderedListStyle getStyleType])]) {
153+
[result appendString:@"\n</ul>"];
154+
} else if(
155+
[previousActiveStyles containsObject:@([H1Style getStyleType])] ||
156+
[previousActiveStyles containsObject:@([H2Style getStyleType])] ||
157+
[previousActiveStyles containsObject:@([H3Style getStyleType])]
158+
) {
159+
// do nothing, heading closing tag has already ben appended
160+
} else {
161+
[result appendString:@"</p>"];
162+
}
121163
}
122164

123165
[result appendString: @"\n</html>"];
@@ -183,6 +225,8 @@ - (NSString *)tagContentForStyle:(NSNumber *)style openingTag:(BOOL)openingTag l
183225
return @"h2";
184226
} else if([style isEqualToNumber:@([H3Style getStyleType])]) {
185227
return @"h3";
228+
} else if([style isEqualToNumber:@([UnorderedListStyle getStyleType])]) {
229+
return @"li";
186230
}
187231
return @"";
188232
}
@@ -289,7 +333,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)html {
289333
gettingTagName = NO;
290334
gettingTagParams = NO;
291335

292-
if([currentTagName isEqualToString:@"p"] || [currentTagName isEqualToString:@"br"]) {
336+
if([currentTagName isEqualToString:@"p"] || [currentTagName isEqualToString:@"br"] || [currentTagName isEqualToString:@"li"]) {
293337
// do nothing, we don't include these tags in styles
294338
} else if(!closingTag) {
295339
// we finish opening tag - get its location and optionally params and put them under tag name key in ongoingTags
@@ -299,8 +343,19 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)html {
299343
[tagArr addObject:[currentTagParams copy]];
300344
}
301345
ongoingTags[currentTagName] = tagArr;
346+
347+
// skip one newline after lists' opening tags
348+
if([currentTagName isEqualToString:@"ul"]) {
349+
i += 1;
350+
}
302351
} else {
303352
// we finish closing tags - pack tag name, tag range and optionally tag params into an entry that goes inside initiallyProcessedTags
353+
354+
// skip one newline that was added before lists' closing tags
355+
if([currentTagName isEqualToString:@"ul"]) {
356+
plainText = [[plainText substringWithRange: NSMakeRange(0, plainText.length - 1)] mutableCopy];
357+
}
358+
304359
NSMutableArray *tagEntry = [[NSMutableArray alloc] init];
305360

306361
NSArray *tagData = ongoingTags[currentTagName];
@@ -409,6 +464,8 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)html {
409464
} else if([tagName isEqualToString:@"h3"]) {
410465
[styleArr addObject:@([H3Style getStyleType])];
411466
}
467+
} else if([tagName isEqualToString:@"ul"]) {
468+
[styleArr addObject:@([UnorderedListStyle getStyleType])];
412469
}
413470

414471
stylePair.rangeValue = tagRangeValue;

ios/styles/H2Style.mm

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,3 @@ @implementation H2Style
44
+ (StyleType)getStyleType { return H2; }
55
- (CGFloat)getHeadingFontSize { return 24; } // TODO: headings style config
66
@end
7-

ios/styles/H3Style.mm

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,3 @@ @implementation H3Style
44
+ (StyleType)getStyleType { return H3; }
55
- (CGFloat)getHeadingFontSize { return 20; } // TODO: headings style config
66
@end
7-
8-

ios/styles/HeadingStyleBase.mm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ - (void)addAttributes:(NSRange)range {
4141
}
4242
];
4343
[_editor->textView.textStorage endEditing];
44+
45+
// also toggle typing attributes
46+
[self addTypingAttributes];
4447
}
4548

4649
// will always be called on empty paragraphs so only typing attributes can be changed

0 commit comments

Comments
 (0)