Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/example/src/constants/editorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export const DEFAULT_STYLES: StylesState = {
mention: DEFAULT_STYLE_STATE,
checkboxList: DEFAULT_STYLE_STATE,
alignment: 'left',
customStyle: {
foregroundColor: '',
backgroundColor: '',
},
};

export const DEFAULT_LINK_STATE = {
Expand Down
86 changes: 84 additions & 2 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "EnrichedTextInputView.h"
#import "AlignmentUtils.h"
#import "AttachmentLayoutUtils.h"
#import "ColorExtension.h"
#import "CoreText/CoreText.h"
#import "DotReplacementUtils.h"
#import "HtmlParser.h"
Expand All @@ -17,6 +18,7 @@
#import "WordsUtils.h"
#import "ZeroWidthSpaceUtils.h"
#import <React/RCTConversions.h>
#import <React/RCTConvert.h>
#import <ReactNativeEnriched/EnrichedTextInputViewComponentDescriptor.h>
#import <ReactNativeEnriched/EventEmitters.h>
#import <ReactNativeEnriched/Props.h>
Expand Down Expand Up @@ -60,6 +62,7 @@ @implementation EnrichedTextInputView {
NSString *_submitBehavior;
NSDictionary<NSAttributedStringKey, id> *_capturedAttributesBeforeChange;
NSString *_recentlyEmittedAlignment;
CustomStyleData *_recentlyEmittedCustomStyle;
}

@synthesize blockEmitting = blockEmitting;
Expand Down Expand Up @@ -1092,13 +1095,23 @@ - (void)tryUpdatingActiveStyles {
updateNeeded = YES;
}

// detect custom style change
CustomStyle *customStyle = stylesDict[@([CustomStyle getType])];
CustomStyleData *currentCustomStyle =
[customStyle getCustomStyleDataAt:textView.selectedRange.location];
if (currentCustomStyle != _recentlyEmittedCustomStyle &&
![currentCustomStyle isEqual:_recentlyEmittedCustomStyle]) {
updateNeeded = YES;
}

if (updateNeeded) {
auto emitter = [self getEventEmitter];
if (emitter != nullptr) {
// update activeStyles and blockedStyles only if emitter is available
_activeStyles = newActiveStyles;
_blockedStyles = newBlockedStyles;
_recentlyEmittedAlignment = currentAlignment;
_recentlyEmittedCustomStyle = currentCustomStyle;

emitter->onChangeState(
{.bold = GET_STYLE_STATE([BoldStyle getType]),
Expand All @@ -1120,7 +1133,14 @@ - (void)tryUpdatingActiveStyles {
.codeBlock = GET_STYLE_STATE([CodeBlockStyle getType]),
.image = GET_STYLE_STATE([ImageStyle getType]),
.checkboxList = GET_STYLE_STATE([CheckboxListStyle getType]),
.alignment = [currentAlignment UTF8String]});
.alignment = [currentAlignment UTF8String],
.customStyle = {
.foregroundColor =
[[currentCustomStyle.foregroundColor rgbaString] UTF8String]
?: "",
.backgroundColor =
[[currentCustomStyle.backgroundColor rgbaString] UTF8String]
?: ""}});
}
}

Expand Down Expand Up @@ -1268,6 +1288,9 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
if (!_placeholderLabel.isHidden) {
[self refreshPlaceholderLabelStyles];
}
} else if ([commandName isEqualToString:@"setStyle"]) {
NSString *styleJSON = (NSString *)args[0];
[self setStyle:styleJSON];
}
}

Expand Down Expand Up @@ -1475,6 +1498,52 @@ - (void)toggleRegularStyle:(StyleType)type {
}
}

- (void)setStyle:(NSString *)styleJSON {
NSData *jsonData = [styleJSON dataUsingEncoding:NSUTF8StringEncoding];
if (jsonData == nil)
return;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:nil];
if (dict == nil)
return;
Comment thread
kacperzolkiewski marked this conversation as resolved.
Outdated

NSRange selectedRange = textView.selectedRange;
CustomStyle *customStyleClass =
(CustomStyle *)stylesDict[@([CustomStyle getType])];
if (customStyleClass == nil)
return;

if (![StyleUtils handleStyleBlocksAndConflicts:[CustomStyle getType]
range:selectedRange
forHost:self]) {
return;
}

// Convert raw JSON values (NSNumber ARGB integers from processColor) to
// UIColor. NSNull is passed through as-is so mergeFromDict: can clear
// the color when the caller explicitly passes null.
NSMutableDictionary *processedDict = [NSMutableDictionary new];

id fgRaw = dict[@"foregroundColor"];
if (fgRaw != nil) {
processedDict[@"foregroundColor"] = [fgRaw isKindOfClass:[NSNull class]]
? [NSNull null]
: [RCTConvert UIColor:fgRaw];
}

id bgRaw = dict[@"backgroundColor"];
if (bgRaw != nil) {
processedDict[@"backgroundColor"] = [bgRaw isKindOfClass:[NSNull class]]
? [NSNull null]
: [RCTConvert UIColor:bgRaw];
}

[customStyleClass applyStyleFromDict:processedDict
selectedRange:selectedRange];
[self anyTextMayHaveBeenModified];
}

- (void)toggleCheckboxList:(BOOL)checked {
CheckboxListStyle *style =
(CheckboxListStyle *)stylesDict[@([CheckboxListStyle getType])];
Expand Down Expand Up @@ -1827,6 +1896,10 @@ - (void)emitOnContextMenuItemPressEvent:(NSString *)itemText {
AlignmentStyle *alignmentStyle = stylesDict[@([AlignmentStyle getType])];
NSString *currentAlignment = [alignmentStyle getStyleState];

CustomStyle *customStyle = stylesDict[@([CustomStyle getType])];
CustomStyleData *contextCustomStyleData =
[customStyle getCustomStyleDataAt:textView.selectedRange.location];

emitter->onContextMenuItemPress(
{.itemText = [itemText toCppString],
.selectedText = [selectedText toCppString],
Expand All @@ -1853,7 +1926,16 @@ - (void)emitOnContextMenuItemPressEvent:(NSString *)itemText {
.image = GET_STYLE_STATE([ImageStyle getType]),
.mention = GET_STYLE_STATE([MentionStyle getType]),
.checkboxList = GET_STYLE_STATE([CheckboxListStyle getType]),
.alignment = [currentAlignment UTF8String]}});
.alignment = [currentAlignment UTF8String],
.customStyle = {
.foregroundColor =
[[contextCustomStyleData.foregroundColor rgbaString]
UTF8String]
?: "",
.backgroundColor =
[[contextCustomStyleData.backgroundColor rgbaString]
UTF8String]
?: ""}}});
}
}

Expand Down
16 changes: 16 additions & 0 deletions ios/customStyleData/CustomStyleData.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once
#import <UIKit/UIKit.h>

@interface CustomStyleData : NSObject <NSCopying>

@property(nonatomic, strong, nullable) UIColor *foregroundColor;
@property(nonatomic, strong, nullable) UIColor *backgroundColor;

- (BOOL)isEmpty;

// Applies a partial update from a dict (keys: "foregroundColor",
// "backgroundColor"). A key absent from the dict leaves the field unchanged;
// NSNull or any non-UIColor value clears it; a UIColor value sets it.
- (void)mergeFromDict:(NSDictionary *)dict;

@end
46 changes: 46 additions & 0 deletions ios/customStyleData/CustomStyleData.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#import "CustomStyleData.h"

@implementation CustomStyleData

- (BOOL)isEmpty {
return _foregroundColor == nil && _backgroundColor == nil;
}

- (void)mergeFromDict:(NSDictionary *)dict {
id fgVal = dict[@"foregroundColor"];
if (fgVal != nil) {
self.foregroundColor =
[fgVal isKindOfClass:[UIColor class]] ? (UIColor *)fgVal : nil;
}
id bgVal = dict[@"backgroundColor"];
if (bgVal != nil) {
self.backgroundColor =
[bgVal isKindOfClass:[UIColor class]] ? (UIColor *)bgVal : nil;
}
}

- (BOOL)isEqual:(id)object {
if (self == object)
return YES;
if (![object isKindOfClass:[CustomStyleData class]])
return NO;
CustomStyleData *other = (CustomStyleData *)object;
BOOL fgEqual = (_foregroundColor == other.foregroundColor) ||
[_foregroundColor isEqual:other.foregroundColor];
BOOL bgEqual = (_backgroundColor == other.backgroundColor) ||
[_backgroundColor isEqual:other.backgroundColor];
return fgEqual && bgEqual;
}

- (NSUInteger)hash {
return [_foregroundColor hash] ^ [_backgroundColor hash];
}

- (id)copyWithZone:(NSZone *)zone {
CustomStyleData *copy = [[CustomStyleData allocWithZone:zone] init];
copy.foregroundColor = self.foregroundColor;
copy.backgroundColor = self.backgroundColor;
return copy;
}

@end
4 changes: 4 additions & 0 deletions ios/extensions/ColorExtension.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
@interface UIColor (ColorExtension)
- (BOOL)isEqualToColor:(UIColor *)otherColor;
- (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha;
- (NSString *)rgbaString;
/// Parses a CSS rgba() string, e.g. @"rgba(255, 0, 0, 1.00)". Returns nil if
/// the string is not a valid rgba() value.
+ (UIColor *_Nullable)colorFromRgbaString:(NSString *_Nullable)rgba;
@end
56 changes: 56 additions & 0 deletions ios/extensions/ColorExtension.mm
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,60 @@ - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha {
}
return self;
}

- (NSString *)rgbaString {
CGFloat red = 0.0;
CGFloat green = 0.0;
CGFloat blue = 0.0;
CGFloat alpha = 0.0;

// getRed:green:blue:alpha: returns YES if the color can be converted to RGB.
// It natively handles monochrome/grayscale colors as well.
if ([self getRed:&red green:&green blue:&blue alpha:&alpha]) {
// Convert 0.0-1.0 floats to 0-255 integers for RGB
int r = (int)round(red * 255.0);
int g = (int)round(green * 255.0);
int b = (int)round(blue * 255.0);
Comment thread
Copilot marked this conversation as resolved.
Outdated

return
[NSString stringWithFormat:@"rgba(%d, %d, %d, %.2f)", r, g, b, alpha];
}

// Fallback for unsupported color
return @"";
}

+ (UIColor *)colorFromRgbaString:(NSString *)rgba {
Comment thread
Copilot marked this conversation as resolved.
Outdated
if (rgba.length == 0)
return nil;

static NSRegularExpression *regex;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
regex = [NSRegularExpression
regularExpressionWithPattern:
@"rgba\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,"
@"\\s*([\\d.]+)\\s*\\)"
options:NSRegularExpressionCaseInsensitive
error:nil];
});

NSTextCheckingResult *match =
[regex firstMatchInString:rgba
options:0
range:NSMakeRange(0, rgba.length)];
if (!match || match.numberOfRanges < 5)
return nil;

CGFloat r =
[[rgba substringWithRange:[match rangeAtIndex:1]] integerValue] / 255.0;
CGFloat g =
[[rgba substringWithRange:[match rangeAtIndex:2]] integerValue] / 255.0;
CGFloat b =
[[rgba substringWithRange:[match rangeAtIndex:3]] integerValue] / 255.0;
CGFloat a = [[rgba substringWithRange:[match rangeAtIndex:4]] doubleValue];

return [UIColor colorWithRed:r green:g blue:b alpha:a];
}

@end
Loading