From 8272e255522d8001c8cf06bc6c5dc24dbc0d5f40 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Wed, 19 Nov 2025 23:21:06 +0100
Subject: [PATCH 01/12] feat: add foreground color style for iOS
---
.../enriched/EnrichedTextInputViewManager.kt | 12 +-
apps/example/src/App.tsx | 17 +-
apps/example/src/components/ColorPreview.tsx | 32 ++
apps/example/src/components/Toolbar.tsx | 29 +-
.../src/components/ToolbarColorButton.tsx | 47 +++
ios/EnrichedTextInputView.mm | 158 +++++++---
ios/inputParser/InputParser.mm | 127 +++++---
ios/styles/ColorStyle.mm | 297 ++++++++++++++++++
ios/utils/ColorExtension.h | 8 +
ios/utils/ColorExtension.mm | 154 +++++++++
ios/utils/ColorUtils.m | 8 +
ios/utils/StyleHeaders.h | 7 +
ios/utils/StyleTypeEnum.h | 1 +
src/EnrichedTextInput.tsx | 10 +
src/EnrichedTextInputNativeComponent.ts | 16 +
15 files changed, 820 insertions(+), 103 deletions(-)
create mode 100644 apps/example/src/components/ColorPreview.tsx
create mode 100644 apps/example/src/components/ToolbarColorButton.tsx
create mode 100644 ios/styles/ColorStyle.mm
create mode 100644 ios/utils/ColorUtils.m
diff --git a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt
index d74f08a9e..2d0719085 100644
--- a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt
+++ b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt
@@ -338,13 +338,11 @@ class EnrichedTextInputViewManager :
view?.verifyAndToggleStyle(EnrichedSpans.UNORDERED_LIST)
}
- override fun addLink(
- view: EnrichedTextInputView?,
- start: Int,
- end: Int,
- text: String,
- url: String,
- ) {
+ override fun toggleColor(view: EnrichedTextInputView?, color: String) {
+ // no-op for now
+ }
+
+ override fun addLink(view: EnrichedTextInputView?, start: Int, end: Int, text: String, url: String) {
view?.addLink(start, end, text, url)
}
diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx
index d91708992..11b393cc2 100644
--- a/apps/example/src/App.tsx
+++ b/apps/example/src/App.tsx
@@ -26,6 +26,8 @@ import {
DEFAULT_IMAGE_WIDTH,
prepareImageDimensions,
} from './utils/prepareImageDimensions';
+import type { OnChangeColorEvent } from '../../src/EnrichedTextInputNativeComponent';
+import ColorPreview from './components/ColorPreview';
type StylesState = OnChangeStateEvent;
@@ -45,6 +47,7 @@ const DEFAULT_STYLE_STATE = {
const DEFAULT_STYLES: StylesState = {
bold: DEFAULT_STYLE_STATE,
+ colored: DEFAULT_STYLE_STATE,
italic: DEFAULT_STYLE_STATE,
underline: DEFAULT_STYLE_STATE,
strikeThrough: DEFAULT_STYLE_STATE,
@@ -94,6 +97,7 @@ export default function App() {
const [stylesState, setStylesState] = useState(DEFAULT_STYLES);
const [currentLink, setCurrentLink] =
useState(DEFAULT_LINK_STATE);
+ const [selectionColor, setSelectionColor] = useState(PRIMARY_COLOR);
const ref = useRef(null);
@@ -293,6 +297,14 @@ export default function App() {
setSelection(sel);
};
+ const handleSelectionColorChange = (
+ e: NativeSyntheticEvent
+ ) => {
+ if (e.nativeEvent.color) {
+ setSelectionColor(e.nativeEvent.color);
+ }
+ };
+
return (
<>
handleChangeText(e.nativeEvent)}
onChangeHtml={(e) => handleChangeHtml(e.nativeEvent)}
onChangeState={(e) => handleChangeState(e.nativeEvent)}
+ onColorChangeInSelection={handleSelectionColorChange}
onLinkDetected={handleLinkDetected}
onMentionDetected={console.log}
onStartMention={handleStartMention}
@@ -330,6 +343,7 @@ export default function App() {
@@ -345,6 +359,7 @@ export default function App() {
/>
{DEBUG_SCROLLABLE && }
+
= ({ color }) => {
+ return (
+ <>
+
+ {color}
+ >
+ );
+};
+
+const styles = StyleSheet.create({
+ preview: {
+ marginVertical: 8,
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ },
+});
+
+export default ColorPreview;
diff --git a/apps/example/src/components/Toolbar.tsx b/apps/example/src/components/Toolbar.tsx
index 78335499b..3a21a0d0b 100644
--- a/apps/example/src/components/Toolbar.tsx
+++ b/apps/example/src/components/Toolbar.tsx
@@ -5,6 +5,7 @@ import type {
EnrichedTextInputInstance,
} from 'react-native-enriched';
import type { FC } from 'react';
+import { ToolbarColorButton } from './ToolbarColorButton';
const STYLE_ITEMS = [
{
@@ -79,6 +80,16 @@ const STYLE_ITEMS = [
name: 'ordered-list',
icon: 'list-ol',
},
+ {
+ name: 'color',
+ value: '#FF0000',
+ text: 'A',
+ },
+ {
+ name: 'color',
+ value: '#E6FF5C',
+ text: 'A',
+ },
] as const;
type Item = (typeof STYLE_ITEMS)[number];
@@ -89,6 +100,7 @@ export interface ToolbarProps {
editorRef?: React.RefObject;
onOpenLinkModal: () => void;
onSelectImage: () => void;
+ selectionColor: string | null;
}
export const Toolbar: FC = ({
@@ -96,6 +108,7 @@ export const Toolbar: FC = ({
editorRef,
onOpenLinkModal,
onSelectImage,
+ selectionColor,
}) => {
const handlePress = (item: Item) => {
const currentRef = editorRef?.current;
@@ -202,6 +215,10 @@ export const Toolbar: FC = ({
}
};
+ const handleColorButtonPress = (color: string) => {
+ editorRef?.current?.toggleColor(color);
+ };
+
const isActive = (item: Item) => {
switch (item.name) {
case 'bold':
@@ -246,7 +263,14 @@ export const Toolbar: FC = ({
};
const renderItem = ({ item }: ListRenderItemInfo- ) => {
- return (
+ return item.name === 'color' ? (
+
+ ) : (
= ({
);
};
- const keyExtractor = (item: Item) => item.name;
+ const keyExtractor = (item: Item) =>
+ item.name === 'color' ? item.value : item.name;
return (
void;
+ color: string;
+}
+
+export const ToolbarColorButton: FC = ({
+ text,
+ isActive,
+ onPress,
+ color,
+}) => {
+ const handlePress = () => {
+ onPress(color);
+ };
+
+ const containerStyle = useMemo(
+ () => [
+ styles.container,
+ { backgroundColor: isActive ? color : 'rgba(0, 26, 114, 0.8)' },
+ ],
+ [isActive, color]
+ );
+
+ return (
+
+ {text}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: 56,
+ height: 56,
+ },
+ text: {
+ color: 'white',
+ fontSize: 20,
+ },
+});
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index 28a806890..85a3fb223 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -15,6 +15,7 @@
#import
#import
#import
+#import "ColorExtension.h"
#define GET_STYLE_STATE(TYPE_ENUM) \
{ \
@@ -42,6 +43,7 @@ @implementation EnrichedTextInputView {
NSRange _recentlyActiveMentionRange;
NSString *_recentlyEmittedHtml;
BOOL _emitHtml;
+ NSString *_recentlyEmittedColor;
UILabel *_placeholderLabel;
UIColor *_placeholderColor;
BOOL _emitFocusBlur;
@@ -88,6 +90,7 @@ - (void)setDefaults {
_recentInputString = @"";
_recentlyEmittedHtml = @"\n\n";
_emitHtml = NO;
+ _recentlyEmittedColor = nil;
blockEmitting = NO;
_emitFocusBlur = YES;
_emitTextChange = NO;
@@ -104,6 +107,7 @@ - (void)setDefaults {
[[StrikethroughStyle alloc] initWithInput:self],
@([InlineCodeStyle getStyleType]) :
[[InlineCodeStyle alloc] initWithInput:self],
+ @([ColorStyle getStyleType]) : [[ColorStyle alloc] initWithInput:self],
@([LinkStyle getStyleType]) : [[LinkStyle alloc] initWithInput:self],
@([MentionStyle getStyleType]) : [[MentionStyle alloc] initWithInput:self],
@([H1Style getStyleType]) : [[H1Style alloc] initWithInput:self],
@@ -134,6 +138,7 @@ - (void)setDefaults {
@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]),
@([MentionStyle getStyleType])
],
+ @([ColorStyle getStyleType]) : @[@([InlineCodeStyle getStyleType]), @([MentionStyle getStyleType])],
@([MentionStyle getStyleType]) :
@[ @([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]) ],
@([H1Style getStyleType]) : @[
@@ -215,6 +220,7 @@ - (void)setDefaults {
blockingStyles = [@{
@([BoldStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ],
+ @([ColorStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ],
@([ItalicStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ],
@([UnderlineStyle getStyleType]) : @[ @([CodeBlockStyle getStyleType]) ],
@([StrikethroughStyle getStyleType]) :
@@ -1025,29 +1031,38 @@ - (void)tryUpdatingActiveStyles {
_blockedStyles = newBlockedStyles;
emitter->onChangeStateDeprecated({
- .isBold = [self isStyleActive:[BoldStyle getStyleType]],
- .isItalic = [self isStyleActive:[ItalicStyle getStyleType]],
- .isUnderline = [self isStyleActive:[UnderlineStyle getStyleType]],
- .isStrikeThrough =
- [self isStyleActive:[StrikethroughStyle getStyleType]],
- .isInlineCode = [self isStyleActive:[InlineCodeStyle getStyleType]],
- .isLink = [self isStyleActive:[LinkStyle getStyleType]],
- .isMention = [self isStyleActive:[MentionStyle getStyleType]],
- .isH1 = [self isStyleActive:[H1Style getStyleType]],
- .isH2 = [self isStyleActive:[H2Style getStyleType]],
- .isH3 = [self isStyleActive:[H3Style getStyleType]],
- .isH4 = [self isStyleActive:[H4Style getStyleType]],
- .isH5 = [self isStyleActive:[H5Style getStyleType]],
- .isH6 = [self isStyleActive:[H6Style getStyleType]],
- .isUnorderedList =
- [self isStyleActive:[UnorderedListStyle getStyleType]],
- .isOrderedList = [self isStyleActive:[OrderedListStyle getStyleType]],
- .isBlockQuote = [self isStyleActive:[BlockQuoteStyle getStyleType]],
- .isCodeBlock = [self isStyleActive:[CodeBlockStyle getStyleType]],
- .isImage = [self isStyleActive:[ImageStyle getStyleType]],
+ .isBold = [_activeStyles containsObject:@([BoldStyle getStyleType])],
+ .isItalic =
+ [_activeStyles containsObject:@([ItalicStyle getStyleType])],
+ .isUnderline =
+ [_activeStyles containsObject:@([UnderlineStyle getStyleType])],
+ .isStrikeThrough =
+ [_activeStyles containsObject:@([StrikethroughStyle getStyleType])],
+ .isColored = [_activeStyles containsObject: @([ColorStyle getStyleType])],
+ .isInlineCode =
+ [_activeStyles containsObject:@([InlineCodeStyle getStyleType])],
+ .isLink = [_activeStyles containsObject:@([LinkStyle getStyleType])],
+ .isMention =
+ [_activeStyles containsObject:@([MentionStyle getStyleType])],
+ .isH1 = [_activeStyles containsObject:@([H1Style getStyleType])],
+ .isH2 = [_activeStyles containsObject:@([H2Style getStyleType])],
+ .isH3 = [_activeStyles containsObject:@([H3Style getStyleType])],
+ .isH4 = [_activeStyles containsObject:@([H4Style getStyleType])],
+ .isH5 = [_activeStyles containsObject:@([H5Style getStyleType])],
+ .isH6 = [_activeStyles containsObject:@([H6Style getStyleType])],
+ .isUnorderedList =
+ [_activeStyles containsObject:@([UnorderedListStyle getStyleType])],
+ .isOrderedList =
+ [_activeStyles containsObject:@([OrderedListStyle getStyleType])],
+ .isBlockQuote =
+ [_activeStyles containsObject:@([BlockQuoteStyle getStyleType])],
+ .isCodeBlock =
+ [_activeStyles containsObject:@([CodeBlockStyle getStyleType])],
+ .isImage = [_activeStyles containsObject:@([ImageStyle getStyleType])],
});
emitter->onChangeState(
{.bold = GET_STYLE_STATE([BoldStyle getStyleType]),
+ .colored = GET_STYLE_STATE([ColorStyle getStyleType]),
.italic = GET_STYLE_STATE([ItalicStyle getStyleType]),
.underline = GET_STYLE_STATE([UnderlineStyle getStyleType]),
.strikeThrough = GET_STYLE_STATE([StrikethroughStyle getStyleType]),
@@ -1084,7 +1099,7 @@ - (void)tryUpdatingActiveStyles {
_recentlyActiveMentionParams = detectedMentionParams;
_recentlyActiveMentionRange = detectedMentionRange;
}
-
+ [self emitCurrentSelectionColorIfChanged];
// emit onChangeHtml event if needed
[self tryEmittingOnChangeHtmlEvent];
}
@@ -1135,19 +1150,23 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
} else if ([commandName isEqualToString:@"setValue"]) {
NSString *value = (NSString *)args[0];
[self setValue:value];
- } else if ([commandName isEqualToString:@"toggleBold"]) {
- [self toggleRegularStyle:[BoldStyle getStyleType]];
- } else if ([commandName isEqualToString:@"toggleItalic"]) {
- [self toggleRegularStyle:[ItalicStyle getStyleType]];
- } else if ([commandName isEqualToString:@"toggleUnderline"]) {
- [self toggleRegularStyle:[UnderlineStyle getStyleType]];
- } else if ([commandName isEqualToString:@"toggleStrikeThrough"]) {
- [self toggleRegularStyle:[StrikethroughStyle getStyleType]];
- } else if ([commandName isEqualToString:@"toggleInlineCode"]) {
- [self toggleRegularStyle:[InlineCodeStyle getStyleType]];
- } else if ([commandName isEqualToString:@"addLink"]) {
- NSInteger start = [((NSNumber *)args[0]) integerValue];
- NSInteger end = [((NSNumber *)args[1]) integerValue];
+ } else if([commandName isEqualToString:@"toggleBold"]) {
+ [self toggleRegularStyle: [BoldStyle getStyleType]];
+ } else if([commandName isEqualToString:@"toggleItalic"]) {
+ [self toggleRegularStyle: [ItalicStyle getStyleType]];
+ } else if([commandName isEqualToString:@"toggleUnderline"]) {
+ [self toggleRegularStyle: [UnderlineStyle getStyleType]];
+ } else if([commandName isEqualToString:@"toggleStrikeThrough"]) {
+ [self toggleRegularStyle: [StrikethroughStyle getStyleType]];
+ } else if([commandName isEqualToString:@"toggleColor"]) {
+ NSString *colorText = (NSString *)args[0];
+ UIColor *color = [UIColor colorFromString: colorText];
+ [self toggleColorStyle: color];
+ } else if([commandName isEqualToString:@"toggleInlineCode"]) {
+ [self toggleRegularStyle: [InlineCodeStyle getStyleType]];
+ } else if([commandName isEqualToString:@"addLink"]) {
+ NSInteger start = [((NSNumber*)args[0]) integerValue];
+ NSInteger end = [((NSNumber*)args[1]) integerValue];
NSString *text = (NSString *)args[2];
NSString *url = (NSString *)args[3];
[self addLinkAt:start end:end text:text url:url];
@@ -1282,9 +1301,50 @@ - (void)emitOnLinkDetectedEvent:(NSString *)text
}
}
-- (void)emitOnMentionDetectedEvent:(NSString *)text
- indicator:(NSString *)indicator
- attributes:(NSString *)attributes {
+- (void)emitCurrentSelectionColorIfChanged {
+ NSRange selRange = textView.selectedRange;
+ UIColor *uniformColor = nil;
+
+ if (selRange.length == 0) {
+ id colorAttr = textView.typingAttributes[NSForegroundColorAttributeName];
+ uniformColor = colorAttr ? (UIColor *)colorAttr : [config primaryColor];
+ } else {
+ // Selection range: check for uniform color
+ __block UIColor *firstColor = nil;
+ __block BOOL hasMultiple = NO;
+
+ [textView.textStorage enumerateAttribute:NSForegroundColorAttributeName
+ inRange:selRange
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range, BOOL *_Nonnull stop) {
+ UIColor *thisColor = value ? (UIColor *)value : [config primaryColor];
+ if (firstColor == nil) {
+ firstColor = thisColor;
+ } else if (![firstColor isEqual:thisColor]) {
+ hasMultiple = YES;
+ *stop = YES;
+ }
+ }];
+
+ if (!hasMultiple && firstColor != nil) {
+ uniformColor = firstColor;
+ }
+ }
+
+ NSString *hexColor = uniformColor ? [uniformColor hexString] : [config.primaryColor hexString];
+
+ if(![_recentlyEmittedColor isEqual: hexColor]) {
+ auto emitter = [self getEventEmitter];
+ if(emitter != nullptr) {
+ emitter->onColorChangeInSelection({
+ .color = [hexColor toCppString]
+ });
+ }
+ _recentlyEmittedColor = hexColor;
+ }
+}
+
+- (void)emitOnMentionDetectedEvent:(NSString *)text indicator:(NSString *)indicator attributes:(NSString *)attributes {
auto emitter = [self getEventEmitter];
if (emitter != nullptr) {
emitter->onMentionDetected({.text = [text toCppString],
@@ -1341,6 +1401,13 @@ - (void)requestHTML:(NSInteger)requestId {
// MARK: - Styles manipulation
+- (void)toggleColorStyle:(UIColor *)color {
+ ColorStyle *colorStyle = stylesDict[@(Colored)];
+
+ [colorStyle applyStyle:textView.selectedRange color: color];
+ [self anyTextMayHaveBeenModified];
+}
+
- (void)toggleRegularStyle:(StyleType)type {
id styleClass = stylesDict[@(type)];
@@ -1712,15 +1779,14 @@ - (void)textViewDidBeginEditing:(UITextView *)textView {
if (_emitFocusBlur) {
emitter->onInputFocus({});
}
-
- NSString *textAtSelection =
- [[[NSMutableString alloc] initWithString:textView.textStorage.string]
- substringWithRange:textView.selectedRange];
- emitter->onChangeSelection(
- {.start = static_cast(textView.selectedRange.location),
- .end = static_cast(textView.selectedRange.location +
- textView.selectedRange.length),
- .text = [textAtSelection toCppString]});
+
+ NSString *textAtSelection = [[[NSMutableString alloc] initWithString:textView.textStorage.string] substringWithRange: textView.selectedRange];
+ emitter->onChangeSelection({
+ .start = static_cast(textView.selectedRange.location),
+ .end = static_cast(textView.selectedRange.location + textView.selectedRange.length),
+ .text = [textAtSelection toCppString]
+ });
+ [self emitCurrentSelectionColorIfChanged];
}
// manage selection changes since textViewDidChangeSelection sometimes doesn't
// run on focus
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index d6e96afd5..38bf86f70 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -4,6 +4,7 @@
#import "StyleHeaders.h"
#import "TextInsertionUtils.h"
#import "UIView+React.h"
+#import "ColorExtension.h"
@implementation InputParser {
EnrichedTextInputView *_input;
@@ -34,8 +35,11 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
BOOL inBlockQuote = NO;
BOOL inCodeBlock = NO;
unichar lastCharacter = 0;
+
+ // Track current values for valued styles
+ UIColor *previousColor = nil;
- for (int i = 0; i < text.length; i++) {
+ for(int i = 0; i < text.length; i++) {
NSRange currentRange = NSMakeRange(offset + i, 1);
NSMutableSet *currentActiveStyles =
[[NSMutableSet alloc] init];
@@ -55,19 +59,28 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
[currentActiveStylesBeginning removeObjectForKey:type];
}
}
-
- NSString *currentCharacterStr =
- [_input->textView.textStorage.string substringWithRange:currentRange];
- unichar currentCharacterChar = [_input->textView.textStorage.string
- characterAtIndex:currentRange.location];
-
- if ([[NSCharacterSet newlineCharacterSet]
- characterIsMember:currentCharacterChar]) {
- if (newLine) {
- // we can either have an empty list item OR need to close the list and
- // put a BR in such a situation the existence of the list must be
- // checked on 0 length range, not on the newline character
- if (inOrderedList) {
+
+ // Handle valued styles changes
+ UIColor *currentColor = nil;
+
+ if([currentActiveStyles containsObject:@(Colored)]) {
+ ColorStyle *colorStyle = _input->stylesDict[@(Colored)];
+ currentColor = [colorStyle getColorAt:currentRange.location];
+ if(previousColor && ![currentColor isEqual:previousColor]) {
+ // Treat as end of previous color and start of new
+ [currentActiveStyles removeObject:@(Colored)];
+ currentActiveStylesBeginning[@(Colored)] = [NSNumber numberWithInt:i];
+ }
+ }
+
+ NSString *currentCharacterStr = [_input->textView.textStorage.string substringWithRange:currentRange];
+ unichar currentCharacterChar = [_input->textView.textStorage.string characterAtIndex:currentRange.location];
+
+ if([[NSCharacterSet newlineCharacterSet] characterIsMember:currentCharacterChar]) {
+ if(newLine) {
+ // we can either have an empty list item OR need to close the list and put a BR in such a situation
+ // the existence of the list must be checked on 0 length range, not on the newline character
+ if(inOrderedList) {
OrderedListStyle *oStyle = _input->stylesDict[@(OrderedList)];
BOOL detected =
[oStyle detectStyle:NSMakeRange(currentRange.location, 0)];
@@ -152,10 +165,11 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
[result appendString:@"
"];
}
}
-
- // clear the previous styles
- previousActiveStyles = [[NSSet alloc] init];
-
+
+ // clear the previous styles and valued trackers
+ previousActiveStyles = [[NSSet alloc]init];
+ previousColor = nil;
+
// next character opens new paragraph
newLine = YES;
} else {
@@ -243,33 +257,21 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
}
// get styles that have ended
- NSMutableSet *endedStyles =
- [previousActiveStyles mutableCopy];
- [endedStyles minusSet:currentActiveStyles];
-
- // also finish styles that should be ended becasue they are nested in a
- // style that ended
+ NSMutableSet *endedStyles = [previousActiveStyles mutableCopy];
+ [endedStyles minusSet: currentActiveStyles];
+
+ // also finish styles that should be ended because they are nested in a style that ended
NSMutableSet *fixedEndedStyles = [endedStyles mutableCopy];
NSMutableSet *stylesToBeReAdded = [[NSMutableSet alloc] init];
-
+
for (NSNumber *style in endedStyles) {
- NSInteger styleBeginning =
- [currentActiveStylesBeginning[style] integerValue];
-
- for (NSNumber *activeStyle in currentActiveStyles) {
- NSInteger activeStyleBeginning =
- [currentActiveStylesBeginning[activeStyle] integerValue];
-
- // we end the styles that began after the currently ended style but
- // not at the "i" (cause the old style ended at exactly "i-1" also the
- // ones that began in the exact same place but are "inner" in relation
- // to them due to StyleTypeEnum integer values
-
- if ((activeStyleBeginning > styleBeginning &&
- activeStyleBeginning < i) ||
- (activeStyleBeginning == styleBeginning &&
- activeStyleBeginning<
- i && [activeStyle integerValue]>[style integerValue])) {
+ NSInteger styleBeginning = [currentActiveStylesBeginning[style] integerValue];
+
+ for(NSNumber *activeStyle in currentActiveStyles) {
+ NSInteger activeStyleBeginning = [currentActiveStylesBeginning[activeStyle] integerValue];
+
+ if((activeStyleBeginning > styleBeginning && activeStyleBeginning < i) ||
+ (activeStyleBeginning == styleBeginning && activeStyleBeginning < i && [activeStyle integerValue] > [style integerValue])) {
[fixedEndedStyles addObject:activeStyle];
[stylesToBeReAdded addObject:activeStyle];
}
@@ -341,6 +343,8 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
// save current styles for next character's checks
previousActiveStyles = currentActiveStyles;
+
+ previousColor = currentColor;
}
// set last character
@@ -467,7 +471,18 @@ - (NSString *)tagContentForStyle:(NSNumber *)style
return @"u";
} else if ([style isEqualToNumber:@([StrikethroughStyle getStyleType])]) {
return @"s";
- } else if ([style isEqualToNumber:@([InlineCodeStyle getStyleType])]) {
+ } else if([style isEqualToNumber:@([ColorStyle getStyleType])]) {
+ if(openingTag) {
+ ColorStyle *colorSpan = _input->stylesDict[@([ColorStyle getStyleType])];
+ UIColor *color = [colorSpan getColorAt: location];
+ if(color) {
+ NSString *hex = [color hexString];
+ return [NSString stringWithFormat:@"font color=\"%@\"", hex];
+ };
+ } else {
+ return @"font";
+ }
+ } else if([style isEqualToNumber: @([InlineCodeStyle getStyleType])]) {
return @"code";
} else if ([style isEqualToNumber:@([LinkStyle getStyleType])]) {
if (openingTag) {
@@ -631,9 +646,10 @@ - (void)applyProcessedStyles:(NSArray *)processedStyles
params:params];
} else if ([styleType isEqualToNumber:@([ImageStyle getStyleType])]) {
ImageData *imgData = (ImageData *)stylePair.styleValue;
- [((ImageStyle *)baseStyle) addImageAtRange:styleRange
- imageData:imgData
- withSelection:NO];
+ [((ImageStyle *)baseStyle) addImageAtRange:styleRange imageData:imgData withSelection:NO];
+ } else if([styleType isEqualToNumber: @([ColorStyle getStyleType])]) {
+ UIColor *color = (UIColor *)stylePair.styleValue;
+ [((ColorStyle *)baseStyle) applyStyle:styleRange color: color];
} else {
BOOL shouldAddTypingAttr =
styleRange.location + styleRange.length == plainTextLength;
@@ -1187,7 +1203,24 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml {
[styleArr addObject:@([UnderlineStyle getStyleType])];
} else if ([tagName isEqualToString:@"s"]) {
[styleArr addObject:@([StrikethroughStyle getStyleType])];
- } else if ([tagName isEqualToString:@"code"]) {
+ } else if([tagName isEqualToString:@"font"]) {
+ [styleArr addObject:@([ColorStyle getStyleType])];
+
+ NSString *pattern = @"color=\"([^\"]+)\"";
+ NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
+ NSTextCheckingResult *match = [regex firstMatchInString:params options:0 range:NSMakeRange(0, params.length)];
+
+ if(match.numberOfRanges == 2) {
+ NSString *colorString = [params substringWithRange:[match rangeAtIndex:1]];
+
+ UIColor *color = [UIColor colorFromString:colorString];
+ if(color == nil) {
+ continue;
+ }
+
+ stylePair.styleValue = color;
+ }
+ } else if([tagName isEqualToString:@"code"]) {
[styleArr addObject:@([InlineCodeStyle getStyleType])];
} else if ([tagName isEqualToString:@"a"]) {
NSRegularExpression *hrefRegex =
diff --git a/ios/styles/ColorStyle.mm b/ios/styles/ColorStyle.mm
new file mode 100644
index 000000000..a26839f00
--- /dev/null
+++ b/ios/styles/ColorStyle.mm
@@ -0,0 +1,297 @@
+#import "StyleHeaders.h"
+#import "EnrichedTextInputView.h"
+#import "OccurenceUtils.h"
+#import "FontExtension.h"
+#import "StyleTypeEnum.h"
+#import "ColorExtension.h"
+
+@implementation ColorStyle {
+ EnrichedTextInputView *_input;
+}
+
++ (StyleType)getStyleType { return Colored; }
+
+- (instancetype)initWithInput:(id)input {
+ self = [super init];
+ _input = (EnrichedTextInputView *)input;
+ return self;
+}
+
+- (void)applyStyle:(NSRange)range {
+}
+
+- (void)applyStyle:(NSRange)range color:(UIColor *)color {
+ BOOL isStylePresent = [self detectStyle:range color:color];
+
+ if (range.length >= 1) {
+ isStylePresent ? [self removeAttributes:range] : [self addAttributes:range color: color];
+ } else {
+ isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes: color];
+ }
+}
+
+#pragma mark - Add attributes
+
+- (void)addAttributes:(NSRange)range {
+}
+
+- (void)addAttributes:(NSRange)range color:(UIColor *)color {
+ if (color == nil) return;
+ [_input->textView.textStorage beginEditing];
+ [_input->textView.textStorage addAttributes:@{
+ NSForegroundColorAttributeName: color,
+ NSUnderlineColorAttributeName: color,
+ NSStrikethroughColorAttributeName: color
+ } range:range];
+ [_input->textView.textStorage endEditing];
+ _color = color;
+}
+
+- (void)addTypingAttributes {
+}
+
+-(void)addTypingAttributes:(UIColor *)color {
+ NSMutableDictionary *newTypingAttrs = [_input->textView.typingAttributes mutableCopy];
+ newTypingAttrs[NSForegroundColorAttributeName] = color;
+ newTypingAttrs[NSUnderlineColorAttributeName] = color;
+ newTypingAttrs[NSStrikethroughColorAttributeName] = color;
+ _input->textView.typingAttributes = newTypingAttrs;
+}
+
+#pragma mark - Remove attributes
+
+- (void)removeAttributes:(NSRange)range {
+ NSTextStorage *textStorage = _input->textView.textStorage;
+
+ LinkStyle *linkStyle = _input->stylesDict[@(Link)];
+ InlineCodeStyle *inlineCodeStyle = _input->stylesDict[@(InlineCode)];
+ BlockQuoteStyle *blockQuoteStyle = _input->stylesDict[@(BlockQuote)];
+ MentionStyle *mentionStyle = _input->stylesDict[@(Mention)];
+
+ NSArray *linkOccurrences = [linkStyle findAllOccurences:range];
+ NSArray *inlineOccurrences = [inlineCodeStyle findAllOccurences:range];
+ NSArray *blockQuoteOccurrences = [blockQuoteStyle findAllOccurences:range];
+ NSArray *mentionOccurrences = [mentionStyle findAllOccurences:range];
+
+ NSMutableSet *points = [NSMutableSet new];
+ [points addObject:@(range.location)];
+ [points addObject:@(NSMaxRange(range))];
+
+ for (StylePair *pair in linkOccurrences) {
+ [points addObject:@([pair.rangeValue rangeValue].location)];
+ [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
+ }
+ for (StylePair *pair in inlineOccurrences) {
+ [points addObject:@([pair.rangeValue rangeValue].location)];
+ [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
+ }
+ for (StylePair *pair in blockQuoteOccurrences) {
+ [points addObject:@([pair.rangeValue rangeValue].location)];
+ [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
+ }
+ for (StylePair *pair in mentionOccurrences) {
+ [points addObject:@([pair.rangeValue rangeValue].location)];
+ [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
+ }
+
+ NSArray *sortedPoints = [points.allObjects sortedArrayUsingSelector:@selector(compare:)];
+
+ [textStorage beginEditing];
+ for (NSUInteger i = 0; i < sortedPoints.count - 1; i++) {
+ NSUInteger start = sortedPoints[i].unsignedIntegerValue;
+ NSUInteger end = sortedPoints[i + 1].unsignedIntegerValue;
+ if (start >= end) continue;
+
+ NSRange subrange = NSMakeRange(start, end - start);
+
+ UIColor *baseColor = [self baseColorForLocation: subrange.location];
+
+ [textStorage addAttribute:NSForegroundColorAttributeName value:baseColor range:subrange];
+ [textStorage addAttribute:NSUnderlineColorAttributeName value:baseColor range:subrange];
+ [textStorage addAttribute:NSStrikethroughColorAttributeName value:baseColor range:subrange];
+ }
+ [textStorage endEditing];
+}
+
+- (void)removeTypingAttributes {
+ NSMutableDictionary *newTypingAttrs = [_input->textView.typingAttributes mutableCopy];
+ NSRange selectedRange = _input->textView.selectedRange;
+ NSUInteger location = selectedRange.location;
+
+ UIColor *baseColor = [self baseColorForLocation:location];
+
+ newTypingAttrs[NSForegroundColorAttributeName] = baseColor;
+ newTypingAttrs[NSUnderlineColorAttributeName] = baseColor;
+ newTypingAttrs[NSStrikethroughColorAttributeName] = baseColor;
+ _input->textView.typingAttributes = newTypingAttrs;
+}
+
+#pragma mark - Detection
+
+-(BOOL)isInStyle:(NSRange) range styleType:(StyleType)styleType {
+ id style = _input->stylesDict[@(styleType)];
+
+ return (range.length > 0
+ ? [style anyOccurence:range]
+ : [style detectStyle:range]);
+}
+
+- (BOOL)inLinkAndForegroundColorIsLinkColor:(id)value :(NSRange)range {
+ BOOL isInLink = [self isInStyle:range styleType: Link];
+
+ return isInLink && [(UIColor *)value isEqualToColor:[_input->config linkColor]];
+}
+
+- (BOOL)inInlineCodeAndHasTheSameColor:(id)value :(NSRange)range {
+ BOOL isInInlineCode = [self isInStyle:range styleType:InlineCode];
+
+ return isInInlineCode && [(UIColor *)value isEqualToColor:[_input->config inlineCodeFgColor]];
+}
+
+- (BOOL)inBlockQuoteAndHasTheSameColor:(id)value :(NSRange)range {
+ BOOL isInBlockQuote = [self isInStyle:range styleType:BlockQuote];
+
+ return isInBlockQuote && [(UIColor *)value isEqualToColor:[_input->config blockquoteColor]];
+}
+
+- (BOOL)inMentionAndHasTheSameColor:(id)value :(NSRange)range {
+ MentionStyle *mentionStyle = _input->stylesDict[@(Mention)];
+ BOOL isInMention = [self isInStyle:range styleType:Mention];
+
+ if (!isInMention) return NO;
+
+ MentionParams *params = [mentionStyle getMentionParamsAt:range.location];
+ if (params == nil) return NO;
+
+ MentionStyleProps *styleProps = [_input->config mentionStylePropsForIndicator:params.indicator];
+
+ return [(UIColor *)value isEqualToColor:styleProps.color];
+}
+
+- (BOOL)styleCondition:(id)value :(NSRange)range {
+ if (value == nil) { return NO; }
+
+ if ([(UIColor *)value isEqualToColor:_input->config.primaryColor]) { return NO; }
+ if ([self inBlockQuoteAndHasTheSameColor:value :range]) { return NO; }
+ if ([self inLinkAndForegroundColorIsLinkColor:value :range]) { return NO; }
+ if ([self inInlineCodeAndHasTheSameColor:value :range]) { return NO; }
+ if ([self inMentionAndHasTheSameColor:value :range]) { return NO; }
+
+ return YES;
+}
+
+- (BOOL)detectStyle:(NSRange)range {
+ UIColor *color = [self getColorInRange:range];
+
+ return [self detectStyle:range color:color];
+}
+
+- (BOOL)detectStyle:(NSRange)range color:(UIColor *)color {
+ if(range.length >= 1) {
+ return [OccurenceUtils detect:NSForegroundColorAttributeName withInput:_input inRange:range
+ withCondition: ^BOOL(id _Nullable value, NSRange range) {
+ return [(UIColor *)value isEqualToColor:color] && [self styleCondition:value :range];
+ }
+ ];
+ } else {
+ return [OccurenceUtils detect:NSForegroundColorAttributeName withInput:_input atIndex:range.location checkPrevious:YES
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [(UIColor *)value isEqualToColor:color] && [self styleCondition:value :range];
+ }
+ ];
+ }
+}
+
+- (BOOL)detectExcludingColor:(UIColor *)excludedColor inRange:(NSRange)range {
+ if (![self detectStyle:range]) {
+ return NO;
+ }
+ UIColor *currentColor = [self getColorInRange:range];
+ return currentColor != nil && ![currentColor isEqualToColor:excludedColor];
+}
+
+- (BOOL)anyOccurence:(NSRange)range {
+ return [OccurenceUtils any:NSForegroundColorAttributeName withInput:_input inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value :range];
+ }
+ ];
+}
+
+- (NSArray *_Nullable)findAllOccurences:(NSRange)range {
+ return [OccurenceUtils all:NSForegroundColorAttributeName withInput:_input inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value :range];
+ }
+ ];
+}
+
+- (UIColor *)getColorAt:(NSUInteger)location {
+ NSRange effectiveRange = NSMakeRange(0, 0);
+ NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length);
+
+ if(location == _input->textView.textStorage.length) {
+ UIColor *typingColor = _input->textView.typingAttributes[NSForegroundColorAttributeName];
+ return typingColor ?: [_input->config primaryColor];
+ }
+
+ return [_input->textView.textStorage
+ attribute:NSForegroundColorAttributeName
+ atIndex:location
+ longestEffectiveRange: &effectiveRange
+ inRange:inputRange
+ ];
+}
+
+- (UIColor *)getColorInRange:(NSRange)range {
+ NSUInteger location = range.location;
+ NSUInteger length = range.length;
+
+ NSRange effectiveRange = NSMakeRange(0, 0);
+ NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length);
+
+ if(location == _input->textView.textStorage.length) {
+ UIColor *typingColor = _input->textView.typingAttributes[NSForegroundColorAttributeName];
+ return typingColor ?: [_input->config primaryColor];
+ }
+
+ NSUInteger queryLocation = location;
+ if (length == 0 && location > 0) {
+ queryLocation = location - 1;
+ }
+
+ UIColor *color = [_input->textView.textStorage
+ attribute:NSForegroundColorAttributeName
+ atIndex:queryLocation
+ longestEffectiveRange: &effectiveRange
+ inRange:inputRange
+ ];
+
+ return color;
+}
+
+- (UIColor *)baseColorForLocation:(NSUInteger)location {
+ BOOL inLink = [self isInStyle:NSMakeRange(location, 0) styleType:Link];
+ BOOL inInlineCode = [self isInStyle:NSMakeRange(location, 0) styleType:InlineCode];
+ BOOL inBlockQuote = [self isInStyle:NSMakeRange(location, 0) styleType:BlockQuote];
+ BOOL inMention = [self isInStyle:NSMakeRange(location, 0) styleType:Mention];
+
+ UIColor *baseColor = [_input->config primaryColor];
+ if (inMention) {
+ MentionStyle *mentionStyle = _input->stylesDict[@(Mention)];
+ MentionParams *params = [mentionStyle getMentionParamsAt:location];
+ if (params != nil) {
+ MentionStyleProps *styleProps = [_input->config mentionStylePropsForIndicator:params.indicator];
+ baseColor = styleProps.color;
+ }
+ } else if (inLink) {
+ baseColor = [_input->config linkColor];
+ } else if (inInlineCode) {
+ baseColor = [_input->config inlineCodeFgColor];
+ } else if (inBlockQuote) {
+ baseColor = [_input->config blockquoteColor];
+ }
+ return baseColor;
+}
+
+@end
diff --git a/ios/utils/ColorExtension.h b/ios/utils/ColorExtension.h
index 1ed38519a..f11b8c871 100644
--- a/ios/utils/ColorExtension.h
+++ b/ios/utils/ColorExtension.h
@@ -5,3 +5,11 @@
- (BOOL)isEqualToColor:(UIColor *)otherColor;
- (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha;
@end
+
+@interface UIColor (FromString)
++ (UIColor *)colorFromString:(NSString *)string;
+@end
+
+@interface UIColor (HexString)
+- (NSString *)hexString;
+@end
diff --git a/ios/utils/ColorExtension.mm b/ios/utils/ColorExtension.mm
index 0cc59b727..b12ea9230 100644
--- a/ios/utils/ColorExtension.mm
+++ b/ios/utils/ColorExtension.mm
@@ -36,3 +36,157 @@ - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha {
return self;
}
@end
+
+@implementation UIColor (FromString)
++ (UIColor *)colorFromString:(NSString *)string {
+ if (!string) return nil;
+
+ NSString *input = [[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
+
+ // Expanded named colors (add more from CSS list as needed)
+ NSDictionary *namedColors = @{
+ @"red": [UIColor redColor],
+ @"green": [UIColor greenColor],
+ @"blue": [UIColor blueColor],
+ @"black": [UIColor blackColor],
+ @"white": [UIColor whiteColor],
+ @"yellow": [UIColor yellowColor],
+ @"gray": [UIColor grayColor],
+ @"grey": [UIColor grayColor],
+ @"transparent": [UIColor clearColor],
+ @"aliceblue": [UIColor colorWithRed:0.941 green:0.973 blue:1.0 alpha:1.0],
+ // Add additional CSS names here, e.g., "chartreuse": [UIColor colorWithRed:0.498 green:1.0 blue:0.0 alpha:1.0],
+ };
+
+ UIColor *namedColor = namedColors[input];
+ if (namedColor) return namedColor;
+
+ // Hex parsing (including short forms)
+ if ([input hasPrefix:@"#"]) {
+ NSString *hex = [input substringFromIndex:1];
+ if (hex.length == 3 || hex.length == 4) { // Short hex: #rgb or #rgba
+ NSMutableString *expanded = [NSMutableString string];
+ for (NSUInteger i = 0; i < hex.length; i++) {
+ unichar c = [hex characterAtIndex:i];
+ [expanded appendFormat:@"%c%c", c, c];
+ }
+ hex = expanded;
+ }
+ if (hex.length == 6 || hex.length == 8) {
+ unsigned int hexValue = 0;
+ NSScanner *scanner = [NSScanner scannerWithString:hex];
+ if ([scanner scanHexInt:&hexValue]) {
+ CGFloat r, g, b, a = 1.0;
+ if (hex.length == 6) {
+ r = ((hexValue & 0xFF0000) >> 16) / 255.0;
+ g = ((hexValue & 0x00FF00) >> 8) / 255.0;
+ b = (hexValue & 0x0000FF) / 255.0;
+ } else {
+ r = ((hexValue & 0xFF000000) >> 24) / 255.0;
+ g = ((hexValue & 0x00FF0000) >> 16) / 255.0;
+ b = ((hexValue & 0x0000FF00) >> 8) / 255.0;
+ a = (hexValue & 0x000000FF) / 255.0;
+ }
+ return [UIColor colorWithRed:r green:g blue:b alpha:a];
+ }
+ }
+ return nil;
+ }
+
+ // RGB/RGBA parsing (with percentages)
+ if ([input hasPrefix:@"rgb"] || [input hasPrefix:@"rgba"]) {
+ NSString *clean = [input stringByReplacingOccurrencesOfString:@"rgb" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@"a" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@"(" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@")" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@" " withString:@""];
+
+ NSArray *parts = [clean componentsSeparatedByString:@","];
+ if (parts.count == 3 || parts.count == 4) {
+ CGFloat r = [self parseColorComponent:parts[0] max:255.0];
+ CGFloat g = [self parseColorComponent:parts[1] max:255.0];
+ CGFloat b = [self parseColorComponent:parts[2] max:255.0];
+ CGFloat a = parts.count == 4 ? [self parseColorComponent:parts[3] max:1.0] : 1.0;
+ if (r >= 0 && g >= 0 && b >= 0 && a >= 0) {
+ return [UIColor colorWithRed:r green:g blue:b alpha:a];
+ }
+ }
+ return nil;
+ }
+
+ // HSL/HSLA parsing (basic implementation)
+ if ([input hasPrefix:@"hsl"] || [input hasPrefix:@"hsla"]) {
+ NSString *clean = [input stringByReplacingOccurrencesOfString:@"hsl" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@"a" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@"(" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@")" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@" " withString:@""];
+
+ NSArray *parts = [clean componentsSeparatedByString:@","];
+ if (parts.count == 3 || parts.count == 4) {
+ CGFloat h = [self parseColorComponent:parts[0] max:360.0];
+ CGFloat s = [self parseColorComponent:parts[1] max:1.0];
+ CGFloat l = [self parseColorComponent:parts[2] max:1.0];
+ CGFloat a = parts.count == 4 ? [self parseColorComponent:parts[3] max:1.0] : 1.0;
+ return [UIColor colorWithHue:h / 360.0 saturation:s brightness:l alpha:a]; // Note: Uses HSB approximation
+ }
+ return nil;
+ }
+
+ return nil;
+}
+
++ (CGFloat)parseColorComponent:(NSString *)comp max:(CGFloat)max {
+ if ([comp hasSuffix:@"%"]) {
+ comp = [comp stringByReplacingOccurrencesOfString:@"%" withString:@""];
+ return [comp floatValue] / 100.0;
+ }
+ return [comp floatValue] / max;
+}
+@end
+
+@implementation UIColor (HexString)
+- (NSString *)hexString {
+ CGColorRef colorRef = self.CGColor;
+ size_t numComponents = CGColorGetNumberOfComponents(colorRef);
+ const CGFloat *components = CGColorGetComponents(colorRef);
+
+ CGFloat r = 0.0, g = 0.0, b = 0.0, a = 1.0;
+
+ if (numComponents == 2) { // Monochrome (grayscale)
+ r = components[0];
+ g = components[0];
+ b = components[0];
+ a = components[1];
+ } else if (numComponents == 4) { // RGBA
+ r = components[0];
+ g = components[1];
+ b = components[2];
+ a = components[3];
+ } else if (numComponents == 3) { // RGB (no alpha)
+ r = components[0];
+ g = components[1];
+ b = components[2];
+ } else {
+ // Unsupported color space (e.g., pattern colors)
+ return @"#FFFFFF"; // Or return nil for better error handling
+ }
+
+ int red = (int)lroundf(r * 255.0f);
+ int green = (int)lroundf(g * 255.0f);
+ int blue = (int)lroundf(b * 255.0f);
+ int alpha = (int)lroundf(a * 255.0f);
+
+ // Clamp values to 0-255 to prevent overflow
+ red = MAX(0, MIN(255, red));
+ green = MAX(0, MIN(255, green));
+ blue = MAX(0, MIN(255, blue));
+ alpha = MAX(0, MIN(255, alpha));
+
+ if (alpha < 255) {
+ return [NSString stringWithFormat:@"#%02X%02X%02X%02X", red, green, blue, alpha];
+ } else {
+ return [NSString stringWithFormat:@"#%02X%02X%02X", red, green, blue];
+ }
+}
+@end
diff --git a/ios/utils/ColorUtils.m b/ios/utils/ColorUtils.m
new file mode 100644
index 000000000..011b0554f
--- /dev/null
+++ b/ios/utils/ColorUtils.m
@@ -0,0 +1,8 @@
+//
+// ColorUtils.m
+// ReactNativeEnriched
+//
+// Created by Ivan Ignathuk on 19/11/2025.
+//
+
+#import
diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h
index 635242009..3d290d552 100644
--- a/ios/utils/StyleHeaders.h
+++ b/ios/utils/StyleHeaders.h
@@ -20,6 +20,13 @@
- (void)handleNewlines;
@end
+@interface ColorStyle : NSObject
+@property (nonatomic, strong) UIColor *color;
+- (UIColor *)getColorAt:(NSUInteger)location;
+- (void)applyStyle:(NSRange)range color:(UIColor *)color;
+- (BOOL)detectExcludingColor:(UIColor *)excludedColor inRange:(NSRange)range;
+@end
+
@interface LinkStyle : NSObject
- (void)addLink:(NSString *)text
url:(NSString *)url
diff --git a/ios/utils/StyleTypeEnum.h b/ios/utils/StyleTypeEnum.h
index b19fcb087..d09d251fc 100644
--- a/ios/utils/StyleTypeEnum.h
+++ b/ios/utils/StyleTypeEnum.h
@@ -21,5 +21,6 @@ typedef NS_ENUM(NSInteger, StyleType) {
Italic,
Underline,
Strikethrough,
+ Colored,
None,
};
diff --git a/src/EnrichedTextInput.tsx b/src/EnrichedTextInput.tsx
index 2d8105b74..04fafc967 100644
--- a/src/EnrichedTextInput.tsx
+++ b/src/EnrichedTextInput.tsx
@@ -20,6 +20,7 @@ import EnrichedTextInputNativeComponent, {
type OnRequestHtmlResultEvent,
type MentionStyleProperties,
type OnChangeStateDeprecatedEvent,
+ type OnChangeColorEvent,
} from './EnrichedTextInputNativeComponent';
import type {
ColorValue,
@@ -68,6 +69,7 @@ export interface EnrichedTextInputInstance extends NativeMethods {
text: string,
attributes?: Record
) => void;
+ toggleColor: (color: string) => void;
}
export interface OnChangeMentionEvent {
@@ -153,6 +155,9 @@ export interface EnrichedTextInputProps extends Omit {
onChangeMention?: (e: OnChangeMentionEvent) => void;
onEndMention?: (indicator: string) => void;
onChangeSelection?: (e: NativeSyntheticEvent) => void;
+ onColorChangeInSelection?: (
+ color: NativeSyntheticEvent
+ ) => void;
/**
* If true, Android will use experimental synchronous events.
* This will prevent from input flickering when updating component size.
@@ -210,6 +215,7 @@ export const EnrichedTextInput = ({
onChangeMention,
onEndMention,
onChangeSelection,
+ onColorChangeInSelection,
androidExperimentalSynchronousEvents = false,
scrollEnabled = true,
...rest
@@ -352,6 +358,9 @@ export const EnrichedTextInput = ({
setSelection: (start: number, end: number) => {
Commands.setSelection(nullthrows(nativeRef.current), start, end);
},
+ toggleColor: (color: string) => {
+ Commands.toggleColor(nullthrows(nativeRef.current), color);
+ },
}));
const handleMentionEvent = (e: NativeSyntheticEvent) => {
@@ -426,6 +435,7 @@ export const EnrichedTextInput = ({
onMention={handleMentionEvent}
onChangeSelection={onChangeSelection}
onRequestHtmlResult={handleRequestHtmlResult}
+ onColorChangeInSelection={onColorChangeInSelection}
androidExperimentalSynchronousEvents={
androidExperimentalSynchronousEvents
}
diff --git a/src/EnrichedTextInputNativeComponent.ts b/src/EnrichedTextInputNativeComponent.ts
index 907fc178b..f63083837 100644
--- a/src/EnrichedTextInputNativeComponent.ts
+++ b/src/EnrichedTextInputNativeComponent.ts
@@ -32,6 +32,11 @@ export interface OnChangeStateEvent {
isConflicting: boolean;
isBlocking: boolean;
};
+ colored: {
+ isActive: boolean;
+ isConflicting: boolean;
+ isBlocking: boolean;
+ };
italic: {
isActive: boolean;
isConflicting: boolean;
@@ -138,6 +143,7 @@ export interface OnChangeStateDeprecatedEvent {
isLink: boolean;
isImage: boolean;
isMention: boolean;
+ isColored: boolean;
}
export interface OnLinkDetected {
@@ -186,6 +192,10 @@ type Heading = {
bold?: boolean;
};
+export interface OnChangeColorEvent {
+ color: string | null;
+}
+
export interface HtmlStyleInternal {
h1?: Heading;
h2?: Heading;
@@ -256,6 +266,7 @@ export interface NativeProps extends ViewProps {
onMention?: DirectEventHandler;
onChangeSelection?: DirectEventHandler;
onRequestHtmlResult?: DirectEventHandler;
+ onColorChangeInSelection?: DirectEventHandler;
// Style related props - used for generating proper setters in component's manager
// These should not be passed as regular props
@@ -330,6 +341,10 @@ interface NativeCommands {
viewRef: React.ElementRef,
requestId: Int32
) => void;
+ toggleColor: (
+ viewRef: React.ElementRef,
+ color: string
+ ) => void;
}
export const Commands: NativeCommands = codegenNativeCommands({
@@ -361,6 +376,7 @@ export const Commands: NativeCommands = codegenNativeCommands({
'startMention',
'addMention',
'requestHTML',
+ 'toggleColor',
],
});
From 4a1814c2c477d4fe10744eca114332d800c230ee Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Wed, 19 Nov 2025 23:37:37 +0100
Subject: [PATCH 02/12] fix: remove unused file
---
ios/utils/ColorUtils.m | 8 --------
1 file changed, 8 deletions(-)
delete mode 100644 ios/utils/ColorUtils.m
diff --git a/ios/utils/ColorUtils.m b/ios/utils/ColorUtils.m
deleted file mode 100644
index 011b0554f..000000000
--- a/ios/utils/ColorUtils.m
+++ /dev/null
@@ -1,8 +0,0 @@
-//
-// ColorUtils.m
-// ReactNativeEnriched
-//
-// Created by Ivan Ignathuk on 19/11/2025.
-//
-
-#import
From e0b99b86e4a187ffb2db5622e8e431eaa06ba6a1 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Mon, 24 Nov 2025 23:05:07 +0100
Subject: [PATCH 03/12] fix: rebase with latest main
---
ios/styles/ColorStyle.mm | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/ios/styles/ColorStyle.mm b/ios/styles/ColorStyle.mm
index a26839f00..ea576cab6 100644
--- a/ios/styles/ColorStyle.mm
+++ b/ios/styles/ColorStyle.mm
@@ -20,6 +20,8 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
}
++ (BOOL)isParagraphStyle { return NO; }
+
- (void)applyStyle:(NSRange)range color:(UIColor *)color {
BOOL isStylePresent = [self detectStyle:range color:color];
@@ -136,6 +138,12 @@ -(BOOL)isInStyle:(NSRange) range styleType:(StyleType)styleType {
: [style detectStyle:range]);
}
+- (BOOL)isInCodeBlockAndHasTheSameColor:(id)value :(NSRange)range {
+ BOOL isInCodeBlock = [self isInStyle:range styleType: CodeBlock];
+
+ return isInCodeBlock && [(UIColor *)value isEqualToColor:[_input->config codeBlockFgColor]];
+}
+
- (BOOL)inLinkAndForegroundColorIsLinkColor:(id)value :(NSRange)range {
BOOL isInLink = [self isInStyle:range styleType: Link];
@@ -176,6 +184,7 @@ - (BOOL)styleCondition:(id)value :(NSRange)range {
if ([self inLinkAndForegroundColorIsLinkColor:value :range]) { return NO; }
if ([self inInlineCodeAndHasTheSameColor:value :range]) { return NO; }
if ([self inMentionAndHasTheSameColor:value :range]) { return NO; }
+ if ([self isInCodeBlockAndHasTheSameColor:value :range]) { return NO; }
return YES;
}
From 4ecf7af27ccac31bf7491487a146d3cb86f2419f Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Fri, 5 Dec 2025 23:14:48 +0100
Subject: [PATCH 04/12] fix: add setColor and removeColor methods
---
.../enriched/EnrichedTextInputViewManager.kt | 6 +++++-
apps/example/src/App.tsx | 11 ++++++++++-
apps/example/src/components/ColorPreview.tsx | 5 ++---
apps/example/src/components/Toolbar.tsx | 2 +-
ios/EnrichedTextInputView.mm | 9 ++++++---
ios/inputParser/InputParser.mm | 7 +++++--
ios/utils/ColorExtension.mm | 3 +--
src/EnrichedTextInput.tsx | 10 +++++++---
src/EnrichedTextInputNativeComponent.ts | 9 ++++-----
src/index.tsx | 1 +
10 files changed, 42 insertions(+), 21 deletions(-)
diff --git a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt
index 2d0719085..e0d87a3c1 100644
--- a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt
+++ b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt
@@ -338,7 +338,11 @@ class EnrichedTextInputViewManager :
view?.verifyAndToggleStyle(EnrichedSpans.UNORDERED_LIST)
}
- override fun toggleColor(view: EnrichedTextInputView?, color: String) {
+ override fun setColor(view: EnrichedTextInputView?, color: String) {
+ // no-op for now
+ }
+
+ override fun removeColor(view: EnrichedTextInputView?) {
// no-op for now
}
diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx
index 11b393cc2..1211831eb 100644
--- a/apps/example/src/App.tsx
+++ b/apps/example/src/App.tsx
@@ -27,7 +27,7 @@ import {
prepareImageDimensions,
} from './utils/prepareImageDimensions';
import type { OnChangeColorEvent } from '../../src/EnrichedTextInputNativeComponent';
-import ColorPreview from './components/ColorPreview';
+import { ColorPreview } from './components/ColorPreview';
type StylesState = OnChangeStateEvent;
@@ -305,6 +305,10 @@ export default function App() {
}
};
+ const handleRemoveColor = () => {
+ ref.current?.removeColor();
+ };
+
return (
<>
+
{DEBUG_SCROLLABLE && }
diff --git a/apps/example/src/components/ColorPreview.tsx b/apps/example/src/components/ColorPreview.tsx
index 337c9f19b..403734185 100644
--- a/apps/example/src/components/ColorPreview.tsx
+++ b/apps/example/src/components/ColorPreview.tsx
@@ -1,10 +1,11 @@
import { StyleSheet, Text, View } from 'react-native';
+import { type FC } from 'react';
type Props = {
color: string;
};
-const ColorPreview: React.FC = ({ color }) => {
+export const ColorPreview: FC = ({ color }) => {
return (
<>
= ({
};
const handleColorButtonPress = (color: string) => {
- editorRef?.current?.toggleColor(color);
+ editorRef?.current?.setColor(color);
};
const isActive = (item: Item) => {
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index 85a3fb223..e1417f584 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1158,10 +1158,13 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
[self toggleRegularStyle: [UnderlineStyle getStyleType]];
} else if([commandName isEqualToString:@"toggleStrikeThrough"]) {
[self toggleRegularStyle: [StrikethroughStyle getStyleType]];
- } else if([commandName isEqualToString:@"toggleColor"]) {
+ } else if([commandName isEqualToString:@"setColor"]) {
NSString *colorText = (NSString *)args[0];
UIColor *color = [UIColor colorFromString: colorText];
- [self toggleColorStyle: color];
+ [self setColor: color];
+ } else if ([commandName isEqualToString:@"removeColor"]) {
+ ColorStyle *colorStyle = stylesDict[@([ColorStyle getStyleType])];
+ [colorStyle removeAttributes: textView.selectedRange];
} else if([commandName isEqualToString:@"toggleInlineCode"]) {
[self toggleRegularStyle: [InlineCodeStyle getStyleType]];
} else if([commandName isEqualToString:@"addLink"]) {
@@ -1401,7 +1404,7 @@ - (void)requestHTML:(NSInteger)requestId {
// MARK: - Styles manipulation
-- (void)toggleColorStyle:(UIColor *)color {
+- (void)setColor:(UIColor *)color {
ColorStyle *colorStyle = stylesDict[@(Colored)];
[colorStyle applyStyle:textView.selectedRange color: color];
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index 38bf86f70..75d66328a 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -264,12 +264,15 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
NSMutableSet *fixedEndedStyles = [endedStyles mutableCopy];
NSMutableSet *stylesToBeReAdded = [[NSMutableSet alloc] init];
- for (NSNumber *style in endedStyles) {
+ for(NSNumber *style in endedStyles) {
NSInteger styleBeginning = [currentActiveStylesBeginning[style] integerValue];
for(NSNumber *activeStyle in currentActiveStyles) {
NSInteger activeStyleBeginning = [currentActiveStylesBeginning[activeStyle] integerValue];
-
+
+ // we end the styles that began after the currently ended style but not at the "i" (cause the old style ended at exactly "i-1"
+ // also the ones that began in the exact same place but are "inner" in relation to them due to StyleTypeEnum integer values
+
if((activeStyleBeginning > styleBeginning && activeStyleBeginning < i) ||
(activeStyleBeginning == styleBeginning && activeStyleBeginning < i && [activeStyle integerValue] > [style integerValue])) {
[fixedEndedStyles addObject:activeStyle];
diff --git a/ios/utils/ColorExtension.mm b/ios/utils/ColorExtension.mm
index b12ea9230..1c0e90f53 100644
--- a/ios/utils/ColorExtension.mm
+++ b/ios/utils/ColorExtension.mm
@@ -55,7 +55,6 @@ + (UIColor *)colorFromString:(NSString *)string {
@"grey": [UIColor grayColor],
@"transparent": [UIColor clearColor],
@"aliceblue": [UIColor colorWithRed:0.941 green:0.973 blue:1.0 alpha:1.0],
- // Add additional CSS names here, e.g., "chartreuse": [UIColor colorWithRed:0.498 green:1.0 blue:0.0 alpha:1.0],
};
UIColor *namedColor = namedColors[input];
@@ -169,7 +168,7 @@ - (NSString *)hexString {
b = components[2];
} else {
// Unsupported color space (e.g., pattern colors)
- return @"#FFFFFF"; // Or return nil for better error handling
+ return @"#FFFFFF";
}
int red = (int)lroundf(r * 255.0f);
diff --git a/src/EnrichedTextInput.tsx b/src/EnrichedTextInput.tsx
index 04fafc967..6f8bb2ba6 100644
--- a/src/EnrichedTextInput.tsx
+++ b/src/EnrichedTextInput.tsx
@@ -69,7 +69,8 @@ export interface EnrichedTextInputInstance extends NativeMethods {
text: string,
attributes?: Record
) => void;
- toggleColor: (color: string) => void;
+ setColor: (color: string) => void;
+ removeColor: () => void;
}
export interface OnChangeMentionEvent {
@@ -358,8 +359,11 @@ export const EnrichedTextInput = ({
setSelection: (start: number, end: number) => {
Commands.setSelection(nullthrows(nativeRef.current), start, end);
},
- toggleColor: (color: string) => {
- Commands.toggleColor(nullthrows(nativeRef.current), color);
+ setColor: (color: string) => {
+ Commands.setColor(nullthrows(nativeRef.current), color);
+ },
+ removeColor: () => {
+ Commands.removeColor(nullthrows(nativeRef.current));
},
}));
diff --git a/src/EnrichedTextInputNativeComponent.ts b/src/EnrichedTextInputNativeComponent.ts
index f63083837..3d51aad45 100644
--- a/src/EnrichedTextInputNativeComponent.ts
+++ b/src/EnrichedTextInputNativeComponent.ts
@@ -341,10 +341,8 @@ interface NativeCommands {
viewRef: React.ElementRef,
requestId: Int32
) => void;
- toggleColor: (
- viewRef: React.ElementRef,
- color: string
- ) => void;
+ setColor: (viewRef: React.ElementRef, color: string) => void;
+ removeColor: (viewRef: React.ElementRef) => void;
}
export const Commands: NativeCommands = codegenNativeCommands({
@@ -376,7 +374,8 @@ export const Commands: NativeCommands = codegenNativeCommands({
'startMention',
'addMention',
'requestHTML',
- 'toggleColor',
+ 'setColor',
+ 'removeColor',
],
});
diff --git a/src/index.tsx b/src/index.tsx
index 82b37a072..d1a20958f 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -6,4 +6,5 @@ export type {
OnLinkDetected,
OnMentionDetected,
OnChangeSelectionEvent,
+ OnChangeColorEvent,
} from './EnrichedTextInputNativeComponent';
From d4dd655b502098515f6237e02bb72fe057388a87 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Sat, 6 Dec 2025 17:01:58 +0100
Subject: [PATCH 05/12] fix: build on iOS
---
ios/EnrichedTextInputView.mm | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index e1417f584..be815223c 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1160,11 +1160,11 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
[self toggleRegularStyle: [StrikethroughStyle getStyleType]];
} else if([commandName isEqualToString:@"setColor"]) {
NSString *colorText = (NSString *)args[0];
- UIColor *color = [UIColor colorFromString: colorText];
- [self setColor: color];
+ [self setColor: colorText];
} else if ([commandName isEqualToString:@"removeColor"]) {
ColorStyle *colorStyle = stylesDict[@([ColorStyle getStyleType])];
[colorStyle removeAttributes: textView.selectedRange];
+ [self anyTextMayHaveBeenModified];
} else if([commandName isEqualToString:@"toggleInlineCode"]) {
[self toggleRegularStyle: [InlineCodeStyle getStyleType]];
} else if([commandName isEqualToString:@"addLink"]) {
@@ -1404,13 +1404,20 @@ - (void)requestHTML:(NSInteger)requestId {
// MARK: - Styles manipulation
-- (void)setColor:(UIColor *)color {
+- (void)setColor:(NSString *)colorText {
ColorStyle *colorStyle = stylesDict[@(Colored)];
+ UIColor *color = [UIColor colorFromString: colorText];
[colorStyle applyStyle:textView.selectedRange color: color];
[self anyTextMayHaveBeenModified];
}
+- (void)removeColor {
+ ColorStyle *colorStyle = stylesDict[@(Colored)];
+ [colorStyle removeAttributes: textView.selectedRange];
+ [self anyTextMayHaveBeenModified];
+}
+
- (void)toggleRegularStyle:(StyleType)type {
id styleClass = stylesDict[@(type)];
From ff2502b5bae2f8082af5f6204ac5c791464e19ca Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Sat, 6 Dec 2025 17:21:49 +0100
Subject: [PATCH 06/12] fix: use removeColor method
---
ios/EnrichedTextInputView.mm | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index be815223c..d58e1b913 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1162,9 +1162,7 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
NSString *colorText = (NSString *)args[0];
[self setColor: colorText];
} else if ([commandName isEqualToString:@"removeColor"]) {
- ColorStyle *colorStyle = stylesDict[@([ColorStyle getStyleType])];
- [colorStyle removeAttributes: textView.selectedRange];
- [self anyTextMayHaveBeenModified];
+ [self removeColor];
} else if([commandName isEqualToString:@"toggleInlineCode"]) {
[self toggleRegularStyle: [InlineCodeStyle getStyleType]];
} else if([commandName isEqualToString:@"addLink"]) {
From 720c40caa5811d86f189002ed56a18ef7a17703f Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Mon, 8 Dec 2025 21:53:36 +0100
Subject: [PATCH 07/12] fix: remove useMemo
---
.../src/components/ToolbarColorButton.tsx | 18 ++++++++----------
1 file changed, 8 insertions(+), 10 deletions(-)
diff --git a/apps/example/src/components/ToolbarColorButton.tsx b/apps/example/src/components/ToolbarColorButton.tsx
index 3d48f0188..c3d604c17 100644
--- a/apps/example/src/components/ToolbarColorButton.tsx
+++ b/apps/example/src/components/ToolbarColorButton.tsx
@@ -1,4 +1,4 @@
-import { useMemo, type FC } from 'react';
+import { type FC } from 'react';
import { Pressable, StyleSheet, Text } from 'react-native';
interface ColorButtonProps {
@@ -18,16 +18,14 @@ export const ToolbarColorButton: FC = ({
onPress(color);
};
- const containerStyle = useMemo(
- () => [
- styles.container,
- { backgroundColor: isActive ? color : 'rgba(0, 26, 114, 0.8)' },
- ],
- [isActive, color]
- );
-
return (
-
+
{text}
);
From ca7a0a3e690fea4c786484aa435c2fe6ff6e7e03 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Mon, 8 Dec 2025 23:21:33 +0100
Subject: [PATCH 08/12] fix: properly handle new colours in parser
---
ios/EnrichedTextInputView.mm | 2 +-
ios/inputParser/InputParser.mm | 25 ++++++++++++++++++-------
ios/styles/ColorStyle.mm | 10 ++++++++++
ios/utils/StyleHeaders.h | 1 +
4 files changed, 30 insertions(+), 8 deletions(-)
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index d58e1b913..e3f295ccb 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1412,7 +1412,7 @@ - (void)setColor:(NSString *)colorText {
- (void)removeColor {
ColorStyle *colorStyle = stylesDict[@(Colored)];
- [colorStyle removeAttributes: textView.selectedRange];
+ [colorStyle removeColorInSelectedRange];
[self anyTextMayHaveBeenModified];
}
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index 75d66328a..c5dc58350 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -60,19 +60,30 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
}
}
- // Handle valued styles changes
UIColor *currentColor = nil;
-
+
if([currentActiveStyles containsObject:@(Colored)]) {
ColorStyle *colorStyle = _input->stylesDict[@(Colored)];
currentColor = [colorStyle getColorAt:currentRange.location];
- if(previousColor && ![currentColor isEqual:previousColor]) {
- // Treat as end of previous color and start of new
- [currentActiveStyles removeObject:@(Colored)];
- currentActiveStylesBeginning[@(Colored)] = [NSNumber numberWithInt:i];
+ // If the end of one color overlaps exactly with the start of the next
+ // the previous font tag should be closed and a new one opened using the new color.
+ if (previousColor && ![currentColor isEqual:previousColor]) {
+ NSString *closeTag = [self tagContentForStyle:@(Colored)
+ openingTag:NO
+ location:currentRange.location];
+ [result appendFormat:@"%@>", closeTag];
+ NSString *openTag = [self tagContentForStyle:@(Colored)
+ openingTag:YES
+ location:currentRange.location];
+ [result appendFormat:@"<%@>", openTag];
+ NSString *currentCharacterStr = [_input->textView.textStorage.string substringWithRange:currentRange];
+ [result appendString: currentCharacterStr];
+ previousColor = currentColor;
+ [currentActiveStyles addObject:@(Colored)];
+ continue;
}
}
-
+
NSString *currentCharacterStr = [_input->textView.textStorage.string substringWithRange:currentRange];
unichar currentCharacterChar = [_input->textView.textStorage.string characterAtIndex:currentRange.location];
diff --git a/ios/styles/ColorStyle.mm b/ios/styles/ColorStyle.mm
index ea576cab6..e8bace11e 100644
--- a/ios/styles/ColorStyle.mm
+++ b/ios/styles/ColorStyle.mm
@@ -303,4 +303,14 @@ - (UIColor *)baseColorForLocation:(NSUInteger)location {
return baseColor;
}
+- (void)removeColorInSelectedRange {
+ NSRange selectedRange = _input->textView.selectedRange;
+
+ if(selectedRange.length > 0) {
+ [self removeAttributes: selectedRange];
+ } else {
+ [self removeTypingAttributes];
+ }
+}
+
@end
diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h
index 3d290d552..be10851fe 100644
--- a/ios/utils/StyleHeaders.h
+++ b/ios/utils/StyleHeaders.h
@@ -25,6 +25,7 @@
- (UIColor *)getColorAt:(NSUInteger)location;
- (void)applyStyle:(NSRange)range color:(UIColor *)color;
- (BOOL)detectExcludingColor:(UIColor *)excludedColor inRange:(NSRange)range;
+- (void)removeColorInSelectedRange;
@end
@interface LinkStyle : NSObject
From 9a8de532a612f9aa678124649cdce34f66872f16 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Tue, 13 Jan 2026 01:15:16 +0100
Subject: [PATCH 09/12] fix: rebase with latest main
---
ios/EnrichedTextInputView.mm | 120 ++++----
ios/inputParser/InputParser.mm | 166 ++++++-----
ios/styles/BlockQuoteStyle.mm | 14 +-
ios/styles/BoldStyle.mm | 14 +-
ios/styles/CodeBlockStyle.mm | 14 +-
ios/styles/ColorStyle.mm | 478 +++++++++++++++++--------------
ios/styles/HeadingStyleBase.mm | 16 +-
ios/styles/ImageStyle.mm | 14 +-
ios/styles/InlineCodeStyle.mm | 16 +-
ios/styles/ItalicStyle.mm | 14 +-
ios/styles/LinkStyle.mm | 57 ++--
ios/styles/MentionStyle.mm | 12 +-
ios/styles/OrderedListStyle.mm | 14 +-
ios/styles/StrikethroughStyle.mm | 14 +-
ios/styles/UnderlineStyle.mm | 14 +-
ios/styles/UnorderedListStyle.mm | 14 +-
ios/utils/BaseStyleProtocol.h | 2 +
ios/utils/ColorExtension.mm | 283 +++++++++---------
ios/utils/StyleHeaders.h | 3 +-
19 files changed, 716 insertions(+), 563 deletions(-)
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index e3f295ccb..1ac33b609 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1,4 +1,5 @@
#import "EnrichedTextInputView.h"
+#import "ColorExtension.h"
#import "CoreText/CoreText.h"
#import "LayoutManagerExtension.h"
#import "ParagraphAttributesUtils.h"
@@ -15,7 +16,6 @@
#import
#import
#import
-#import "ColorExtension.h"
#define GET_STYLE_STATE(TYPE_ENUM) \
{ \
@@ -132,13 +132,16 @@ - (void)setDefaults {
@([ItalicStyle getStyleType]) : @[],
@([UnderlineStyle getStyleType]) : @[],
@([StrikethroughStyle getStyleType]) : @[],
- @([InlineCodeStyle getStyleType]) :
- @[ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]) ],
+ @([InlineCodeStyle getStyleType]) : @[
+ @([LinkStyle getStyleType]), @([MentionStyle getStyleType]),
+ @([ColorStyle getStyleType])
+ ],
@([LinkStyle getStyleType]) : @[
@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]),
@([MentionStyle getStyleType])
],
- @([ColorStyle getStyleType]) : @[@([InlineCodeStyle getStyleType]), @([MentionStyle getStyleType])],
+ @([ColorStyle getStyleType]) :
+ @[ @([InlineCodeStyle getStyleType]), @([MentionStyle getStyleType]) ],
@([MentionStyle getStyleType]) :
@[ @([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]) ],
@([H1Style getStyleType]) : @[
@@ -202,7 +205,7 @@ - (void)setDefaults {
@([H3Style getStyleType]), @([H4Style getStyleType]),
@([H5Style getStyleType]), @([H6Style getStyleType]),
@([UnorderedListStyle getStyleType]), @([OrderedListStyle getStyleType]),
- @([CodeBlockStyle getStyleType])
+ @([CodeBlockStyle getStyleType]), @([ColorStyle getStyleType])
],
@([CodeBlockStyle getStyleType]) : @[
@([H1Style getStyleType]), @([H2Style getStyleType]),
@@ -1038,7 +1041,8 @@ - (void)tryUpdatingActiveStyles {
[_activeStyles containsObject:@([UnderlineStyle getStyleType])],
.isStrikeThrough =
[_activeStyles containsObject:@([StrikethroughStyle getStyleType])],
- .isColored = [_activeStyles containsObject: @([ColorStyle getStyleType])],
+ .isColored =
+ [_activeStyles containsObject:@([ColorStyle getStyleType])],
.isInlineCode =
[_activeStyles containsObject:@([InlineCodeStyle getStyleType])],
.isLink = [_activeStyles containsObject:@([LinkStyle getStyleType])],
@@ -1150,24 +1154,24 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
} else if ([commandName isEqualToString:@"setValue"]) {
NSString *value = (NSString *)args[0];
[self setValue:value];
- } else if([commandName isEqualToString:@"toggleBold"]) {
- [self toggleRegularStyle: [BoldStyle getStyleType]];
- } else if([commandName isEqualToString:@"toggleItalic"]) {
- [self toggleRegularStyle: [ItalicStyle getStyleType]];
- } else if([commandName isEqualToString:@"toggleUnderline"]) {
- [self toggleRegularStyle: [UnderlineStyle getStyleType]];
- } else if([commandName isEqualToString:@"toggleStrikeThrough"]) {
- [self toggleRegularStyle: [StrikethroughStyle getStyleType]];
- } else if([commandName isEqualToString:@"setColor"]) {
+ } else if ([commandName isEqualToString:@"toggleBold"]) {
+ [self toggleRegularStyle:[BoldStyle getStyleType]];
+ } else if ([commandName isEqualToString:@"toggleItalic"]) {
+ [self toggleRegularStyle:[ItalicStyle getStyleType]];
+ } else if ([commandName isEqualToString:@"toggleUnderline"]) {
+ [self toggleRegularStyle:[UnderlineStyle getStyleType]];
+ } else if ([commandName isEqualToString:@"toggleStrikeThrough"]) {
+ [self toggleRegularStyle:[StrikethroughStyle getStyleType]];
+ } else if ([commandName isEqualToString:@"setColor"]) {
NSString *colorText = (NSString *)args[0];
- [self setColor: colorText];
+ [self setColor:colorText];
} else if ([commandName isEqualToString:@"removeColor"]) {
[self removeColor];
- } else if([commandName isEqualToString:@"toggleInlineCode"]) {
- [self toggleRegularStyle: [InlineCodeStyle getStyleType]];
- } else if([commandName isEqualToString:@"addLink"]) {
- NSInteger start = [((NSNumber*)args[0]) integerValue];
- NSInteger end = [((NSNumber*)args[1]) integerValue];
+ } else if ([commandName isEqualToString:@"toggleInlineCode"]) {
+ [self toggleRegularStyle:[InlineCodeStyle getStyleType]];
+ } else if ([commandName isEqualToString:@"addLink"]) {
+ NSInteger start = [((NSNumber *)args[0]) integerValue];
+ NSInteger end = [((NSNumber *)args[1]) integerValue];
NSString *text = (NSString *)args[2];
NSString *url = (NSString *)args[3];
[self addLinkAt:start end:end text:text url:url];
@@ -1305,7 +1309,7 @@ - (void)emitOnLinkDetectedEvent:(NSString *)text
- (void)emitCurrentSelectionColorIfChanged {
NSRange selRange = textView.selectedRange;
UIColor *uniformColor = nil;
-
+
if (selRange.length == 0) {
id colorAttr = textView.typingAttributes[NSForegroundColorAttributeName];
uniformColor = colorAttr ? (UIColor *)colorAttr : [config primaryColor];
@@ -1313,39 +1317,43 @@ - (void)emitCurrentSelectionColorIfChanged {
// Selection range: check for uniform color
__block UIColor *firstColor = nil;
__block BOOL hasMultiple = NO;
-
- [textView.textStorage enumerateAttribute:NSForegroundColorAttributeName
- inRange:selRange
- options:0
- usingBlock:^(id _Nullable value, NSRange range, BOOL *_Nonnull stop) {
- UIColor *thisColor = value ? (UIColor *)value : [config primaryColor];
- if (firstColor == nil) {
- firstColor = thisColor;
- } else if (![firstColor isEqual:thisColor]) {
- hasMultiple = YES;
- *stop = YES;
- }
- }];
-
+
+ [textView.textStorage
+ enumerateAttribute:NSForegroundColorAttributeName
+ inRange:selRange
+ options:0
+ usingBlock:^(id _Nullable value, NSRange range,
+ BOOL *_Nonnull stop) {
+ UIColor *thisColor =
+ value ? (UIColor *)value : [config primaryColor];
+ if (firstColor == nil) {
+ firstColor = thisColor;
+ } else if (![firstColor isEqual:thisColor]) {
+ hasMultiple = YES;
+ *stop = YES;
+ }
+ }];
+
if (!hasMultiple && firstColor != nil) {
uniformColor = firstColor;
}
}
-
- NSString *hexColor = uniformColor ? [uniformColor hexString] : [config.primaryColor hexString];
-
- if(![_recentlyEmittedColor isEqual: hexColor]) {
+
+ NSString *hexColor =
+ uniformColor ? [uniformColor hexString] : [config.primaryColor hexString];
+
+ if (![_recentlyEmittedColor isEqual:hexColor]) {
auto emitter = [self getEventEmitter];
- if(emitter != nullptr) {
- emitter->onColorChangeInSelection({
- .color = [hexColor toCppString]
- });
+ if (emitter != nullptr) {
+ emitter->onColorChangeInSelection({.color = [hexColor toCppString]});
}
_recentlyEmittedColor = hexColor;
}
}
-- (void)emitOnMentionDetectedEvent:(NSString *)text indicator:(NSString *)indicator attributes:(NSString *)attributes {
+- (void)emitOnMentionDetectedEvent:(NSString *)text
+ indicator:(NSString *)indicator
+ attributes:(NSString *)attributes {
auto emitter = [self getEventEmitter];
if (emitter != nullptr) {
emitter->onMentionDetected({.text = [text toCppString],
@@ -1404,9 +1412,9 @@ - (void)requestHTML:(NSInteger)requestId {
- (void)setColor:(NSString *)colorText {
ColorStyle *colorStyle = stylesDict[@(Colored)];
- UIColor *color = [UIColor colorFromString: colorText];
-
- [colorStyle applyStyle:textView.selectedRange color: color];
+ UIColor *color = [UIColor colorFromString:colorText];
+
+ [colorStyle applyStyle:textView.selectedRange color:color];
[self anyTextMayHaveBeenModified];
}
@@ -1787,13 +1795,15 @@ - (void)textViewDidBeginEditing:(UITextView *)textView {
if (_emitFocusBlur) {
emitter->onInputFocus({});
}
-
- NSString *textAtSelection = [[[NSMutableString alloc] initWithString:textView.textStorage.string] substringWithRange: textView.selectedRange];
- emitter->onChangeSelection({
- .start = static_cast(textView.selectedRange.location),
- .end = static_cast(textView.selectedRange.location + textView.selectedRange.length),
- .text = [textAtSelection toCppString]
- });
+
+ NSString *textAtSelection =
+ [[[NSMutableString alloc] initWithString:textView.textStorage.string]
+ substringWithRange:textView.selectedRange];
+ emitter->onChangeSelection(
+ {.start = static_cast(textView.selectedRange.location),
+ .end = static_cast(textView.selectedRange.location +
+ textView.selectedRange.length),
+ .text = [textAtSelection toCppString]});
[self emitCurrentSelectionColorIfChanged];
}
// manage selection changes since textViewDidChangeSelection sometimes doesn't
diff --git a/ios/inputParser/InputParser.mm b/ios/inputParser/InputParser.mm
index c5dc58350..0a3885258 100644
--- a/ios/inputParser/InputParser.mm
+++ b/ios/inputParser/InputParser.mm
@@ -1,10 +1,10 @@
#import "InputParser.h"
+#import "ColorExtension.h"
#import "EnrichedTextInputView.h"
#import "StringExtension.h"
#import "StyleHeaders.h"
#import "TextInsertionUtils.h"
#import "UIView+React.h"
-#import "ColorExtension.h"
@implementation InputParser {
EnrichedTextInputView *_input;
@@ -35,11 +35,11 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
BOOL inBlockQuote = NO;
BOOL inCodeBlock = NO;
unichar lastCharacter = 0;
-
+
// Track current values for valued styles
UIColor *previousColor = nil;
- for(int i = 0; i < text.length; i++) {
+ for (int i = 0; i < text.length; i++) {
NSRange currentRange = NSMakeRange(offset + i, 1);
NSMutableSet *currentActiveStyles =
[[NSMutableSet alloc] init];
@@ -59,39 +59,45 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
[currentActiveStylesBeginning removeObjectForKey:type];
}
}
-
+
UIColor *currentColor = nil;
-
- if([currentActiveStyles containsObject:@(Colored)]) {
+
+ if ([currentActiveStyles containsObject:@(Colored)]) {
ColorStyle *colorStyle = _input->stylesDict[@(Colored)];
currentColor = [colorStyle getColorAt:currentRange.location];
- // If the end of one color overlaps exactly with the start of the next
- // the previous font tag should be closed and a new one opened using the new color.
+ // If the end of one color overlaps exactly with the start of the next
+ // the previous font tag should be closed and a new one opened using the
+ // new color.
if (previousColor && ![currentColor isEqual:previousColor]) {
- NSString *closeTag = [self tagContentForStyle:@(Colored)
- openingTag:NO
- location:currentRange.location];
- [result appendFormat:@"%@>", closeTag];
- NSString *openTag = [self tagContentForStyle:@(Colored)
- openingTag:YES
- location:currentRange.location];
- [result appendFormat:@"<%@>", openTag];
- NSString *currentCharacterStr = [_input->textView.textStorage.string substringWithRange:currentRange];
- [result appendString: currentCharacterStr];
- previousColor = currentColor;
- [currentActiveStyles addObject:@(Colored)];
- continue;
+ NSString *closeTag = [self tagContentForStyle:@(Colored)
+ openingTag:NO
+ location:currentRange.location];
+ [result appendFormat:@"%@>", closeTag];
+ NSString *openTag = [self tagContentForStyle:@(Colored)
+ openingTag:YES
+ location:currentRange.location];
+ [result appendFormat:@"<%@>", openTag];
+ NSString *currentCharacterStr = [_input->textView.textStorage.string
+ substringWithRange:currentRange];
+ [result appendString:currentCharacterStr];
+ previousColor = currentColor;
+ [currentActiveStyles addObject:@(Colored)];
+ continue;
}
}
- NSString *currentCharacterStr = [_input->textView.textStorage.string substringWithRange:currentRange];
- unichar currentCharacterChar = [_input->textView.textStorage.string characterAtIndex:currentRange.location];
-
- if([[NSCharacterSet newlineCharacterSet] characterIsMember:currentCharacterChar]) {
- if(newLine) {
- // we can either have an empty list item OR need to close the list and put a BR in such a situation
- // the existence of the list must be checked on 0 length range, not on the newline character
- if(inOrderedList) {
+ NSString *currentCharacterStr =
+ [_input->textView.textStorage.string substringWithRange:currentRange];
+ unichar currentCharacterChar = [_input->textView.textStorage.string
+ characterAtIndex:currentRange.location];
+
+ if ([[NSCharacterSet newlineCharacterSet]
+ characterIsMember:currentCharacterChar]) {
+ if (newLine) {
+ // we can either have an empty list item OR need to close the list and
+ // put a BR in such a situation the existence of the list must be
+ // checked on 0 length range, not on the newline character
+ if (inOrderedList) {
OrderedListStyle *oStyle = _input->stylesDict[@(OrderedList)];
BOOL detected =
[oStyle detectStyle:NSMakeRange(currentRange.location, 0)];
@@ -176,11 +182,11 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
[result appendString:@""];
}
}
-
+
// clear the previous styles and valued trackers
- previousActiveStyles = [[NSSet alloc]init];
+ previousActiveStyles = [[NSSet alloc] init];
previousColor = nil;
-
+
// next character opens new paragraph
newLine = YES;
} else {
@@ -268,24 +274,33 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
}
// get styles that have ended
- NSMutableSet *endedStyles = [previousActiveStyles mutableCopy];
- [endedStyles minusSet: currentActiveStyles];
-
- // also finish styles that should be ended because they are nested in a style that ended
+ NSMutableSet *endedStyles =
+ [previousActiveStyles mutableCopy];
+ [endedStyles minusSet:currentActiveStyles];
+
+ // also finish styles that should be ended because they are nested in a
+ // style that ended
NSMutableSet *fixedEndedStyles = [endedStyles mutableCopy];
NSMutableSet *stylesToBeReAdded = [[NSMutableSet alloc] init];
-
- for(NSNumber *style in endedStyles) {
- NSInteger styleBeginning = [currentActiveStylesBeginning[style] integerValue];
-
- for(NSNumber *activeStyle in currentActiveStyles) {
- NSInteger activeStyleBeginning = [currentActiveStylesBeginning[activeStyle] integerValue];
-
- // we end the styles that began after the currently ended style but not at the "i" (cause the old style ended at exactly "i-1"
- // also the ones that began in the exact same place but are "inner" in relation to them due to StyleTypeEnum integer values
-
- if((activeStyleBeginning > styleBeginning && activeStyleBeginning < i) ||
- (activeStyleBeginning == styleBeginning && activeStyleBeginning < i && [activeStyle integerValue] > [style integerValue])) {
+
+ for (NSNumber *style in endedStyles) {
+ NSInteger styleBeginning =
+ [currentActiveStylesBeginning[style] integerValue];
+
+ for (NSNumber *activeStyle in currentActiveStyles) {
+ NSInteger activeStyleBeginning =
+ [currentActiveStylesBeginning[activeStyle] integerValue];
+
+ // we end the styles that began after the currently ended style but
+ // not at the "i" (cause the old style ended at exactly "i-1" also the
+ // ones that began in the exact same place but are "inner" in relation
+ // to them due to StyleTypeEnum integer values
+
+ if ((activeStyleBeginning > styleBeginning &&
+ activeStyleBeginning < i) ||
+ (activeStyleBeginning == styleBeginning &&
+ activeStyleBeginning<
+ i && [activeStyle integerValue]>[style integerValue])) {
[fixedEndedStyles addObject:activeStyle];
[stylesToBeReAdded addObject:activeStyle];
}
@@ -357,7 +372,7 @@ - (NSString *)parseToHtmlFromRange:(NSRange)range {
// save current styles for next character's checks
previousActiveStyles = currentActiveStyles;
-
+
previousColor = currentColor;
}
@@ -485,18 +500,18 @@ - (NSString *)tagContentForStyle:(NSNumber *)style
return @"u";
} else if ([style isEqualToNumber:@([StrikethroughStyle getStyleType])]) {
return @"s";
- } else if([style isEqualToNumber:@([ColorStyle getStyleType])]) {
- if(openingTag) {
+ } else if ([style isEqualToNumber:@([ColorStyle getStyleType])]) {
+ if (openingTag) {
ColorStyle *colorSpan = _input->stylesDict[@([ColorStyle getStyleType])];
- UIColor *color = [colorSpan getColorAt: location];
- if(color) {
+ UIColor *color = [colorSpan getColorAt:location];
+ if (color) {
NSString *hex = [color hexString];
return [NSString stringWithFormat:@"font color=\"%@\"", hex];
};
} else {
return @"font";
}
- } else if([style isEqualToNumber: @([InlineCodeStyle getStyleType])]) {
+ } else if ([style isEqualToNumber:@([InlineCodeStyle getStyleType])]) {
return @"code";
} else if ([style isEqualToNumber:@([LinkStyle getStyleType])]) {
if (openingTag) {
@@ -660,10 +675,12 @@ - (void)applyProcessedStyles:(NSArray *)processedStyles
params:params];
} else if ([styleType isEqualToNumber:@([ImageStyle getStyleType])]) {
ImageData *imgData = (ImageData *)stylePair.styleValue;
- [((ImageStyle *)baseStyle) addImageAtRange:styleRange imageData:imgData withSelection:NO];
- } else if([styleType isEqualToNumber: @([ColorStyle getStyleType])]) {
+ [((ImageStyle *)baseStyle) addImageAtRange:styleRange
+ imageData:imgData
+ withSelection:NO];
+ } else if ([styleType isEqualToNumber:@([ColorStyle getStyleType])]) {
UIColor *color = (UIColor *)stylePair.styleValue;
- [((ColorStyle *)baseStyle) applyStyle:styleRange color: color];
+ [((ColorStyle *)baseStyle) applyStyle:styleRange color:color];
} else {
BOOL shouldAddTypingAttr =
styleRange.location + styleRange.length == plainTextLength;
@@ -1217,24 +1234,31 @@ - (NSArray *)getTextAndStylesFromHtml:(NSString *)fixedHtml {
[styleArr addObject:@([UnderlineStyle getStyleType])];
} else if ([tagName isEqualToString:@"s"]) {
[styleArr addObject:@([StrikethroughStyle getStyleType])];
- } else if([tagName isEqualToString:@"font"]) {
+ } else if ([tagName isEqualToString:@"font"]) {
[styleArr addObject:@([ColorStyle getStyleType])];
NSString *pattern = @"color=\"([^\"]+)\"";
- NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
- NSTextCheckingResult *match = [regex firstMatchInString:params options:0 range:NSMakeRange(0, params.length)];
-
- if(match.numberOfRanges == 2) {
- NSString *colorString = [params substringWithRange:[match rangeAtIndex:1]];
-
- UIColor *color = [UIColor colorFromString:colorString];
- if(color == nil) {
- continue;
- }
-
- stylePair.styleValue = color;
+ NSRegularExpression *regex =
+ [NSRegularExpression regularExpressionWithPattern:pattern
+ options:0
+ error:nil];
+ NSTextCheckingResult *match =
+ [regex firstMatchInString:params
+ options:0
+ range:NSMakeRange(0, params.length)];
+
+ if (match.numberOfRanges == 2) {
+ NSString *colorString =
+ [params substringWithRange:[match rangeAtIndex:1]];
+
+ UIColor *color = [UIColor colorFromString:colorString];
+ if (color == nil) {
+ continue;
+ }
+
+ stylePair.styleValue = color;
}
- } else if([tagName isEqualToString:@"code"]) {
+ } else if ([tagName isEqualToString:@"code"]) {
[styleArr addObject:@([InlineCodeStyle getStyleType])];
} else if ([tagName isEqualToString:@"a"]) {
NSRegularExpression *hrefRegex =
diff --git a/ios/styles/BlockQuoteStyle.mm b/ios/styles/BlockQuoteStyle.mm
index f8d039cd9..12da2a584 100644
--- a/ios/styles/BlockQuoteStyle.mm
+++ b/ios/styles/BlockQuoteStyle.mm
@@ -18,6 +18,10 @@ + (BOOL)isParagraphStyle {
return YES;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSParagraphStyleAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -186,7 +190,7 @@ - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text {
return NO;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSParagraphStyle *pStyle = (NSParagraphStyle *)value;
return pStyle != nullptr && pStyle.headIndent == [self getHeadIndent] &&
pStyle.firstLineHeadIndent == [self getHeadIndent] &&
@@ -199,7 +203,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
@@ -207,7 +211,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -217,7 +221,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -226,7 +230,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/BoldStyle.mm b/ios/styles/BoldStyle.mm
index 1afe0d609..127449117 100644
--- a/ios/styles/BoldStyle.mm
+++ b/ios/styles/BoldStyle.mm
@@ -15,6 +15,10 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSFontAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -123,7 +127,7 @@ - (BOOL)boldHeadingConflictsInRange:(NSRange)range type:(StyleType)type {
: [headingStyle detectStyle:range];
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
UIFont *font = (UIFont *)value;
return font != nullptr && [font isBold] &&
![self boldHeadingConflictsInRange:range type:H1] &&
@@ -140,7 +144,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSFontAttributeName
@@ -148,7 +152,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -158,7 +162,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -167,7 +171,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/CodeBlockStyle.mm b/ios/styles/CodeBlockStyle.mm
index 1f5aa9976..05c9dd6f0 100644
--- a/ios/styles/CodeBlockStyle.mm
+++ b/ios/styles/CodeBlockStyle.mm
@@ -15,6 +15,10 @@ + (StyleType)getStyleType {
return CodeBlock;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSParagraphStyleAttributeName;
+}
+
+ (BOOL)isParagraphStyle {
return YES;
}
@@ -177,7 +181,7 @@ - (BOOL)handleBackspaceInRange:(NSRange)range replacementText:(NSString *)text {
return NO;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSParagraphStyle *paragraph = (NSParagraphStyle *)value;
return paragraph != nullptr && paragraph.textLists.count == 1 &&
[paragraph.textLists.firstObject.markerFormat
@@ -190,7 +194,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
@@ -198,7 +202,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -208,7 +212,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -217,7 +221,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/ColorStyle.mm b/ios/styles/ColorStyle.mm
index e8bace11e..1e5c77854 100644
--- a/ios/styles/ColorStyle.mm
+++ b/ios/styles/ColorStyle.mm
@@ -1,15 +1,40 @@
-#import "StyleHeaders.h"
+#import "ColorExtension.h"
#import "EnrichedTextInputView.h"
-#import "OccurenceUtils.h"
#import "FontExtension.h"
+#import "OccurenceUtils.h"
+#import "StyleHeaders.h"
#import "StyleTypeEnum.h"
-#import "ColorExtension.h"
@implementation ColorStyle {
- EnrichedTextInputView *_input;
+ EnrichedTextInputView *_input;
}
-+ (StyleType)getStyleType { return Colored; }
+- (NSArray *)coloredStyleTypes {
+ static NSArray *types = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ types = @[
+ @(Link),
+ @(InlineCode),
+ @(BlockQuote),
+ @(CodeBlock),
+ @(Mention),
+ ];
+ });
+ return types;
+}
+
++ (StyleType)getStyleType {
+ return Colored;
+}
+
++ (NSAttributedStringKey)attributeKey {
+ return NSForegroundColorAttributeName;
+}
+
++ (BOOL)isParagraphStyle {
+ return NO;
+}
- (instancetype)initWithInput:(id)input {
self = [super init];
@@ -20,31 +45,38 @@ - (instancetype)initWithInput:(id)input {
- (void)applyStyle:(NSRange)range {
}
-+ (BOOL)isParagraphStyle { return NO; }
++ (NSDictionary *)getParametersFromValue:(id)value {
+ UIColor *color = value;
+
+ return @{
+ @"color" : [color hexString],
+ };
+}
- (void)applyStyle:(NSRange)range color:(UIColor *)color {
BOOL isStylePresent = [self detectStyle:range color:color];
-
+
if (range.length >= 1) {
- isStylePresent ? [self removeAttributes:range] : [self addAttributes:range color: color];
+ isStylePresent ? [self removeAttributes:range]
+ : [self addAttributes:range color:color];
} else {
- isStylePresent ? [self removeTypingAttributes] : [self addTypingAttributes: color];
+ isStylePresent ? [self removeTypingAttributes]
+ : [self addTypingAttributes:color];
}
}
#pragma mark - Add attributes
-- (void)addAttributes:(NSRange)range {
-}
-
- (void)addAttributes:(NSRange)range color:(UIColor *)color {
- if (color == nil) return;
+ if (color == nil)
+ return;
[_input->textView.textStorage beginEditing];
[_input->textView.textStorage addAttributes:@{
- NSForegroundColorAttributeName: color,
- NSUnderlineColorAttributeName: color,
- NSStrikethroughColorAttributeName: color
- } range:range];
+ NSForegroundColorAttributeName : color,
+ NSUnderlineColorAttributeName : color,
+ NSStrikethroughColorAttributeName : color
+ }
+ range:range];
[_input->textView.textStorage endEditing];
_color = color;
}
@@ -52,8 +84,9 @@ - (void)addAttributes:(NSRange)range color:(UIColor *)color {
- (void)addTypingAttributes {
}
--(void)addTypingAttributes:(UIColor *)color {
- NSMutableDictionary *newTypingAttrs = [_input->textView.typingAttributes mutableCopy];
+- (void)addTypingAttributes:(UIColor *)color {
+ NSMutableDictionary *newTypingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
newTypingAttrs[NSForegroundColorAttributeName] = color;
newTypingAttrs[NSUnderlineColorAttributeName] = color;
newTypingAttrs[NSStrikethroughColorAttributeName] = color;
@@ -63,254 +96,277 @@ -(void)addTypingAttributes:(UIColor *)color {
#pragma mark - Remove attributes
- (void)removeAttributes:(NSRange)range {
- NSTextStorage *textStorage = _input->textView.textStorage;
-
- LinkStyle *linkStyle = _input->stylesDict[@(Link)];
- InlineCodeStyle *inlineCodeStyle = _input->stylesDict[@(InlineCode)];
- BlockQuoteStyle *blockQuoteStyle = _input->stylesDict[@(BlockQuote)];
- MentionStyle *mentionStyle = _input->stylesDict[@(Mention)];
-
- NSArray *linkOccurrences = [linkStyle findAllOccurences:range];
- NSArray *inlineOccurrences = [inlineCodeStyle findAllOccurences:range];
- NSArray *blockQuoteOccurrences = [blockQuoteStyle findAllOccurences:range];
- NSArray *mentionOccurrences = [mentionStyle findAllOccurences:range];
-
- NSMutableSet *points = [NSMutableSet new];
- [points addObject:@(range.location)];
- [points addObject:@(NSMaxRange(range))];
-
- for (StylePair *pair in linkOccurrences) {
- [points addObject:@([pair.rangeValue rangeValue].location)];
- [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
- }
- for (StylePair *pair in inlineOccurrences) {
- [points addObject:@([pair.rangeValue rangeValue].location)];
- [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
- }
- for (StylePair *pair in blockQuoteOccurrences) {
- [points addObject:@([pair.rangeValue rangeValue].location)];
- [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
- }
- for (StylePair *pair in mentionOccurrences) {
- [points addObject:@([pair.rangeValue rangeValue].location)];
- [points addObject:@(NSMaxRange([pair.rangeValue rangeValue]))];
- }
-
- NSArray *sortedPoints = [points.allObjects sortedArrayUsingSelector:@selector(compare:)];
-
- [textStorage beginEditing];
- for (NSUInteger i = 0; i < sortedPoints.count - 1; i++) {
- NSUInteger start = sortedPoints[i].unsignedIntegerValue;
- NSUInteger end = sortedPoints[i + 1].unsignedIntegerValue;
- if (start >= end) continue;
-
- NSRange subrange = NSMakeRange(start, end - start);
-
- UIColor *baseColor = [self baseColorForLocation: subrange.location];
-
- [textStorage addAttribute:NSForegroundColorAttributeName value:baseColor range:subrange];
- [textStorage addAttribute:NSUnderlineColorAttributeName value:baseColor range:subrange];
- [textStorage addAttribute:NSStrikethroughColorAttributeName value:baseColor range:subrange];
- }
- [textStorage endEditing];
+ NSTextStorage *textStorage = _input->textView.textStorage;
+ if (range.length == 0)
+ return;
+
+ NSUInteger len = textStorage.length;
+ if (range.location >= len)
+ return;
+
+ NSUInteger max = MIN(NSMaxRange(range), len);
+
+ [textStorage beginEditing];
+
+ for (NSUInteger i = range.location; i < max; i++) {
+ UIColor *restoreColor = [self originalColorAtIndex:i];
+ NSDictionary *newAttributes = @{
+ NSForegroundColorAttributeName : restoreColor,
+ NSUnderlineColorAttributeName : restoreColor,
+ NSForegroundColorAttributeName : restoreColor,
+ };
+ [textStorage addAttributes:newAttributes range:NSMakeRange(i, 1)];
+ }
+
+ [textStorage endEditing];
}
- (void)removeTypingAttributes {
- NSMutableDictionary *newTypingAttrs = [_input->textView.typingAttributes mutableCopy];
- NSRange selectedRange = _input->textView.selectedRange;
- NSUInteger location = selectedRange.location;
-
- UIColor *baseColor = [self baseColorForLocation:location];
-
- newTypingAttrs[NSForegroundColorAttributeName] = baseColor;
- newTypingAttrs[NSUnderlineColorAttributeName] = baseColor;
- newTypingAttrs[NSStrikethroughColorAttributeName] = baseColor;
- _input->textView.typingAttributes = newTypingAttrs;
-}
+ NSMutableDictionary *newTypingAttrs =
+ [_input->textView.typingAttributes mutableCopy];
+ NSRange selectedRange = _input->textView.selectedRange;
+ NSUInteger location = selectedRange.location;
-#pragma mark - Detection
+ UIColor *baseColor = [self originalColorAtIndex:location];
--(BOOL)isInStyle:(NSRange) range styleType:(StyleType)styleType {
- id style = _input->stylesDict[@(styleType)];
-
- return (range.length > 0
- ? [style anyOccurence:range]
- : [style detectStyle:range]);
+ newTypingAttrs[NSForegroundColorAttributeName] = baseColor;
+ newTypingAttrs[NSUnderlineColorAttributeName] = baseColor;
+ newTypingAttrs[NSStrikethroughColorAttributeName] = baseColor;
+ _input->textView.typingAttributes = newTypingAttrs;
}
-- (BOOL)isInCodeBlockAndHasTheSameColor:(id)value :(NSRange)range {
- BOOL isInCodeBlock = [self isInStyle:range styleType: CodeBlock];
-
- return isInCodeBlock && [(UIColor *)value isEqualToColor:[_input->config codeBlockFgColor]];
-}
+#pragma mark - Main detection entry
+- (BOOL)styleConditionWithAttributes:(NSDictionary *)attrs
+ range:(NSRange)range {
+ UIColor *color = attrs[NSForegroundColorAttributeName];
+ if (!color)
+ return NO;
-- (BOOL)inLinkAndForegroundColorIsLinkColor:(id)value :(NSRange)range {
- BOOL isInLink = [self isInStyle:range styleType: Link];
-
- return isInLink && [(UIColor *)value isEqualToColor:[_input->config linkColor]];
-}
+ if (color == _input->config.primaryColor)
+ return NO;
-- (BOOL)inInlineCodeAndHasTheSameColor:(id)value :(NSRange)range {
- BOOL isInInlineCode = [self isInStyle:range styleType:InlineCode];
-
- return isInInlineCode && [(UIColor *)value isEqualToColor:[_input->config inlineCodeFgColor]];
+ return ![self isColorUsedByAnotherStyle:color attributes:attrs range:range];
}
-- (BOOL)inBlockQuoteAndHasTheSameColor:(id)value :(NSRange)range {
- BOOL isInBlockQuote = [self isInStyle:range styleType:BlockQuote];
-
- return isInBlockQuote && [(UIColor *)value isEqualToColor:[_input->config blockquoteColor]];
-}
+- (BOOL)styleCondition:(id)value range:(NSRange)range {
+ if (!value)
+ return NO;
-- (BOOL)inMentionAndHasTheSameColor:(id)value :(NSRange)range {
- MentionStyle *mentionStyle = _input->stylesDict[@(Mention)];
- BOOL isInMention = [self isInStyle:range styleType:Mention];
-
- if (!isInMention) return NO;
-
- MentionParams *params = [mentionStyle getMentionParamsAt:range.location];
- if (params == nil) return NO;
-
- MentionStyleProps *styleProps = [_input->config mentionStylePropsForIndicator:params.indicator];
-
- return [(UIColor *)value isEqualToColor:styleProps.color];
-}
+ NSTextStorage *ts = _input->textView.textStorage;
+ NSUInteger len = ts.length;
-- (BOOL)styleCondition:(id)value :(NSRange)range {
- if (value == nil) { return NO; }
-
- if ([(UIColor *)value isEqualToColor:_input->config.primaryColor]) { return NO; }
- if ([self inBlockQuoteAndHasTheSameColor:value :range]) { return NO; }
- if ([self inLinkAndForegroundColorIsLinkColor:value :range]) { return NO; }
- if ([self inInlineCodeAndHasTheSameColor:value :range]) { return NO; }
- if ([self inMentionAndHasTheSameColor:value :range]) { return NO; }
- if ([self isInCodeBlockAndHasTheSameColor:value :range]) { return NO; }
-
- return YES;
+ NSDictionary *attrs;
+ if (range.length == 0 || range.location >= len) {
+ attrs = _input->textView.typingAttributes;
+ } else {
+ NSUInteger loc = MIN(range.location, len - 1);
+ attrs = [ts attributesAtIndex:loc effectiveRange:nil];
+ }
+
+ return [self styleConditionWithAttributes:attrs range:range];
}
- (BOOL)detectStyle:(NSRange)range {
- UIColor *color = [self getColorInRange:range];
-
- return [self detectStyle:range color:color];
+ if (range.length >= 1) {
+ return [OccurenceUtils detect:NSForegroundColorAttributeName
+ withInput:_input
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value range:range];
+ }];
+ } else {
+ id value =
+ _input->textView.typingAttributes[NSForegroundColorAttributeName];
+ return [self styleCondition:value range:range];
+ }
}
- (BOOL)detectStyle:(NSRange)range color:(UIColor *)color {
- if(range.length >= 1) {
- return [OccurenceUtils detect:NSForegroundColorAttributeName withInput:_input inRange:range
- withCondition: ^BOOL(id _Nullable value, NSRange range) {
- return [(UIColor *)value isEqualToColor:color] && [self styleCondition:value :range];
- }
- ];
+ if (range.length >= 1) {
+ return [OccurenceUtils detect:NSForegroundColorAttributeName
+ withInput:_input
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [(UIColor *)value isEqualToColor:color] &&
+ [self styleCondition:value range:range];
+ }];
} else {
- return [OccurenceUtils detect:NSForegroundColorAttributeName withInput:_input atIndex:range.location checkPrevious:YES
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [(UIColor *)value isEqualToColor:color] && [self styleCondition:value :range];
- }
- ];
+ return [OccurenceUtils detect:NSForegroundColorAttributeName
+ withInput:_input
+ atIndex:range.location
+ checkPrevious:NO
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [(UIColor *)value isEqualToColor:color] &&
+ [self styleCondition:value range:range];
+ }];
}
}
-- (BOOL)detectExcludingColor:(UIColor *)excludedColor inRange:(NSRange)range {
- if (![self detectStyle:range]) {
- return NO;
- }
- UIColor *currentColor = [self getColorInRange:range];
- return currentColor != nil && ![currentColor isEqualToColor:excludedColor];
-}
-
- (BOOL)anyOccurence:(NSRange)range {
- return [OccurenceUtils any:NSForegroundColorAttributeName withInput:_input inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value :range];
- }
- ];
+ return [OccurenceUtils any:NSForegroundColorAttributeName
+ withInput:_input
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value range:range];
+ }];
}
- (NSArray *_Nullable)findAllOccurences:(NSRange)range {
- return [OccurenceUtils all:NSForegroundColorAttributeName withInput:_input inRange:range
- withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value :range];
- }
- ];
+ return [OccurenceUtils all:NSForegroundColorAttributeName
+ withInput:_input
+ inRange:range
+ withCondition:^BOOL(id _Nullable value, NSRange range) {
+ return [self styleCondition:value range:range];
+ }];
+}
+
+- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr {
+ [self addAttributes:range];
+}
+
+- (void)addAttributes:(NSRange)range {
+ // no-op
}
- (UIColor *)getColorAt:(NSUInteger)location {
NSRange effectiveRange = NSMakeRange(0, 0);
NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length);
-
- if(location == _input->textView.textStorage.length) {
- UIColor *typingColor = _input->textView.typingAttributes[NSForegroundColorAttributeName];
+
+ if (location == _input->textView.textStorage.length) {
+ UIColor *typingColor =
+ _input->textView.typingAttributes[NSForegroundColorAttributeName];
return typingColor ?: [_input->config primaryColor];
}
-
- return [_input->textView.textStorage
- attribute:NSForegroundColorAttributeName
- atIndex:location
- longestEffectiveRange: &effectiveRange
- inRange:inputRange
- ];
+
+ return [_input->textView.textStorage attribute:NSForegroundColorAttributeName
+ atIndex:location
+ longestEffectiveRange:&effectiveRange
+ inRange:inputRange];
}
- (UIColor *)getColorInRange:(NSRange)range {
NSUInteger location = range.location;
NSUInteger length = range.length;
-
+
NSRange effectiveRange = NSMakeRange(0, 0);
NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length);
-
- if(location == _input->textView.textStorage.length) {
- UIColor *typingColor = _input->textView.typingAttributes[NSForegroundColorAttributeName];
+
+ if (location == _input->textView.textStorage.length) {
+ UIColor *typingColor =
+ _input->textView.typingAttributes[NSForegroundColorAttributeName];
return typingColor ?: [_input->config primaryColor];
}
-
+
NSUInteger queryLocation = location;
if (length == 0 && location > 0) {
- queryLocation = location - 1;
+ queryLocation = location - 1;
}
-
- UIColor *color = [_input->textView.textStorage
- attribute:NSForegroundColorAttributeName
- atIndex:queryLocation
- longestEffectiveRange: &effectiveRange
- inRange:inputRange
- ];
-
+
+ UIColor *color =
+ [_input->textView.textStorage attribute:NSForegroundColorAttributeName
+ atIndex:queryLocation
+ longestEffectiveRange:&effectiveRange
+ inRange:inputRange];
+
return color;
}
-- (UIColor *)baseColorForLocation:(NSUInteger)location {
- BOOL inLink = [self isInStyle:NSMakeRange(location, 0) styleType:Link];
- BOOL inInlineCode = [self isInStyle:NSMakeRange(location, 0) styleType:InlineCode];
- BOOL inBlockQuote = [self isInStyle:NSMakeRange(location, 0) styleType:BlockQuote];
- BOOL inMention = [self isInStyle:NSMakeRange(location, 0) styleType:Mention];
-
- UIColor *baseColor = [_input->config primaryColor];
- if (inMention) {
- MentionStyle *mentionStyle = _input->stylesDict[@(Mention)];
- MentionParams *params = [mentionStyle getMentionParamsAt:location];
- if (params != nil) {
- MentionStyleProps *styleProps = [_input->config mentionStylePropsForIndicator:params.indicator];
- baseColor = styleProps.color;
- }
- } else if (inLink) {
- baseColor = [_input->config linkColor];
- } else if (inInlineCode) {
- baseColor = [_input->config inlineCodeFgColor];
- } else if (inBlockQuote) {
- baseColor = [_input->config blockquoteColor];
- }
- return baseColor;
+- (UIColor *)naturalColorForAttributes:(NSDictionary *)attrs
+ index:(NSUInteger)index {
+ for (NSNumber *num in self.coloredStyleTypes) {
+ UIColor *color = [self colorForStyle:(StyleType)num.integerValue
+ attributes:attrs
+ index:index];
+ if (color)
+ return color;
+ }
+
+ return _input->config.primaryColor;
+}
+
+- (UIColor *)originalColorAtIndex:(NSUInteger)index {
+ NSTextStorage *ts = _input->textView.textStorage;
+ NSUInteger len = ts.length;
+
+ if (len == 0)
+ return _input->config.primaryColor;
+
+ if (index >= len)
+ index = len - 1;
+
+ NSDictionary *attrs = [ts attributesAtIndex:index effectiveRange:nil];
+
+ return [self naturalColorForAttributes:attrs index:index];
}
- (void)removeColorInSelectedRange {
NSRange selectedRange = _input->textView.selectedRange;
-
- if(selectedRange.length > 0) {
- [self removeAttributes: selectedRange];
+
+ if (selectedRange.length > 0) {
+ [self removeAttributes:selectedRange];
} else {
[self removeTypingAttributes];
}
}
+- (BOOL)isColorUsedByAnotherStyle:(UIColor *)color
+ attributes:(NSDictionary *)attrs
+ range:(NSRange)range {
+ NSUInteger index = range.location;
+
+ for (NSNumber *num in self.coloredStyleTypes) {
+ UIColor *styleColor = [self colorForStyle:(StyleType)num.integerValue
+ attributes:attrs
+ index:index];
+
+ if (styleColor && [styleColor isEqual:color]) {
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
+- (UIColor *)colorForStyle:(StyleType)type
+ attributes:(NSDictionary *)attrs
+ index:(NSUInteger)index {
+ id style = _input->stylesDict[@(type)];
+ if (!style)
+ return nil;
+
+ NSAttributedStringKey key = [[style class] attributeKey];
+ id attr = attrs[key];
+ if (!attr || ![style styleCondition:attr range:NSMakeRange(index, 0)])
+ return nil;
+
+ InputConfig *config = _input->config;
+
+ switch (type) {
+ case Link:
+ return config.linkColor;
+
+ case InlineCode:
+ return config.inlineCodeFgColor;
+
+ case BlockQuote:
+ return config.blockquoteColor;
+
+ case CodeBlock:
+ return config.codeBlockFgColor;
+
+ case Mention: {
+ MentionParams *params = (MentionParams *)attr;
+ if (!params)
+ return nil;
+
+ MentionStyleProps *props =
+ [config mentionStylePropsForIndicator:params.indicator];
+ return props.color;
+ }
+
+ default:
+ return nil;
+ }
+}
+
@end
diff --git a/ios/styles/HeadingStyleBase.mm b/ios/styles/HeadingStyleBase.mm
index 24672d3ac..223c0af8e 100644
--- a/ios/styles/HeadingStyleBase.mm
+++ b/ios/styles/HeadingStyleBase.mm
@@ -10,6 +10,9 @@ @implementation HeadingStyleBase
+ (StyleType)getStyleType {
return None;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSFontAttributeName;
+}
- (CGFloat)getHeadingFontSize {
return 0;
}
@@ -100,7 +103,7 @@ - (void)removeAttributes:(NSRange)range {
options:0
usingBlock:^(id _Nullable value, NSRange range,
BOOL *_Nonnull stop) {
- if ([self styleCondition:value:range]) {
+ if ([self styleCondition:value range:range]) {
UIFont *newFont = [(UIFont *)value
setSize:[[[self typedInput]->config scaledPrimaryFontSize]
floatValue]];
@@ -138,11 +141,10 @@ - (void)removeTypingAttributes {
// there as well
[self removeAttributes:[self typedInput]->textView.selectedRange];
}
-
// when the traits already change, the getHeadginFontSize will return the new
// font size and no headings would be properly detected, so that's why we have
// to use the latest applied font size rather than that value.
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
UIFont *font = (UIFont *)value;
if (font == nullptr) {
return NO;
@@ -161,7 +163,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:[self typedInput]
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSFontAttributeName
@@ -169,7 +171,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -179,7 +181,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:[self typedInput]
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -188,7 +190,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:[self typedInput]
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/ImageStyle.mm b/ios/styles/ImageStyle.mm
index 8606fa817..fd3b277b9 100644
--- a/ios/styles/ImageStyle.mm
+++ b/ios/styles/ImageStyle.mm
@@ -20,6 +20,10 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (NSAttributedStringKey)attributeKey {
+ return ImageAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -54,7 +58,7 @@ - (void)removeTypingAttributes {
_input->textView.typingAttributes = currentAttributes;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
return [value isKindOfClass:[ImageData class]];
}
@@ -63,7 +67,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -73,7 +77,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:ImageAttributeName
@@ -81,7 +85,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -91,7 +95,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/InlineCodeStyle.mm b/ios/styles/InlineCodeStyle.mm
index 78f5c2fd1..17fa6abb4 100644
--- a/ios/styles/InlineCodeStyle.mm
+++ b/ios/styles/InlineCodeStyle.mm
@@ -17,6 +17,10 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSBackgroundColorAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -174,7 +178,7 @@ - (void)handleNewlines {
atIndex:index
effectiveRange:nil];
- if (bgColor != nil && [self styleCondition:bgColor:newlineRange]) {
+ if (bgColor != nil && [self styleCondition:bgColor range:newlineRange]) {
[self removeAttributes:newlineRange];
}
}
@@ -182,7 +186,7 @@ - (void)handleNewlines {
// emojis don't retain monospace font attribute so we check for the background
// color if there is no mention
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
UIColor *bgColor = (UIColor *)value;
MentionStyle *mStyle = _input->stylesDict[@([MentionStyle getStyleType])];
return bgColor != nullptr && mStyle != nullptr && ![mStyle detectStyle:range];
@@ -205,7 +209,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:currentRange
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
detected = detected && currentDetected;
}
@@ -217,7 +221,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -227,7 +231,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -236,7 +240,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/ItalicStyle.mm b/ios/styles/ItalicStyle.mm
index 82ddf7072..1c12c9484 100644
--- a/ios/styles/ItalicStyle.mm
+++ b/ios/styles/ItalicStyle.mm
@@ -15,6 +15,10 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSFontAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -91,7 +95,7 @@ - (void)removeTypingAttributes {
}
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
UIFont *font = (UIFont *)value;
return font != nullptr && [font isItalic];
}
@@ -102,7 +106,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSFontAttributeName
@@ -110,7 +114,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -120,7 +124,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -129,7 +133,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/LinkStyle.mm b/ios/styles/LinkStyle.mm
index 025a8b7f4..e752bbe6e 100644
--- a/ios/styles/LinkStyle.mm
+++ b/ios/styles/LinkStyle.mm
@@ -10,6 +10,8 @@
static NSString *const ManualLinkAttributeName = @"ManualLinkAttributeName";
static NSString *const AutomaticLinkAttributeName =
@"AutomaticLinkAttributeName";
+// custom NSAttributedStringKey to differentiate the link during html creation
+static NSString *const LinkAttributeName = @"LinkAttributeName";
@implementation LinkStyle {
EnrichedTextInputView *_input;
@@ -66,6 +68,10 @@ + (NSRegularExpression *)bareRegex {
return regex;
}
++ (NSAttributedStringKey)attributeKey {
+ return LinkAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -95,6 +101,8 @@ - (void)removeAttributes:(NSRange)range {
range:linkRange];
[_input->textView.textStorage removeAttribute:AutomaticLinkAttributeName
range:linkRange];
+ [_input->textView.textStorage removeAttribute:LinkAttributeName
+ range:linkRange];
[_input->textView.textStorage addAttribute:NSForegroundColorAttributeName
value:[_input->config primaryColor]
range:linkRange];
@@ -135,6 +143,8 @@ - (void)removeTypingAttributes {
range:linkRange];
[_input->textView.textStorage removeAttribute:AutomaticLinkAttributeName
range:linkRange];
+ [_input->textView.textStorage removeAttribute:LinkAttributeName
+ range:linkRange];
[_input->textView.textStorage addAttribute:NSForegroundColorAttributeName
value:[_input->config primaryColor]
range:linkRange];
@@ -164,7 +174,7 @@ - (void)removeTypingAttributes {
_input->textView.typingAttributes = newTypingAttrs;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSString *linkValue = (NSString *)value;
return linkValue != nullptr;
}
@@ -176,7 +186,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
return onlyLinks ? [self isSingleLinkIn:range] : NO;
} else {
@@ -190,7 +200,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -200,7 +210,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -222,11 +232,13 @@ - (void)addLink:(NSString *)text
if ([_input->config linkDecorationLine] == DecorationUnderline) {
newAttrs[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
}
+ NSString *copiedUrl = [url copy];
if (manual) {
- newAttrs[ManualLinkAttributeName] = [url copy];
+ newAttrs[ManualLinkAttributeName] = copiedUrl;
} else {
- newAttrs[AutomaticLinkAttributeName] = [url copy];
+ newAttrs[AutomaticLinkAttributeName] = copiedUrl;
}
+ newAttrs[LinkAttributeName] = copiedUrl;
if (range.length == 0) {
// insert link
@@ -274,8 +286,7 @@ - (void)addLink:(NSString *)text
// get exact link data at the given location if it exists
- (LinkData *)getLinkDataAt:(NSUInteger)location {
- NSRange manualLinkRange = NSMakeRange(0, 0);
- NSRange automaticLinkRange = NSMakeRange(0, 0);
+ NSRange linkRange = NSMakeRange(0, 0);
NSRange inputRange = NSMakeRange(0, _input->textView.textStorage.length);
// don't search at the very end of input
@@ -284,26 +295,15 @@ - (LinkData *)getLinkDataAt:(NSUInteger)location {
return nullptr;
}
- NSString *manualUrl =
- [_input->textView.textStorage attribute:ManualLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&manualLinkRange
- inRange:inputRange];
- NSString *automaticUrl =
- [_input->textView.textStorage attribute:AutomaticLinkAttributeName
- atIndex:searchLocation
- longestEffectiveRange:&automaticLinkRange
- inRange:inputRange];
+ NSString *linkUrl = [_input->textView.textStorage attribute:LinkAttributeName
+ atIndex:searchLocation
+ longestEffectiveRange:&linkRange
+ inRange:inputRange];
- if ((manualUrl == nullptr && automaticUrl == nullptr) ||
- (manualLinkRange.length == 0 && automaticLinkRange.length == 0)) {
+ if ((linkUrl == nullptr) || linkRange.length == 0) {
return nullptr;
}
- NSString *linkUrl = manualUrl == nullptr ? automaticUrl : manualUrl;
- NSRange linkRange =
- manualUrl == nullptr ? automaticLinkRange : manualLinkRange;
-
LinkData *data = [[LinkData alloc] init];
data.url = linkUrl;
data.text =
@@ -568,6 +568,9 @@ - (void)handleManualLinks:(NSString *)word inRange:(NSRange)wordRange {
[_input->textView.textStorage addAttribute:ManualLinkAttributeName
value:manualLinkMinValue
range:newRange];
+ [_input->textView.textStorage addAttribute:LinkAttributeName
+ value:manualLinkMinValue
+ range:newRange];
}
// link typing attributes need to be fixed after these changes
@@ -614,14 +617,14 @@ - (void)removeConnectedLinksIfNeeded:(NSString *)word range:(NSRange)wordRange {
withInput:_input
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
BOOL anyManual =
[OccurenceUtils any:ManualLinkAttributeName
withInput:_input
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
// both manual and automatic links are somewhere - delete!
@@ -637,7 +640,7 @@ - (void)removeConnectedLinksIfNeeded:(NSString *)word range:(NSRange)wordRange {
withInput:_input
inRange:wordRange
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
// only one link might be present!
diff --git a/ios/styles/MentionStyle.mm b/ios/styles/MentionStyle.mm
index a60b687fb..c6792cf9e 100644
--- a/ios/styles/MentionStyle.mm
+++ b/ios/styles/MentionStyle.mm
@@ -24,6 +24,10 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (NSAttributedStringKey)attributeKey {
+ return MentionAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -136,7 +140,7 @@ - (void)removeTypingAttributes {
_input->textView.typingAttributes = newTypingAttrs;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
MentionParams *params = (MentionParams *)value;
return params != nullptr;
}
@@ -147,7 +151,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [self getMentionParamsAt:range.location] != nullptr;
@@ -159,7 +163,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -168,7 +172,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/OrderedListStyle.mm b/ios/styles/OrderedListStyle.mm
index d9c918efa..916c660f0 100644
--- a/ios/styles/OrderedListStyle.mm
+++ b/ios/styles/OrderedListStyle.mm
@@ -17,6 +17,10 @@ + (BOOL)isParagraphStyle {
return YES;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSParagraphStyleAttributeName;
+}
+
- (CGFloat)getHeadIndent {
// lists are drawn manually
// margin before marker + gap between marker and paragraph
@@ -235,7 +239,7 @@ - (BOOL)tryHandlingListShorcutInRange:(NSRange)range
return NO;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSParagraphStyle *paragraph = (NSParagraphStyle *)value;
return paragraph != nullptr && paragraph.textLists.count == 1 &&
paragraph.textLists.firstObject.markerFormat ==
@@ -248,7 +252,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
@@ -256,7 +260,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -266,7 +270,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -275,7 +279,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/StrikethroughStyle.mm b/ios/styles/StrikethroughStyle.mm
index 9cf96d34a..1fe4538a2 100644
--- a/ios/styles/StrikethroughStyle.mm
+++ b/ios/styles/StrikethroughStyle.mm
@@ -14,6 +14,10 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSStrikethroughStyleAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -56,7 +60,7 @@ - (void)removeTypingAttributes {
_input->textView.typingAttributes = newTypingAttrs;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSNumber *strikethroughStyle = (NSNumber *)value;
return strikethroughStyle != nullptr &&
[strikethroughStyle intValue] != NSUnderlineStyleNone;
@@ -68,7 +72,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSStrikethroughStyleAttributeName
@@ -76,7 +80,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -86,7 +90,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -95,7 +99,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/UnderlineStyle.mm b/ios/styles/UnderlineStyle.mm
index 98050475d..38aefc5b2 100644
--- a/ios/styles/UnderlineStyle.mm
+++ b/ios/styles/UnderlineStyle.mm
@@ -14,6 +14,10 @@ + (BOOL)isParagraphStyle {
return NO;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSUnderlineStyleAttributeName;
+}
+
- (instancetype)initWithInput:(id)input {
self = [super init];
_input = (EnrichedTextInputView *)input;
@@ -91,7 +95,7 @@ - (BOOL)underlinedMentionConflictsInRange:(NSRange)range {
return conflicted;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSNumber *underlineStyle = (NSNumber *)value;
return underlineStyle != nullptr &&
[underlineStyle intValue] != NSUnderlineStyleNone &&
@@ -105,7 +109,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSUnderlineStyleAttributeName
@@ -113,7 +117,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:NO
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -123,7 +127,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -132,7 +136,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/styles/UnorderedListStyle.mm b/ios/styles/UnorderedListStyle.mm
index 9fb2f7b24..254a6158d 100644
--- a/ios/styles/UnorderedListStyle.mm
+++ b/ios/styles/UnorderedListStyle.mm
@@ -17,6 +17,10 @@ + (BOOL)isParagraphStyle {
return YES;
}
++ (NSAttributedStringKey)attributeKey {
+ return NSParagraphStyleAttributeName;
+}
+
- (CGFloat)getHeadIndent {
// lists are drawn manually
// margin before bullet + gap between bullet and paragraph
@@ -234,7 +238,7 @@ - (BOOL)tryHandlingListShorcutInRange:(NSRange)range
return NO;
}
-- (BOOL)styleCondition:(id _Nullable)value:(NSRange)range {
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range {
NSParagraphStyle *paragraph = (NSParagraphStyle *)value;
return paragraph != nullptr && paragraph.textLists.count == 1 &&
paragraph.textLists.firstObject.markerFormat == NSTextListMarkerDisc;
@@ -246,7 +250,7 @@ - (BOOL)detectStyle:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
} else {
return [OccurenceUtils detect:NSParagraphStyleAttributeName
@@ -254,7 +258,7 @@ - (BOOL)detectStyle:(NSRange)range {
atIndex:range.location
checkPrevious:YES
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
}
@@ -264,7 +268,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
@@ -273,7 +277,7 @@ - (BOOL)anyOccurence:(NSRange)range {
withInput:_input
inRange:range
withCondition:^BOOL(id _Nullable value, NSRange range) {
- return [self styleCondition:value:range];
+ return [self styleCondition:value range:range];
}];
}
diff --git a/ios/utils/BaseStyleProtocol.h b/ios/utils/BaseStyleProtocol.h
index 5e08e558a..a6c8554ab 100644
--- a/ios/utils/BaseStyleProtocol.h
+++ b/ios/utils/BaseStyleProtocol.h
@@ -5,6 +5,8 @@
@protocol BaseStyleProtocol
+ (StyleType)getStyleType;
+ (BOOL)isParagraphStyle;
++ (NSAttributedStringKey _Nonnull)attributeKey;
+- (BOOL)styleCondition:(id _Nullable)value range:(NSRange)range;
- (instancetype _Nonnull)initWithInput:(id _Nonnull)input;
- (void)applyStyle:(NSRange)range;
- (void)addAttributes:(NSRange)range withTypingAttr:(BOOL)withTypingAttr;
diff --git a/ios/utils/ColorExtension.mm b/ios/utils/ColorExtension.mm
index 1c0e90f53..55dda3b28 100644
--- a/ios/utils/ColorExtension.mm
+++ b/ios/utils/ColorExtension.mm
@@ -39,153 +39,166 @@ - (UIColor *)colorWithAlphaIfNotTransparent:(CGFloat)newAlpha {
@implementation UIColor (FromString)
+ (UIColor *)colorFromString:(NSString *)string {
- if (!string) return nil;
-
- NSString *input = [[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString];
-
- // Expanded named colors (add more from CSS list as needed)
- NSDictionary *namedColors = @{
- @"red": [UIColor redColor],
- @"green": [UIColor greenColor],
- @"blue": [UIColor blueColor],
- @"black": [UIColor blackColor],
- @"white": [UIColor whiteColor],
- @"yellow": [UIColor yellowColor],
- @"gray": [UIColor grayColor],
- @"grey": [UIColor grayColor],
- @"transparent": [UIColor clearColor],
- @"aliceblue": [UIColor colorWithRed:0.941 green:0.973 blue:1.0 alpha:1.0],
- };
-
- UIColor *namedColor = namedColors[input];
- if (namedColor) return namedColor;
-
- // Hex parsing (including short forms)
- if ([input hasPrefix:@"#"]) {
- NSString *hex = [input substringFromIndex:1];
- if (hex.length == 3 || hex.length == 4) { // Short hex: #rgb or #rgba
- NSMutableString *expanded = [NSMutableString string];
- for (NSUInteger i = 0; i < hex.length; i++) {
- unichar c = [hex characterAtIndex:i];
- [expanded appendFormat:@"%c%c", c, c];
- }
- hex = expanded;
- }
- if (hex.length == 6 || hex.length == 8) {
- unsigned int hexValue = 0;
- NSScanner *scanner = [NSScanner scannerWithString:hex];
- if ([scanner scanHexInt:&hexValue]) {
- CGFloat r, g, b, a = 1.0;
- if (hex.length == 6) {
- r = ((hexValue & 0xFF0000) >> 16) / 255.0;
- g = ((hexValue & 0x00FF00) >> 8) / 255.0;
- b = (hexValue & 0x0000FF) / 255.0;
- } else {
- r = ((hexValue & 0xFF000000) >> 24) / 255.0;
- g = ((hexValue & 0x00FF0000) >> 16) / 255.0;
- b = ((hexValue & 0x0000FF00) >> 8) / 255.0;
- a = (hexValue & 0x000000FF) / 255.0;
- }
- return [UIColor colorWithRed:r green:g blue:b alpha:a];
- }
- }
- return nil;
+ if (!string)
+ return nil;
+
+ NSString *input = [[string
+ stringByTrimmingCharactersInSet:[NSCharacterSet
+ whitespaceAndNewlineCharacterSet]]
+ lowercaseString];
+
+ // Expanded named colors (add more from CSS list as needed)
+ NSDictionary *namedColors = @{
+ @"red" : [UIColor redColor],
+ @"green" : [UIColor greenColor],
+ @"blue" : [UIColor blueColor],
+ @"black" : [UIColor blackColor],
+ @"white" : [UIColor whiteColor],
+ @"yellow" : [UIColor yellowColor],
+ @"gray" : [UIColor grayColor],
+ @"grey" : [UIColor grayColor],
+ @"transparent" : [UIColor clearColor],
+ @"aliceblue" : [UIColor colorWithRed:0.941 green:0.973 blue:1.0 alpha:1.0],
+ };
+
+ UIColor *namedColor = namedColors[input];
+ if (namedColor)
+ return namedColor;
+
+ // Hex parsing (including short forms)
+ if ([input hasPrefix:@"#"]) {
+ NSString *hex = [input substringFromIndex:1];
+ if (hex.length == 3 || hex.length == 4) { // Short hex: #rgb or #rgba
+ NSMutableString *expanded = [NSMutableString string];
+ for (NSUInteger i = 0; i < hex.length; i++) {
+ unichar c = [hex characterAtIndex:i];
+ [expanded appendFormat:@"%c%c", c, c];
+ }
+ hex = expanded;
}
-
- // RGB/RGBA parsing (with percentages)
- if ([input hasPrefix:@"rgb"] || [input hasPrefix:@"rgba"]) {
- NSString *clean = [input stringByReplacingOccurrencesOfString:@"rgb" withString:@""];
- clean = [clean stringByReplacingOccurrencesOfString:@"a" withString:@""];
- clean = [clean stringByReplacingOccurrencesOfString:@"(" withString:@""];
- clean = [clean stringByReplacingOccurrencesOfString:@")" withString:@""];
- clean = [clean stringByReplacingOccurrencesOfString:@" " withString:@""];
-
- NSArray *parts = [clean componentsSeparatedByString:@","];
- if (parts.count == 3 || parts.count == 4) {
- CGFloat r = [self parseColorComponent:parts[0] max:255.0];
- CGFloat g = [self parseColorComponent:parts[1] max:255.0];
- CGFloat b = [self parseColorComponent:parts[2] max:255.0];
- CGFloat a = parts.count == 4 ? [self parseColorComponent:parts[3] max:1.0] : 1.0;
- if (r >= 0 && g >= 0 && b >= 0 && a >= 0) {
- return [UIColor colorWithRed:r green:g blue:b alpha:a];
- }
+ if (hex.length == 6 || hex.length == 8) {
+ unsigned int hexValue = 0;
+ NSScanner *scanner = [NSScanner scannerWithString:hex];
+ if ([scanner scanHexInt:&hexValue]) {
+ CGFloat r, g, b, a = 1.0;
+ if (hex.length == 6) {
+ r = ((hexValue & 0xFF0000) >> 16) / 255.0;
+ g = ((hexValue & 0x00FF00) >> 8) / 255.0;
+ b = (hexValue & 0x0000FF) / 255.0;
+ } else {
+ r = ((hexValue & 0xFF000000) >> 24) / 255.0;
+ g = ((hexValue & 0x00FF0000) >> 16) / 255.0;
+ b = ((hexValue & 0x0000FF00) >> 8) / 255.0;
+ a = (hexValue & 0x000000FF) / 255.0;
}
- return nil;
+ return [UIColor colorWithRed:r green:g blue:b alpha:a];
+ }
}
-
- // HSL/HSLA parsing (basic implementation)
- if ([input hasPrefix:@"hsl"] || [input hasPrefix:@"hsla"]) {
- NSString *clean = [input stringByReplacingOccurrencesOfString:@"hsl" withString:@""];
- clean = [clean stringByReplacingOccurrencesOfString:@"a" withString:@""];
- clean = [clean stringByReplacingOccurrencesOfString:@"(" withString:@""];
- clean = [clean stringByReplacingOccurrencesOfString:@")" withString:@""];
- clean = [clean stringByReplacingOccurrencesOfString:@" " withString:@""];
-
- NSArray *parts = [clean componentsSeparatedByString:@","];
- if (parts.count == 3 || parts.count == 4) {
- CGFloat h = [self parseColorComponent:parts[0] max:360.0];
- CGFloat s = [self parseColorComponent:parts[1] max:1.0];
- CGFloat l = [self parseColorComponent:parts[2] max:1.0];
- CGFloat a = parts.count == 4 ? [self parseColorComponent:parts[3] max:1.0] : 1.0;
- return [UIColor colorWithHue:h / 360.0 saturation:s brightness:l alpha:a]; // Note: Uses HSB approximation
- }
- return nil;
+ return nil;
+ }
+
+ // RGB/RGBA parsing (with percentages)
+ if ([input hasPrefix:@"rgb"] || [input hasPrefix:@"rgba"]) {
+ NSString *clean = [input stringByReplacingOccurrencesOfString:@"rgb"
+ withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@"a" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@"(" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@")" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@" " withString:@""];
+
+ NSArray *parts = [clean componentsSeparatedByString:@","];
+ if (parts.count == 3 || parts.count == 4) {
+ CGFloat r = [self parseColorComponent:parts[0] max:255.0];
+ CGFloat g = [self parseColorComponent:parts[1] max:255.0];
+ CGFloat b = [self parseColorComponent:parts[2] max:255.0];
+ CGFloat a =
+ parts.count == 4 ? [self parseColorComponent:parts[3] max:1.0] : 1.0;
+ if (r >= 0 && g >= 0 && b >= 0 && a >= 0) {
+ return [UIColor colorWithRed:r green:g blue:b alpha:a];
+ }
}
-
return nil;
+ }
+
+ // HSL/HSLA parsing (basic implementation)
+ if ([input hasPrefix:@"hsl"] || [input hasPrefix:@"hsla"]) {
+ NSString *clean = [input stringByReplacingOccurrencesOfString:@"hsl"
+ withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@"a" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@"(" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@")" withString:@""];
+ clean = [clean stringByReplacingOccurrencesOfString:@" " withString:@""];
+
+ NSArray *parts = [clean componentsSeparatedByString:@","];
+ if (parts.count == 3 || parts.count == 4) {
+ CGFloat h = [self parseColorComponent:parts[0] max:360.0];
+ CGFloat s = [self parseColorComponent:parts[1] max:1.0];
+ CGFloat l = [self parseColorComponent:parts[2] max:1.0];
+ CGFloat a =
+ parts.count == 4 ? [self parseColorComponent:parts[3] max:1.0] : 1.0;
+ return [UIColor colorWithHue:h / 360.0
+ saturation:s
+ brightness:l
+ alpha:a]; // Note: Uses HSB approximation
+ }
+ return nil;
+ }
+
+ return nil;
}
+ (CGFloat)parseColorComponent:(NSString *)comp max:(CGFloat)max {
- if ([comp hasSuffix:@"%"]) {
- comp = [comp stringByReplacingOccurrencesOfString:@"%" withString:@""];
- return [comp floatValue] / 100.0;
- }
- return [comp floatValue] / max;
+ if ([comp hasSuffix:@"%"]) {
+ comp = [comp stringByReplacingOccurrencesOfString:@"%" withString:@""];
+ return [comp floatValue] / 100.0;
+ }
+ return [comp floatValue] / max;
}
@end
@implementation UIColor (HexString)
- (NSString *)hexString {
- CGColorRef colorRef = self.CGColor;
- size_t numComponents = CGColorGetNumberOfComponents(colorRef);
- const CGFloat *components = CGColorGetComponents(colorRef);
-
- CGFloat r = 0.0, g = 0.0, b = 0.0, a = 1.0;
-
- if (numComponents == 2) { // Monochrome (grayscale)
- r = components[0];
- g = components[0];
- b = components[0];
- a = components[1];
- } else if (numComponents == 4) { // RGBA
- r = components[0];
- g = components[1];
- b = components[2];
- a = components[3];
- } else if (numComponents == 3) { // RGB (no alpha)
- r = components[0];
- g = components[1];
- b = components[2];
- } else {
- // Unsupported color space (e.g., pattern colors)
- return @"#FFFFFF";
- }
-
- int red = (int)lroundf(r * 255.0f);
- int green = (int)lroundf(g * 255.0f);
- int blue = (int)lroundf(b * 255.0f);
- int alpha = (int)lroundf(a * 255.0f);
-
- // Clamp values to 0-255 to prevent overflow
- red = MAX(0, MIN(255, red));
- green = MAX(0, MIN(255, green));
- blue = MAX(0, MIN(255, blue));
- alpha = MAX(0, MIN(255, alpha));
-
- if (alpha < 255) {
- return [NSString stringWithFormat:@"#%02X%02X%02X%02X", red, green, blue, alpha];
- } else {
- return [NSString stringWithFormat:@"#%02X%02X%02X", red, green, blue];
- }
+ CGColorRef colorRef = self.CGColor;
+ size_t numComponents = CGColorGetNumberOfComponents(colorRef);
+ const CGFloat *components = CGColorGetComponents(colorRef);
+
+ CGFloat r = 0.0, g = 0.0, b = 0.0, a = 1.0;
+
+ if (numComponents == 2) { // Monochrome (grayscale)
+ r = components[0];
+ g = components[0];
+ b = components[0];
+ a = components[1];
+ } else if (numComponents == 4) { // RGBA
+ r = components[0];
+ g = components[1];
+ b = components[2];
+ a = components[3];
+ } else if (numComponents == 3) { // RGB (no alpha)
+ r = components[0];
+ g = components[1];
+ b = components[2];
+ } else {
+ // Unsupported color space (e.g., pattern colors)
+ return @"#FFFFFF";
+ }
+
+ int red = (int)lroundf(r * 255.0f);
+ int green = (int)lroundf(g * 255.0f);
+ int blue = (int)lroundf(b * 255.0f);
+ int alpha = (int)lroundf(a * 255.0f);
+
+ // Clamp values to 0-255 to prevent overflow
+ red = MAX(0, MIN(255, red));
+ green = MAX(0, MIN(255, green));
+ blue = MAX(0, MIN(255, blue));
+ alpha = MAX(0, MIN(255, alpha));
+
+ if (alpha < 255) {
+ return [NSString
+ stringWithFormat:@"#%02X%02X%02X%02X", red, green, blue, alpha];
+ } else {
+ return [NSString stringWithFormat:@"#%02X%02X%02X", red, green, blue];
+ }
}
@end
diff --git a/ios/utils/StyleHeaders.h b/ios/utils/StyleHeaders.h
index be10851fe..ca9677d2e 100644
--- a/ios/utils/StyleHeaders.h
+++ b/ios/utils/StyleHeaders.h
@@ -21,10 +21,9 @@
@end
@interface ColorStyle : NSObject
-@property (nonatomic, strong) UIColor *color;
+@property(nonatomic, strong) UIColor *color;
- (UIColor *)getColorAt:(NSUInteger)location;
- (void)applyStyle:(NSRange)range color:(UIColor *)color;
-- (BOOL)detectExcludingColor:(UIColor *)excludedColor inRange:(NSRange)range;
- (void)removeColorInSelectedRange;
@end
From af1c209e33bad539f8ca458dbe64b52fb910ef37 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Tue, 13 Jan 2026 01:43:59 +0100
Subject: [PATCH 10/12] feat: add colored style on android
---
.../enriched/EnrichedTextInputView.kt | 9 +
.../enriched/EnrichedTextInputViewManager.kt | 18 +-
.../enriched/events/OnColorChangeEvent.kt | 28 +++
.../enriched/spans/EnrichedColoredSpan.kt | 20 ++
.../swmansion/enriched/spans/EnrichedSpans.kt | 9 +
.../swmansion/enriched/styles/InlineStyles.kt | 193 +++++++++++++++++-
.../enriched/utils/EnrichedParser.java | 72 +++++++
.../enriched/utils/EnrichedSelection.kt | 4 +
.../enriched/utils/EnrichedSpanState.kt | 57 ++++++
apps/example/src/App.tsx | 14 +-
src/EnrichedTextInputNativeComponent.ts | 2 +-
11 files changed, 408 insertions(+), 18 deletions(-)
create mode 100644 android/src/main/java/com/swmansion/enriched/events/OnColorChangeEvent.kt
create mode 100644 android/src/main/java/com/swmansion/enriched/spans/EnrichedColoredSpan.kt
diff --git a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt
index e14f51389..e6666ce47 100644
--- a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt
+++ b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt
@@ -691,6 +691,15 @@ class EnrichedTextInputView : AppCompatEditText {
dispatcher?.dispatchEvent(OnRequestHtmlResultEvent(surfaceId, id, requestId, html, experimentalSynchronousEvents))
}
+ fun setColor(color: Int) {
+ val isValid = verifyStyle(EnrichedSpans.COLOR)
+ if (!isValid) return
+
+ inlineStyles?.setColorStyle(color)
+ }
+
+ fun removeColor() = inlineStyles?.removeColorSpan()
+
// Sometimes setting up style triggers many changes in sequence
// Eg. removing conflicting styles -> changing text -> applying spans
// In such scenario we want to prevent from handling side effects (eg. onTextChanged)
diff --git a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt
index e0d87a3c1..1c8e2ea11 100644
--- a/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt
+++ b/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt
@@ -1,6 +1,7 @@
package com.swmansion.enriched
import android.content.Context
+import androidx.core.graphics.toColorInt
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
@@ -338,15 +339,24 @@ class EnrichedTextInputViewManager :
view?.verifyAndToggleStyle(EnrichedSpans.UNORDERED_LIST)
}
- override fun setColor(view: EnrichedTextInputView?, color: String) {
- // no-op for now
+ override fun setColor(
+ view: EnrichedTextInputView?,
+ color: String,
+ ) {
+ view?.setColor(color.toColorInt())
}
override fun removeColor(view: EnrichedTextInputView?) {
- // no-op for now
+ view?.removeColor()
}
- override fun addLink(view: EnrichedTextInputView?, start: Int, end: Int, text: String, url: String) {
+ override fun addLink(
+ view: EnrichedTextInputView?,
+ start: Int,
+ end: Int,
+ text: String,
+ url: String,
+ ) {
view?.addLink(start, end, text, url)
}
diff --git a/android/src/main/java/com/swmansion/enriched/events/OnColorChangeEvent.kt b/android/src/main/java/com/swmansion/enriched/events/OnColorChangeEvent.kt
new file mode 100644
index 000000000..e8877b88d
--- /dev/null
+++ b/android/src/main/java/com/swmansion/enriched/events/OnColorChangeEvent.kt
@@ -0,0 +1,28 @@
+package com.swmansion.enriched.events
+
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.uimanager.events.Event
+
+class OnColorChangeEvent(
+ surfaceId: Int,
+ viewId: Int,
+ private val experimentalSynchronousEvents: Boolean,
+ private val color: String?,
+) : Event(surfaceId, viewId) {
+ override fun getEventName(): String = EVENT_NAME
+
+ override fun getEventData(): WritableMap {
+ val eventData: WritableMap = Arguments.createMap()
+
+ eventData.putString("color", color)
+
+ return eventData
+ }
+
+ override fun experimental_isSynchronous(): Boolean = experimentalSynchronousEvents
+
+ companion object {
+ const val EVENT_NAME: String = "onColorChangeInSelection"
+ }
+}
diff --git a/android/src/main/java/com/swmansion/enriched/spans/EnrichedColoredSpan.kt b/android/src/main/java/com/swmansion/enriched/spans/EnrichedColoredSpan.kt
new file mode 100644
index 000000000..9b89a3546
--- /dev/null
+++ b/android/src/main/java/com/swmansion/enriched/spans/EnrichedColoredSpan.kt
@@ -0,0 +1,20 @@
+package com.swmansion.enriched.spans
+
+import android.text.style.ForegroundColorSpan
+import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
+import com.swmansion.enriched.styles.HtmlStyle
+
+class EnrichedColoredSpan(
+ htmlStyle: HtmlStyle,
+ val color: Int,
+) : ForegroundColorSpan(color),
+ EnrichedInlineSpan {
+ override val dependsOnHtmlStyle: Boolean = false
+
+ override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedColoredSpan = EnrichedColoredSpan(htmlStyle, color)
+
+ fun getHexColor(): String {
+ val rgb = foregroundColor and 0x00FFFFFF
+ return String.format("#%06X", rgb)
+ }
+}
diff --git a/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt b/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt
index 3589a73f7..d70720c1b 100644
--- a/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt
+++ b/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt
@@ -34,6 +34,7 @@ object EnrichedSpans {
const val UNDERLINE = "underline"
const val STRIKETHROUGH = "strikethrough"
const val INLINE_CODE = "inline_code"
+ const val COLOR = "color"
// paragraph styles
const val H1 = "h1"
@@ -61,6 +62,7 @@ object EnrichedSpans {
UNDERLINE to BaseSpanConfig(EnrichedUnderlineSpan::class.java),
STRIKETHROUGH to BaseSpanConfig(EnrichedStrikeThroughSpan::class.java),
INLINE_CODE to BaseSpanConfig(EnrichedInlineCodeSpan::class.java),
+ COLOR to BaseSpanConfig(EnrichedColoredSpan::class.java),
)
val paragraphSpans: Map =
@@ -106,6 +108,13 @@ object EnrichedSpans {
StylesMergingConfig(blockingStyles = blockingStyles.toTypedArray())
}
+ COLOR -> {
+ StylesMergingConfig(
+ conflictingStyles = arrayOf(INLINE_CODE),
+ blockingStyles = arrayOf(CODE_BLOCK, MENTION),
+ )
+ }
+
ITALIC -> {
StylesMergingConfig(
blockingStyles = arrayOf(CODE_BLOCK),
diff --git a/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt b/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt
index 967f6a223..7de95afce 100644
--- a/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt
+++ b/android/src/main/java/com/swmansion/enriched/styles/InlineStyles.kt
@@ -3,6 +3,7 @@ package com.swmansion.enriched.styles
import android.text.Editable
import android.text.Spannable
import com.swmansion.enriched.EnrichedTextInputView
+import com.swmansion.enriched.spans.EnrichedColoredSpan
import com.swmansion.enriched.spans.EnrichedSpans
import com.swmansion.enriched.utils.getSafeSpanBoundaries
@@ -101,21 +102,203 @@ class InlineStyles(
}
}
+ private fun applyColorSpan(
+ spannable: Spannable,
+ start: Int,
+ end: Int,
+ color: Int,
+ ) {
+ val (safeStart, safeEnd) = spannable.getSafeSpanBoundaries(start, end)
+ spannable.setSpan(
+ EnrichedColoredSpan(view.htmlStyle, color),
+ safeStart,
+ safeEnd,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
+ )
+ }
+
+ private fun splitExistingColorSpans(
+ spannable: Spannable,
+ start: Int,
+ end: Int,
+ onRemain: (s: Int, e: Int, color: Int) -> Unit,
+ ) {
+ val spans = spannable.getSpans(start, end, EnrichedColoredSpan::class.java)
+ for (span in spans) {
+ val spanStart = spannable.getSpanStart(span)
+ val spanEnd = spannable.getSpanEnd(span)
+ val color = span.color
+
+ spannable.removeSpan(span)
+
+ if (spanStart < start) {
+ onRemain(spanStart, start, color)
+ }
+
+ if (spanEnd > end) {
+ onRemain(end, spanEnd, color)
+ }
+ }
+ }
+
+ private fun mergeAdjacentColors(spannable: Spannable) {
+ val colorSpans =
+ spannable
+ .getSpans(0, spannable.length, EnrichedColoredSpan::class.java)
+ .sortedBy { spannable.getSpanStart(it) }
+
+ var index = 0
+ while (index < colorSpans.size - 1) {
+ val currentSpan = colorSpans[index]
+ val nextSpan = colorSpans[index + 1]
+
+ val currentStart = spannable.getSpanStart(currentSpan)
+ val currentEnd = spannable.getSpanEnd(currentSpan)
+ val nextStart = spannable.getSpanStart(nextSpan)
+ val nextEnd = spannable.getSpanEnd(nextSpan)
+
+ if (currentEnd == nextStart && currentSpan.color == nextSpan.color) {
+ spannable.removeSpan(currentSpan)
+ spannable.removeSpan(nextSpan)
+
+ applyColorSpan(spannable, currentStart, nextEnd, currentSpan.color)
+
+ return mergeAdjacentColors(spannable)
+ }
+
+ index++
+ }
+ }
+
+ private fun isFullyColoredWith(
+ spannable: Spannable,
+ start: Int,
+ end: Int,
+ color: Int,
+ ): Boolean {
+ val spans = spannable.getSpans(start, end, EnrichedColoredSpan::class.java)
+ if (spans.isEmpty()) return false
+
+ val allSame = spans.all { it.color == color }
+
+ if (!allSame) {
+ return false
+ }
+
+ val minStart = spans.minOf { spannable.getSpanStart(it) }
+ val maxEnd = spans.maxOf { spannable.getSpanEnd(it) }
+
+ return minStart <= start && maxEnd >= end
+ }
+
+ fun setColorStyle(color: Int) {
+ val (start, end) = view.selection?.getInlineSelection() ?: return
+ val spannable = view.text as Spannable
+
+ if (start == end) {
+ val spanState = view.spanState
+ if (spanState?.colorStart != null && spanState.typingColor == color) {
+ view.spanState.setColorStart(null, null)
+ } else {
+ view.spanState?.setColorStart(start, color)
+ }
+ return
+ }
+
+ if (isFullyColoredWith(spannable, start, end, color)) {
+ removeColorRange(start, end)
+ view.spanState?.setColorStart(null, null)
+ view.selection.validateStyles()
+ return
+ }
+
+ splitExistingColorSpans(spannable, start, end) { spanStart, spanEnd, existingColor ->
+ applyColorSpan(spannable, spanStart, spanEnd, existingColor)
+ }
+
+ applyColorSpan(spannable, start, end, color)
+
+ mergeAdjacentColors(spannable)
+
+ view.spanState?.setColorStart(null, null)
+ view.selection.validateStyles()
+ }
+
+ private fun removeColorRange(
+ start: Int,
+ end: Int,
+ ) {
+ val spannable = view.text as Spannable
+
+ splitExistingColorSpans(spannable, start, end) { spanStart, spanEnd, color ->
+ if (spanStart < start) applyColorSpan(spannable, spanStart, start, color)
+ if (spanEnd > end) applyColorSpan(spannable, end, spanEnd, color)
+ }
+ }
+
+ fun removeColorSpan() {
+ val (start, end) = view.selection?.getInlineSelection() ?: return
+
+ if (start == end) {
+ view.spanState?.setColorStart(null, null)
+ return
+ }
+
+ removeColorRange(start, end)
+
+ view.spanState?.setColorStart(null, null)
+ view.selection.validateStyles()
+ }
+
+ private fun applyTypingColorIfActive(
+ spannable: Spannable,
+ cursor: Int,
+ ) {
+ val state = view.spanState ?: return
+ val colorStart = state.colorStart ?: return
+ val color = state.typingColor ?: return
+
+ val existing =
+ spannable
+ .getSpans(colorStart, colorStart, EnrichedColoredSpan::class.java)
+ .firstOrNull { it.color == color }
+
+ if (existing != null) {
+ val spanStart = spannable.getSpanStart(existing)
+ val spanEnd = spannable.getSpanEnd(existing)
+
+ if (cursor > spanEnd) {
+ spannable.removeSpan(existing)
+ applyColorSpan(spannable, spanStart, cursor, color)
+ }
+
+ view.spanState.setColorStart(cursor, color)
+ return
+ }
+
+ applyColorSpan(spannable, colorStart, cursor, color)
+ view.spanState.setColorStart(cursor, color)
+ }
+
fun afterTextChanged(
- s: Editable,
+ editable: Editable,
endCursorPosition: Int,
) {
for ((style, config) in EnrichedSpans.inlineSpans) {
val start = view.spanState?.getStart(style) ?: continue
var end = endCursorPosition
- val spans = s.getSpans(start, end, config.clazz)
+ if (config.clazz == EnrichedColoredSpan::class.java) {
+ applyTypingColorIfActive(editable, end)
+ continue
+ }
+ val spans = editable.getSpans(start, end, config.clazz)
for (span in spans) {
- end = s.getSpanEnd(span).coerceAtLeast(end)
- s.removeSpan(span)
+ end = editable.getSpanEnd(span).coerceAtLeast(end)
+ editable.removeSpan(span)
}
- setSpan(s, config.clazz, start, end)
+ setSpan(editable, config.clazz, start, end)
}
}
diff --git a/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java b/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java
index 82c89efd6..572fbb933 100644
--- a/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java
+++ b/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java
@@ -1,5 +1,6 @@
package com.swmansion.enriched.utils;
+import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.Layout;
@@ -9,9 +10,11 @@
import android.text.TextUtils;
import android.text.style.AlignmentSpan;
import android.text.style.ParagraphStyle;
+import androidx.annotation.Nullable;
import com.swmansion.enriched.spans.EnrichedBlockQuoteSpan;
import com.swmansion.enriched.spans.EnrichedBoldSpan;
import com.swmansion.enriched.spans.EnrichedCodeBlockSpan;
+import com.swmansion.enriched.spans.EnrichedColoredSpan;
import com.swmansion.enriched.spans.EnrichedH1Span;
import com.swmansion.enriched.spans.EnrichedH2Span;
import com.swmansion.enriched.spans.EnrichedH3Span;
@@ -306,6 +309,10 @@ private static void withinParagraph(StringBuilder out, Spanned text, int start,
// Don't output the placeholder character underlying the image.
i = next;
}
+ if (style[j] instanceof EnrichedColoredSpan) {
+ String color = ((EnrichedColoredSpan) style[j]).getHexColor();
+ out.append("");
+ }
}
withinStyle(out, text, i, next);
for (int j = style.length - 1; j >= 0; j--) {
@@ -330,6 +337,9 @@ private static void withinParagraph(StringBuilder out, Spanned text, int start,
if (style[j] instanceof EnrichedItalicSpan) {
out.append("");
}
+ if (style[j] instanceof EnrichedColoredSpan) {
+ out.append("");
+ }
}
}
}
@@ -498,6 +508,8 @@ private void handleStartTag(String tag, Attributes attributes) {
start(mSpannableStringBuilder, new Code());
} else if (tag.equalsIgnoreCase("mention")) {
startMention(mSpannableStringBuilder, attributes);
+ } else if (tag.equalsIgnoreCase("font")) {
+ startFont(mSpannableStringBuilder, attributes);
}
}
@@ -540,6 +552,8 @@ private void handleEndTag(String tag) {
end(mSpannableStringBuilder, Code.class, new EnrichedInlineCodeSpan(mStyle));
} else if (tag.equalsIgnoreCase("mention")) {
endMention(mSpannableStringBuilder, mStyle);
+ } else if (tag.equalsIgnoreCase("font")) {
+ endFont(mSpannableStringBuilder, mStyle);
}
}
@@ -926,4 +940,62 @@ public Alignment(Layout.Alignment alignment) {
mAlignment = alignment;
}
}
+
+ private static class Font {
+ public int color;
+
+ public Font(int color) {
+ this.color = color;
+ }
+ }
+
+ private static void startFont(Editable text, @Nullable Attributes attributes) {
+ if (attributes == null) {
+ return;
+ }
+
+ int color = parseCssColor(attributes.getValue("", "color"));
+
+ start(text, new Font(color));
+ }
+
+ private static void endFont(Editable text, HtmlStyle style) {
+ Font font = getLast(text, Font.class);
+
+ if (font == null) {
+ return;
+ }
+
+ setSpanFromMark(text, font, new EnrichedColoredSpan(style, font.color));
+ }
+
+ private static int parseCssColor(String css) {
+ if (css == null) return Color.BLACK;
+
+ css = css.trim();
+
+ try {
+ return Color.parseColor(css);
+ } catch (Exception ignore) {
+ }
+
+ if (css.startsWith("rgb(")) {
+ String[] parts = css.substring(4, css.length() - 1).split(",");
+ int r = Integer.parseInt(parts[0].trim());
+ int g = Integer.parseInt(parts[1].trim());
+ int b = Integer.parseInt(parts[2].trim());
+ return Color.rgb(r, g, b);
+ }
+
+ if (css.startsWith("rgba(")) {
+ String[] parts = css.substring(5, css.length() - 1).split(",");
+ int r = Integer.parseInt(parts[0].trim());
+ int g = Integer.parseInt(parts[1].trim());
+ int b = Integer.parseInt(parts[2].trim());
+ float a = Float.parseFloat(parts[3].trim());
+ return Color.argb((int) (a * 255), r, g, b);
+ }
+
+ return Color.BLACK;
+ }
}
diff --git a/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt b/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt
index 4d36459a4..f77771c4b 100644
--- a/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt
+++ b/android/src/main/java/com/swmansion/enriched/utils/EnrichedSelection.kt
@@ -8,6 +8,7 @@ import com.swmansion.enriched.EnrichedTextInputView
import com.swmansion.enriched.events.OnChangeSelectionEvent
import com.swmansion.enriched.events.OnLinkDetectedEvent
import com.swmansion.enriched.events.OnMentionDetectedEvent
+import com.swmansion.enriched.spans.EnrichedColoredSpan
import com.swmansion.enriched.spans.EnrichedLinkSpan
import com.swmansion.enriched.spans.EnrichedMentionSpan
import com.swmansion.enriched.spans.EnrichedSpans
@@ -125,6 +126,9 @@ class EnrichedSelection(
if (start == end && start == spanStart) {
styleStart = null
} else if (start >= spanStart && end <= spanEnd) {
+ if (span is EnrichedColoredSpan) {
+ view.spanState?.setTypingColor(span.color)
+ }
styleStart = spanStart
}
}
diff --git a/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt b/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt
index ef72537b0..ceac5f9ef 100644
--- a/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt
+++ b/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt
@@ -8,6 +8,7 @@ import com.facebook.react.uimanager.events.EventDispatcher
import com.swmansion.enriched.EnrichedTextInputView
import com.swmansion.enriched.events.OnChangeStateDeprecatedEvent
import com.swmansion.enriched.events.OnChangeStateEvent
+import com.swmansion.enriched.events.OnColorChangeEvent
import com.swmansion.enriched.spans.EnrichedSpans
class EnrichedSpanState(
@@ -15,6 +16,7 @@ class EnrichedSpanState(
) {
private var previousPayload: WritableMap? = null
private var previousDeprecatedPayload: WritableMap? = null
+ private var previousDispatchedColor: Int? = null
var boldStart: Int? = null
private set
@@ -52,6 +54,33 @@ class EnrichedSpanState(
private set
var mentionStart: Int? = null
private set
+ var colorStart: Int? = null
+ private set
+ var typingColor: Int? = null
+ private set
+
+ fun setTypingColor(color: Int?) {
+ typingColor = color
+ emitColorChangeEvent(color)
+ }
+
+ fun setColorStart(start: Int?) {
+ if (start == null) {
+ setColorStart(null, null)
+ } else {
+ setColorStart(start, typingColor)
+ }
+ }
+
+ fun setColorStart(
+ start: Int?,
+ color: Int?,
+ ) {
+ colorStart = start
+ typingColor = null
+ emitStateChangeEvent()
+ setTypingColor(color)
+ }
fun setBoldStart(start: Int?) {
this.boldStart = start
@@ -147,6 +176,7 @@ class EnrichedSpanState(
val start =
when (name) {
EnrichedSpans.BOLD -> boldStart
+ EnrichedSpans.COLOR -> colorStart
EnrichedSpans.ITALIC -> italicStart
EnrichedSpans.UNDERLINE -> underlineStart
EnrichedSpans.STRIKETHROUGH -> strikethroughStart
@@ -176,6 +206,7 @@ class EnrichedSpanState(
) {
when (name) {
EnrichedSpans.BOLD -> setBoldStart(start)
+ EnrichedSpans.COLOR -> setColorStart(start)
EnrichedSpans.ITALIC -> setItalicStart(start)
EnrichedSpans.UNDERLINE -> setUnderlineStart(start)
EnrichedSpans.STRIKETHROUGH -> setStrikethroughStart(start)
@@ -196,6 +227,31 @@ class EnrichedSpanState(
}
}
+ private fun emitColorChangeEvent(color: Int?) {
+ val resolvedColor = color ?: view.currentTextColor
+
+ if (previousDispatchedColor == resolvedColor) {
+ return
+ }
+
+ previousDispatchedColor = resolvedColor
+
+ val colorToDispatch = String.format("#%06X", resolvedColor and 0x00FFFFFF)
+
+ val context = view.context as ReactContext
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
+
+ dispatcher?.dispatchEvent(
+ OnColorChangeEvent(
+ surfaceId,
+ view.id,
+ view.experimentalSynchronousEvents,
+ colorToDispatch,
+ ),
+ )
+ }
+
private fun emitStateChangeEvent() {
val context = view.context as ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(context)
@@ -292,6 +348,7 @@ class EnrichedSpanState(
payload.putMap("link", getStyleState(activeStyles, EnrichedSpans.LINK))
payload.putMap("image", getStyleState(activeStyles, EnrichedSpans.IMAGE))
payload.putMap("mention", getStyleState(activeStyles, EnrichedSpans.MENTION))
+ payload.putMap("colored", getStyleState(activeStyles, EnrichedSpans.COLOR))
// Do not emit event if payload is the same
if (previousPayload == payload) {
diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx
index 1211831eb..3702c5a2f 100644
--- a/apps/example/src/App.tsx
+++ b/apps/example/src/App.tsx
@@ -9,6 +9,7 @@ import {
type OnChangeStateEvent,
type OnChangeSelectionEvent,
type HtmlStyle,
+ type OnChangeColorEvent,
} from 'react-native-enriched';
import { useRef, useState } from 'react';
import { Button } from './components/Button';
@@ -26,7 +27,6 @@ import {
DEFAULT_IMAGE_WIDTH,
prepareImageDimensions,
} from './utils/prepareImageDimensions';
-import type { OnChangeColorEvent } from '../../src/EnrichedTextInputNativeComponent';
import { ColorPreview } from './components/ColorPreview';
type StylesState = OnChangeStateEvent;
@@ -297,12 +297,8 @@ export default function App() {
setSelection(sel);
};
- const handleSelectionColorChange = (
- e: NativeSyntheticEvent
- ) => {
- if (e.nativeEvent.color) {
- setSelectionColor(e.nativeEvent.color);
- }
+ const handleSelectionColorChange = (e: OnChangeColorEvent) => {
+ setSelectionColor(e.color);
};
const handleRemoveColor = () => {
@@ -331,7 +327,9 @@ export default function App() {
onChangeText={(e) => handleChangeText(e.nativeEvent)}
onChangeHtml={(e) => handleChangeHtml(e.nativeEvent)}
onChangeState={(e) => handleChangeState(e.nativeEvent)}
- onColorChangeInSelection={handleSelectionColorChange}
+ onColorChangeInSelection={(e) =>
+ handleSelectionColorChange(e.nativeEvent)
+ }
onLinkDetected={handleLinkDetected}
onMentionDetected={console.log}
onStartMention={handleStartMention}
diff --git a/src/EnrichedTextInputNativeComponent.ts b/src/EnrichedTextInputNativeComponent.ts
index 3d51aad45..74e1a035d 100644
--- a/src/EnrichedTextInputNativeComponent.ts
+++ b/src/EnrichedTextInputNativeComponent.ts
@@ -193,7 +193,7 @@ type Heading = {
};
export interface OnChangeColorEvent {
- color: string | null;
+ color: string;
}
export interface HtmlStyleInternal {
From 66e09a2aaf57c0bb73a5b1e750e88d6accc0abd3 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Tue, 13 Jan 2026 01:56:06 +0100
Subject: [PATCH 11/12] fix: rebase with latest main
---
.../java/com/swmansion/enriched/utils/EnrichedSpanState.kt | 1 +
apps/example/src/App.tsx | 3 +++
2 files changed, 4 insertions(+)
diff --git a/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt b/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt
index ceac5f9ef..6cc7374ab 100644
--- a/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt
+++ b/android/src/main/java/com/swmansion/enriched/utils/EnrichedSpanState.kt
@@ -311,6 +311,7 @@ class EnrichedSpanState(
val activeStyles =
listOfNotNull(
if (boldStart != null) EnrichedSpans.BOLD else null,
+ if (colorStart != null) EnrichedSpans.COLOR else null,
if (italicStart != null) EnrichedSpans.ITALIC else null,
if (underlineStart != null) EnrichedSpans.UNDERLINE else null,
if (strikethroughStart != null) EnrichedSpans.STRIKETHROUGH else null,
diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx
index 3702c5a2f..60751a391 100644
--- a/apps/example/src/App.tsx
+++ b/apps/example/src/App.tsx
@@ -67,6 +67,8 @@ const DEFAULT_STYLES: StylesState = {
mention: DEFAULT_STYLE_STATE,
};
+const PRIMARY_COLOR = '#000000';
+
const DEFAULT_LINK_STATE = {
text: '',
url: '',
@@ -514,6 +516,7 @@ const styles = StyleSheet.create({
fontFamily: 'Nunito-Regular',
paddingVertical: 12,
paddingHorizontal: 14,
+ color: PRIMARY_COLOR,
},
scrollPlaceholder: {
marginTop: 24,
From 8f55ffe4a0679a59aa29ce8aba8181da5b46af98 Mon Sep 17 00:00:00 2001
From: IvanIhnatsiuk
Date: Tue, 13 Jan 2026 11:38:10 +0100
Subject: [PATCH 12/12] fix: update onChangeStateDeprecated
---
ios/EnrichedTextInputView.mm | 50 +++++++++++++++---------------------
1 file changed, 21 insertions(+), 29 deletions(-)
diff --git a/ios/EnrichedTextInputView.mm b/ios/EnrichedTextInputView.mm
index 1ac33b609..8d991bf4c 100644
--- a/ios/EnrichedTextInputView.mm
+++ b/ios/EnrichedTextInputView.mm
@@ -1034,35 +1034,27 @@ - (void)tryUpdatingActiveStyles {
_blockedStyles = newBlockedStyles;
emitter->onChangeStateDeprecated({
- .isBold = [_activeStyles containsObject:@([BoldStyle getStyleType])],
- .isItalic =
- [_activeStyles containsObject:@([ItalicStyle getStyleType])],
- .isUnderline =
- [_activeStyles containsObject:@([UnderlineStyle getStyleType])],
- .isStrikeThrough =
- [_activeStyles containsObject:@([StrikethroughStyle getStyleType])],
- .isColored =
- [_activeStyles containsObject:@([ColorStyle getStyleType])],
- .isInlineCode =
- [_activeStyles containsObject:@([InlineCodeStyle getStyleType])],
- .isLink = [_activeStyles containsObject:@([LinkStyle getStyleType])],
- .isMention =
- [_activeStyles containsObject:@([MentionStyle getStyleType])],
- .isH1 = [_activeStyles containsObject:@([H1Style getStyleType])],
- .isH2 = [_activeStyles containsObject:@([H2Style getStyleType])],
- .isH3 = [_activeStyles containsObject:@([H3Style getStyleType])],
- .isH4 = [_activeStyles containsObject:@([H4Style getStyleType])],
- .isH5 = [_activeStyles containsObject:@([H5Style getStyleType])],
- .isH6 = [_activeStyles containsObject:@([H6Style getStyleType])],
- .isUnorderedList =
- [_activeStyles containsObject:@([UnorderedListStyle getStyleType])],
- .isOrderedList =
- [_activeStyles containsObject:@([OrderedListStyle getStyleType])],
- .isBlockQuote =
- [_activeStyles containsObject:@([BlockQuoteStyle getStyleType])],
- .isCodeBlock =
- [_activeStyles containsObject:@([CodeBlockStyle getStyleType])],
- .isImage = [_activeStyles containsObject:@([ImageStyle getStyleType])],
+ .isBold = [self isStyleActive:[BoldStyle getStyleType]],
+ .isItalic = [self isStyleActive:[ItalicStyle getStyleType]],
+ .isColored = [self isStyleActive:[ColorStyle getStyleType]],
+ .isUnderline = [self isStyleActive:[UnderlineStyle getStyleType]],
+ .isStrikeThrough =
+ [self isStyleActive:[StrikethroughStyle getStyleType]],
+ .isInlineCode = [self isStyleActive:[InlineCodeStyle getStyleType]],
+ .isLink = [self isStyleActive:[LinkStyle getStyleType]],
+ .isMention = [self isStyleActive:[MentionStyle getStyleType]],
+ .isH1 = [self isStyleActive:[H1Style getStyleType]],
+ .isH2 = [self isStyleActive:[H2Style getStyleType]],
+ .isH3 = [self isStyleActive:[H3Style getStyleType]],
+ .isH4 = [self isStyleActive:[H4Style getStyleType]],
+ .isH5 = [self isStyleActive:[H5Style getStyleType]],
+ .isH6 = [self isStyleActive:[H6Style getStyleType]],
+ .isUnorderedList =
+ [self isStyleActive:[UnorderedListStyle getStyleType]],
+ .isOrderedList = [self isStyleActive:[OrderedListStyle getStyleType]],
+ .isBlockQuote = [self isStyleActive:[BlockQuoteStyle getStyleType]],
+ .isCodeBlock = [self isStyleActive:[CodeBlockStyle getStyleType]],
+ .isImage = [self isStyleActive:[ImageStyle getStyleType]],
});
emitter->onChangeState(
{.bold = GET_STYLE_STATE([BoldStyle getStyleType]),