Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,13 @@ class EnrichedTextInputViewManager :
TODO("Not yet implemented")
}

override fun setStyle(
view: EnrichedTextInputView?,
styleJSON: String,
) {
// TODO: Implement
}
Comment thread
kacperzolkiewski marked this conversation as resolved.

override fun measure(
context: Context,
localData: ReadableMap?,
Expand Down
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
93 changes: 89 additions & 4 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,53 @@ - (void)toggleRegularStyle:(StyleType)type {
}
}

- (void)setStyle:(NSString *)styleJSON {
NSData *jsonData = [styleJSON dataUsingEncoding:NSUTF8StringEncoding];
if (jsonData == nil)
return;
id parsed = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:nil];
if (![parsed isKindOfClass:[NSDictionary class]])
return;
NSDictionary *dict = (NSDictionary *)parsed;

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 +1897,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 +1927,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 All @@ -1880,8 +1963,10 @@ - (bool)textView:(UITextView *)textView
replacementText:(NSString *)text {
// Capture the attributes at range.location that are being replaced
// (autocorrect / predictive) so didProcessEditing: can re-stamp them onto the
// replacement.
if (range.length > 0) {
// replacement. Skip pure deletions (text.length == 0) — there is no incoming
// text to receive attributes, and capturing here would cause the deleted
// character's CustomStyleData to be re-stamped onto the widened editedRange.
if (range.length > 0 && text.length > 0) {
_capturedAttributesBeforeChange =
[textView.textStorage attributesAtIndex:range.location
effectiveRange:NULL];
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)(red * 255.0 + 0.5);
int g = (int)(green * 255.0 + 0.5);
int b = (int)(blue * 255.0 + 0.5);

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

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

+ (UIColor *_Nullable)colorFromRgbaString:(NSString *_Nullable)rgba {
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