Skip to content

Commit dd3127e

Browse files
authored
feat: add headings (#90)
1 parent ff3f0aa commit dd3127e

10 files changed

Lines changed: 226 additions & 13 deletions

ios/ReactNativeRichTextEditorView.mm

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ - (void)setDefaults {
8686
@([StrikethroughStyle getStyleType]): [[StrikethroughStyle alloc] initWithEditor:self],
8787
@([InlineCodeStyle getStyleType]): [[InlineCodeStyle alloc] initWithEditor:self],
8888
@([LinkStyle getStyleType]): [[LinkStyle alloc] initWithEditor:self],
89-
@([MentionStyle getStyleType]): [[MentionStyle alloc] initWithEditor:self]
89+
@([MentionStyle getStyleType]): [[MentionStyle alloc] initWithEditor:self],
90+
@([H1Style getStyleType]): [[H1Style alloc] initWithEditor:self],
91+
@([H2Style getStyleType]): [[H2Style alloc] initWithEditor:self],
92+
@([H3Style getStyleType]): [[H3Style alloc] initWithEditor:self]
9093
};
9194

9295
_conflictingStyles = @{
@@ -96,7 +99,10 @@ - (void)setDefaults {
9699
@([StrikethroughStyle getStyleType]) : @[],
97100
@([InlineCodeStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType])],
98101
@([LinkStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), @([MentionStyle getStyleType])],
99-
@([MentionStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType])]
102+
@([MentionStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType])],
103+
@([H1Style getStyleType]): @[@([H2Style getStyleType]), @([H3Style getStyleType])],
104+
@([H2Style getStyleType]): @[@([H1Style getStyleType]), @([H3Style getStyleType])],
105+
@([H3Style getStyleType]): @[@([H1Style getStyleType]), @([H2Style getStyleType])]
100106
};
101107

102108
_blockingStyles = @{
@@ -107,6 +113,9 @@ - (void)setDefaults {
107113
@([InlineCodeStyle getStyleType]) : @[],
108114
@([LinkStyle getStyleType]): @[],
109115
@([MentionStyle getStyleType]): @[],
116+
@([H1Style getStyleType]): @[],
117+
@([H2Style getStyleType]): @[],
118+
@([H3Style getStyleType]): @[]
110119
};
111120

112121
_editorParser = [[EditorParser alloc] initWithEditor:self];
@@ -320,15 +329,18 @@ - (CGSize)measureSize:(CGFloat)maxWidth {
320329
// edge case: empty input should still be of a height of a single line, so we add a mock "I" character
321330
if([currentStr length] == 0 ) {
322331
[currentStr appendAttributedString:
323-
[[NSAttributedString alloc] initWithString:@"I" attributes:defaultTypingAttributes]
332+
[[NSAttributedString alloc] initWithString:@"I" attributes:textView.typingAttributes]
324333
];
325334
}
326335

327336
// edge case: trailing newlines aren't counted towards height calculations, so we add a mock "I" character
328-
if([currentStr length] > 0 && [[currentStr.string substringFromIndex:[currentStr length] - 1] isEqualToString:@"\n"]) {
329-
[currentStr appendAttributedString:
330-
[[NSAttributedString alloc] initWithString:@"I" attributes:defaultTypingAttributes]
331-
];
337+
if(currentStr.length > 0) {
338+
unichar lastChar = [currentStr.string characterAtIndex:currentStr.length-1];
339+
if([[NSCharacterSet newlineCharacterSet] characterIsMember:lastChar]) {
340+
[currentStr appendAttributedString:
341+
[[NSAttributedString alloc] initWithString:@"I" attributes:textView.typingAttributes]
342+
];
343+
}
332344
}
333345

334346
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)currentStr);
@@ -455,9 +467,9 @@ - (void)tryUpdatingActiveStyles {
455467
.isInlineCode = [_activeStyles containsObject: @([InlineCodeStyle getStyleType])],
456468
.isLink = [_activeStyles containsObject: @([LinkStyle getStyleType])],
457469
.isMention = [_activeStyles containsObject: @([MentionStyle getStyleType])],
458-
.isH1 = NO, // [_activeStyles containsObject: @([H1Style getStyleType])],
459-
.isH2 = NO, // [_activeStyles containsObject: @([H2Style getStyleType])],
460-
.isH3 = NO, // [_activeStyles containsObject: @([H3Style getStyleType])],
470+
.isH1 = [_activeStyles containsObject: @([H1Style getStyleType])],
471+
.isH2 = [_activeStyles containsObject: @([H2Style getStyleType])],
472+
.isH3 = [_activeStyles containsObject: @([H3Style getStyleType])],
461473
.isCodeBlock = NO, // [_activeStyles containsObject: @([CodeBlockStyle getStyleType])],
462474
.isBlockQuote = NO, // [_activeStyles containsObject: @([BlockQuoteStyle getStyleType])],
463475
.isUnorderedList = NO, // [_activeStyles containsObject: @([UnorderedListStyle getStyleType])],
@@ -518,6 +530,12 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
518530
} else if([commandName isEqualToString:@"startMention"]) {
519531
NSString *indicator = (NSString *)args[0];
520532
[self startMentionWithIndicator:indicator];
533+
} else if([commandName isEqualToString:@"toggleH1"]) {
534+
[self toggleParagraphStyle:[H1Style getStyleType]];
535+
} else if([commandName isEqualToString:@"toggleH2"]) {
536+
[self toggleParagraphStyle:[H2Style getStyleType]];
537+
} else if([commandName isEqualToString:@"toggleH3"]) {
538+
[self toggleParagraphStyle:[H3Style getStyleType]];
521539
}
522540
}
523541

@@ -606,6 +624,19 @@ - (void)toggleRegularStyle:(StyleType)type {
606624
}
607625
}
608626

627+
- (void)toggleParagraphStyle:(StyleType)type {
628+
id<BaseStyleProtocol> styleClass = stylesDict[@(type)];
629+
// we always pass whole paragraph/s range to these styles
630+
NSRange paragraphRange = [textView.textStorage.string paragraphRangeForRange:textView.selectedRange];
631+
632+
if([self handleStyleBlocksAndConflicts:type range:paragraphRange]) {
633+
[styleClass applyStyle:paragraphRange];
634+
// height needs to be checked as well with paragraph styles
635+
[self tryUpdatingHeight];
636+
[self tryUpdatingActiveStyles];
637+
}
638+
}
639+
609640
- (void)addLinkAt:(NSInteger)start end:(NSInteger)end text:(NSString *)text url:(NSString *)url {
610641
LinkStyle *linkStyleClass = (LinkStyle *)stylesDict[@([LinkStyle getStyleType])];
611642
if(linkStyleClass == nullptr) { return; }

ios/parser/EditorParser.mm

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ - (NSString *)tagContentForStyle:(NSNumber *)style openingTag:(BOOL)openingTag l
177177
} else {
178178
return @"mention";
179179
}
180+
} else if([style isEqualToNumber:@([H1Style getStyleType])]) {
181+
return @"h1";
182+
} else if([style isEqualToNumber:@([H2Style getStyleType])]) {
183+
return @"h2";
184+
} else if([style isEqualToNumber:@([H3Style getStyleType])]) {
185+
return @"h3";
180186
}
181187
return @"";
182188
}
@@ -364,7 +370,7 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)html {
364370
[styleArr addObject:@([InlineCodeStyle getStyleType])];
365371
} else if([tagName isEqualToString:@"a"]) {
366372
[styleArr addObject:@([LinkStyle getStyleType])];
367-
// cut only url from the href="..." string
373+
// cut only the url from the href="..." string
368374
NSString *url = [params substringWithRange:NSMakeRange(6, params.length - 7)];
369375
stylePair.styleValue = url;
370376
} else if([tagName isEqualToString:@"mention"]) {
@@ -395,6 +401,14 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)html {
395401
mentionParams.attributes = formattedAttrsString;
396402

397403
stylePair.styleValue = mentionParams;
404+
} else if([[tagName substringWithRange:NSMakeRange(0, 1)] isEqualToString: @"h"]) {
405+
if([tagName isEqualToString:@"h1"]) {
406+
[styleArr addObject:@([H1Style getStyleType])];
407+
} else if([tagName isEqualToString:@"h2"]) {
408+
[styleArr addObject:@([H2Style getStyleType])];
409+
} else if([tagName isEqualToString:@"h3"]) {
410+
[styleArr addObject:@([H3Style getStyleType])];
411+
}
398412
}
399413

400414
stylePair.rangeValue = tagRangeValue;

ios/styles/H1Style.mm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#import "StyleHeaders.h"
2+
3+
@implementation H1Style
4+
+ (StyleType)getStyleType { return H1; }
5+
- (CGFloat)getHeadingFontSize { return 32; } // TODO: headings style config
6+
@end

ios/styles/H2Style.mm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import "StyleHeaders.h"
2+
3+
@implementation H2Style
4+
+ (StyleType)getStyleType { return H2; }
5+
- (CGFloat)getHeadingFontSize { return 24; } // TODO: headings style config
6+
@end
7+

ios/styles/H3Style.mm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#import "StyleHeaders.h"
2+
3+
@implementation H3Style
4+
+ (StyleType)getStyleType { return H3; }
5+
- (CGFloat)getHeadingFontSize { return 20; } // TODO: headings style config
6+
@end
7+
8+

ios/styles/HeadingStyleBase.mm

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#import "StyleHeaders.h"
2+
#import "ReactNativeRichTextEditorView.h"
3+
#import "FontExtension.h"
4+
#import "OccurenceUtils.h"
5+
6+
@implementation HeadingStyleBase {
7+
ReactNativeRichTextEditorView *_editor;
8+
}
9+
10+
+ (StyleType)getStyleType { return None; }
11+
12+
- (CGFloat)getHeadingFontSize { return 0; }
13+
14+
- (instancetype)initWithEditor:(id)editor {
15+
self = [super init];
16+
_editor = (ReactNativeRichTextEditorView *) editor;
17+
return self;
18+
}
19+
20+
// the range will already be the full paragraph/s range
21+
// but if the paragraph is empty it still is of length 0
22+
- (void)applyStyle:(NSRange)range {
23+
BOOL isStylePresent = [self detectStyle:range];
24+
if(range.length >= 1) {
25+
isStylePresent ? [self removeAttributes:range] : [self addAttributes:range];
26+
} else {
27+
isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes];
28+
}
29+
}
30+
31+
// the range will already be the proper full paragraph/s range
32+
- (void)addAttributes:(NSRange)range {
33+
[_editor->textView.textStorage beginEditing];
34+
[_editor->textView.textStorage enumerateAttribute:NSFontAttributeName inRange:range options:0
35+
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
36+
UIFont *font = (UIFont *)value;
37+
if(font != nullptr) {
38+
UIFont *newFont = [font setSize:[self getHeadingFontSize]];
39+
[_editor->textView.textStorage addAttribute:NSFontAttributeName value:newFont range:range];
40+
}
41+
}
42+
];
43+
[_editor->textView.textStorage endEditing];
44+
}
45+
46+
// will always be called on empty paragraphs so only typing attributes can be changed
47+
- (void)addTypingAttributes {
48+
UIFont *currentFontAttr = (UIFont *)_editor->textView.typingAttributes[NSFontAttributeName];
49+
if(currentFontAttr != nullptr) {
50+
NSMutableDictionary *newTypingAttrs = [_editor->textView.typingAttributes mutableCopy];
51+
newTypingAttrs[NSFontAttributeName] = [currentFontAttr setSize:[self getHeadingFontSize]];
52+
_editor->textView.typingAttributes = newTypingAttrs;
53+
}
54+
}
55+
56+
// we need to remove the style from the whole paragraph
57+
- (void)removeAttributes:(NSRange)range {
58+
NSRange paragraphRange = [_editor->textView.textStorage.string paragraphRangeForRange:range];
59+
60+
[_editor->textView.textStorage beginEditing];
61+
[_editor->textView.textStorage enumerateAttribute:NSFontAttributeName inRange:paragraphRange options:0
62+
usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
63+
if([self styleCondition:value :range]) {
64+
UIFont *newFont = [(UIFont *)value setSize:[[_editor->config primaryFontSize] floatValue]];
65+
[_editor->textView.textStorage addAttribute:NSFontAttributeName value:newFont range:range];
66+
}
67+
}
68+
];
69+
[_editor->textView.textStorage endEditing];
70+
71+
// typing attributes still need to be removed
72+
UIFont *currentFontAttr = (UIFont *)_editor->textView.typingAttributes[NSFontAttributeName];
73+
if(currentFontAttr != nullptr) {
74+
NSMutableDictionary *newTypingAttrs = [_editor->textView.typingAttributes mutableCopy];
75+
newTypingAttrs[NSFontAttributeName] = [currentFontAttr setSize:[[_editor->config primaryFontSize] floatValue]];
76+
_editor->textView.typingAttributes = newTypingAttrs;
77+
}
78+
}
79+
80+
- (void)removeTypingAttributes {
81+
// all the heading still needs to be removed because this function may be called in conflicting styles logic
82+
// typing attributes already get removed in there as well
83+
[self removeAttributes:_editor->textView.selectedRange];
84+
}
85+
86+
- (BOOL)styleCondition:(id _Nullable)value :(NSRange)range {
87+
UIFont *font = (UIFont *)value;
88+
return font != nullptr && font.pointSize == [self getHeadingFontSize];
89+
}
90+
91+
- (BOOL)detectStyle:(NSRange)range {
92+
if(range.length >= 1) {
93+
return [OccurenceUtils detect:NSFontAttributeName withEditor:_editor inRange:range
94+
withCondition: ^BOOL(id _Nullable value, NSRange range) {
95+
return [self styleCondition:value :range];
96+
}
97+
];
98+
} else {
99+
UIFont *currentFontAttr = (UIFont *)_editor->textView.typingAttributes[NSFontAttributeName];
100+
if(currentFontAttr == nullptr) {
101+
return false;
102+
}
103+
return currentFontAttr.pointSize == [self getHeadingFontSize];
104+
}
105+
}
106+
107+
- (BOOL)anyOccurence:(NSRange)range {
108+
return [OccurenceUtils any:NSFontAttributeName withEditor:_editor inRange:range
109+
withCondition:^BOOL(id _Nullable value, NSRange range) {
110+
return [self styleCondition:value :range];
111+
}
112+
];
113+
}
114+
115+
- (NSArray<StylePair *> *_Nullable)findAllOccurences:(NSRange)range {
116+
return [OccurenceUtils all:NSFontAttributeName withEditor:_editor inRange:range
117+
withCondition:^BOOL(id _Nullable value, NSRange range) {
118+
return [self styleCondition:value :range];
119+
}
120+
];
121+
}
122+
123+
@end
124+

ios/utils/FontExtension.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#pragma once
44

55
@interface UIFont (FontExtension)
6-
76
- (BOOL)isBold;
87
- (UIFont *)setBold;
98
- (UIFont *)removeBold;
@@ -12,5 +11,5 @@
1211
- (UIFont *)removeItalic;
1312
- (BOOL)isMonospace:(EditorConfig *)withConfig;
1413
- (UIFont *)withFontTraits:(UIFont *)from;
15-
14+
- (UIFont *)setSize:(CGFloat)size;
1615
@end

ios/utils/FontExtension.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,14 @@ - (UIFont *)withFontTraits:(UIFont *)from {
8282
return newFont;
8383
}
8484

85+
- (UIFont *)setSize:(CGFloat)size {
86+
UIFontDescriptor *newFontDescriptor = [self.fontDescriptor fontDescriptorWithSize:size];
87+
if(newFontDescriptor != nullptr) {
88+
return [UIFont fontWithDescriptor:newFontDescriptor size:0];
89+
} else {
90+
RCTLogWarn(@"[RichTextEditor]: Couldn't apply heading style to the font.");
91+
return self;
92+
}
93+
}
94+
8595
@end

ios/utils/StyleHeaders.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,16 @@
3838
- (NSRange)getFullMentionRangeAt:(NSUInteger)location;
3939
- (NSValue *)getActiveMentionRange;
4040
@end
41+
42+
@interface HeadingStyleBase : NSObject<BaseStyleProtocol>
43+
- (CGFloat)getHeadingFontSize;
44+
@end
45+
46+
@interface H1Style : HeadingStyleBase
47+
@end
48+
49+
@interface H2Style : HeadingStyleBase
50+
@end
51+
52+
@interface H3Style : HeadingStyleBase
53+
@end

ios/utils/StyleTypeEnum.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#import <UIKit/UIKit.h>
33

44
typedef NS_ENUM(NSInteger, StyleType) {
5+
None,
56
Bold,
67
Italic,
78
Underline,

0 commit comments

Comments
 (0)