Skip to content

Commit 7b32d2c

Browse files
committed
feat: add foreground color style for iOS
1 parent 571fa10 commit 7b32d2c

15 files changed

Lines changed: 743 additions & 12 deletions

android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
251251
view?.verifyAndToggleStyle(EnrichedSpans.UNORDERED_LIST)
252252
}
253253

254+
override fun toggleColor(view: EnrichedTextInputView?, color: String) {
255+
// no-op for now
256+
}
257+
254258
override fun addLink(view: EnrichedTextInputView?, start: Int, end: Int, text: String, url: String) {
255259
view?.addLink(start, end, text, url)
256260
}

example/src/App.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { type MentionItem, MentionPopup } from './components/MentionPopup';
2626
import { useUserMention } from './useUserMention';
2727
import { useChannelMention } from './useChannelMention';
2828
import { HtmlSection } from './components/HtmlSection';
29+
import type { OnChangeColorEvent } from '../../src/EnrichedTextInputNativeComponent';
30+
import ColorPreview from './components/ColorPreview';
2931

3032
type StylesState = OnChangeStateEvent;
3133

@@ -37,6 +39,8 @@ interface Selection {
3739
text: string;
3840
}
3941

42+
const PRIMARY_COLOR = '#000000';
43+
4044
const DEFAULT_STYLE: StylesState = {
4145
isBold: false,
4246
isItalic: false,
@@ -53,6 +57,7 @@ const DEFAULT_STYLE: StylesState = {
5357
isLink: false,
5458
isImage: false,
5559
isMention: false,
60+
isColored: false,
5661
};
5762

5863
const DEFAULT_LINK_STATE = {
@@ -81,6 +86,7 @@ export default function App() {
8186
const [stylesState, setStylesState] = useState<StylesState>(DEFAULT_STYLE);
8287
const [currentLink, setCurrentLink] =
8388
useState<CurrentLinkState>(DEFAULT_LINK_STATE);
89+
const [selectionColor, setSelectionColor] = useState<string>(PRIMARY_COLOR);
8490

8591
const ref = useRef<EnrichedTextInputInstance>(null);
8692

@@ -252,6 +258,14 @@ export default function App() {
252258
setSelection(e.nativeEvent);
253259
};
254260

261+
const handleSelectionColorChange = (
262+
e: NativeSyntheticEvent<OnChangeColorEvent>
263+
) => {
264+
if (e.nativeEvent.color) {
265+
setSelectionColor(e.nativeEvent.color);
266+
}
267+
};
268+
255269
return (
256270
<>
257271
<ScrollView
@@ -270,6 +284,7 @@ export default function App() {
270284
selectionColor="deepskyblue"
271285
cursorColor="dodgerblue"
272286
autoCapitalize="sentences"
287+
onColorChangeInSelection={handleSelectionColorChange}
273288
onChangeText={handleChangeText}
274289
onChangeHtml={handleChangeHtml}
275290
onChangeState={handleChangeState}
@@ -288,6 +303,7 @@ export default function App() {
288303
<Toolbar
289304
stylesState={stylesState}
290305
editorRef={ref}
306+
selectionColor={selectionColor}
291307
onOpenLinkModal={openLinkModal}
292308
onSelectImage={selectImage}
293309
/>
@@ -303,6 +319,7 @@ export default function App() {
303319
/>
304320
<HtmlSection currentHtml={currentHtml} />
305321
{DEBUG_SCROLLABLE && <View style={styles.scrollPlaceholder} />}
322+
<ColorPreview color={selectionColor} />
306323
</ScrollView>
307324
<LinkModal
308325
isOpen={isLinkModalOpen}
@@ -363,7 +380,7 @@ const htmlStyle: HtmlStyle = {
363380
backgroundColor: 'yellow',
364381
},
365382
a: {
366-
color: 'green',
383+
color: 'blue',
367384
textDecorationLine: 'underline',
368385
},
369386
mention: {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { StyleSheet, Text, View } from 'react-native';
2+
3+
type Props = {
4+
color: string;
5+
};
6+
7+
const ColorPreview: React.FC<Props> = ({ color }) => {
8+
return (
9+
<>
10+
<View
11+
style={[
12+
styles.preview,
13+
{
14+
backgroundColor: color,
15+
},
16+
]}
17+
/>
18+
<Text>{color}</Text>
19+
</>
20+
);
21+
};
22+
23+
const styles = StyleSheet.create({
24+
preview: {
25+
marginVertical: 8,
26+
width: 40,
27+
height: 40,
28+
borderRadius: 20,
29+
},
30+
});
31+
32+
export default ColorPreview;

example/src/components/Toolbar.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
EnrichedTextInputInstance,
66
} from 'react-native-enriched';
77
import type { FC } from 'react';
8+
import { ToolbarColorButton } from './ToolbarColorButton';
89

910
const STYLE_ITEMS = [
1011
{
@@ -67,6 +68,16 @@ const STYLE_ITEMS = [
6768
name: 'ordered-list',
6869
icon: 'list-ol',
6970
},
71+
{
72+
name: 'color',
73+
value: '#FF0000',
74+
text: 'A',
75+
},
76+
{
77+
name: 'color',
78+
value: '#E6FF5C',
79+
text: 'A',
80+
},
7081
] as const;
7182

7283
type Item = (typeof STYLE_ITEMS)[number];
@@ -77,13 +88,15 @@ export interface ToolbarProps {
7788
editorRef?: React.RefObject<EnrichedTextInputInstance | null>;
7889
onOpenLinkModal: () => void;
7990
onSelectImage: () => void;
91+
selectionColor: string | null;
8092
}
8193

8294
export const Toolbar: FC<ToolbarProps> = ({
8395
stylesState,
8496
editorRef,
8597
onOpenLinkModal,
8698
onSelectImage,
99+
selectionColor,
87100
}) => {
88101
const handlePress = (item: Item) => {
89102
const currentRef = editorRef?.current;
@@ -138,6 +151,10 @@ export const Toolbar: FC<ToolbarProps> = ({
138151
}
139152
};
140153

154+
const handleColorButtonPress = (color: string) => {
155+
editorRef?.current?.toggleColor(color);
156+
};
157+
141158
const isActive = (item: Item) => {
142159
switch (item.name) {
143160
case 'bold':
@@ -176,7 +193,14 @@ export const Toolbar: FC<ToolbarProps> = ({
176193
};
177194

178195
const renderItem = ({ item }: ListRenderItemInfo<Item>) => {
179-
return (
196+
return item.name === 'color' ? (
197+
<ToolbarColorButton
198+
onPress={handleColorButtonPress}
199+
color={item.value}
200+
text={item.text}
201+
isActive={stylesState.isColored && selectionColor === item.value}
202+
/>
203+
) : (
180204
<ToolbarButton
181205
{...item}
182206
isActive={isActive(item)}
@@ -185,7 +209,8 @@ export const Toolbar: FC<ToolbarProps> = ({
185209
);
186210
};
187211

188-
const keyExtractor = (item: Item) => item.name;
212+
const keyExtractor = (item: Item) =>
213+
item.name === 'color' ? item.value : item.name;
189214

190215
return (
191216
<FlatList
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useMemo, type FC } from 'react';
2+
import { Pressable, StyleSheet, Text } from 'react-native';
3+
4+
interface ColorButtonProps {
5+
text: string;
6+
isActive: boolean;
7+
onPress: (color: string) => void;
8+
color: string;
9+
}
10+
11+
export const ToolbarColorButton: FC<ColorButtonProps> = ({
12+
text,
13+
isActive,
14+
onPress,
15+
color,
16+
}) => {
17+
const handlePress = () => {
18+
onPress(color);
19+
};
20+
21+
const containerStyle = useMemo(
22+
() => [
23+
styles.container,
24+
{ backgroundColor: isActive ? color : 'rgba(0, 26, 114, 0.8)' },
25+
],
26+
[isActive, color]
27+
);
28+
29+
return (
30+
<Pressable style={containerStyle} onPress={handlePress}>
31+
<Text style={[styles.text, !isActive && { color }]}>{text}</Text>
32+
</Pressable>
33+
);
34+
};
35+
36+
const styles = StyleSheet.create({
37+
container: {
38+
justifyContent: 'center',
39+
alignItems: 'center',
40+
width: 56,
41+
height: 56,
42+
},
43+
text: {
44+
color: 'white',
45+
fontSize: 20,
46+
},
47+
});

ios/EnrichedTextInputView.mm

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#import "LayoutManagerExtension.h"
1616
#import "ZeroWidthSpaceUtils.h"
1717
#import "ParagraphAttributesUtils.h"
18+
#import "ColorExtension.h"
1819

1920
using namespace facebook::react;
2021

@@ -33,6 +34,7 @@ @implementation EnrichedTextInputView {
3334
NSRange _recentlyActiveMentionRange;
3435
NSString *_recentlyEmittedHtml;
3536
BOOL _emitHtml;
37+
NSString *_recentlyEmittedColor;
3638
UILabel *_placeholderLabel;
3739
UIColor *_placeholderColor;
3840
BOOL _emitFocusBlur;
@@ -75,6 +77,7 @@ - (void)setDefaults {
7577
_recentlyEmittedString = @"";
7678
_recentlyEmittedHtml = @"<html>\n<p></p>\n</html>";
7779
_emitHtml = NO;
80+
_recentlyEmittedColor = nil;
7881
blockEmitting = NO;
7982
_emitFocusBlur = YES;
8083

@@ -85,6 +88,7 @@ - (void)setDefaults {
8588
@([ItalicStyle getStyleType]): [[ItalicStyle alloc] initWithInput:self],
8689
@([UnderlineStyle getStyleType]): [[UnderlineStyle alloc] initWithInput:self],
8790
@([StrikethroughStyle getStyleType]): [[StrikethroughStyle alloc] initWithInput:self],
91+
@([ColorStyle getStyleType]): [[ColorStyle alloc] initWithInput:self],
8892
@([InlineCodeStyle getStyleType]): [[InlineCodeStyle alloc] initWithInput:self],
8993
@([LinkStyle getStyleType]): [[LinkStyle alloc] initWithInput:self],
9094
@([MentionStyle getStyleType]): [[MentionStyle alloc] initWithInput:self],
@@ -102,6 +106,7 @@ - (void)setDefaults {
102106
@([ItalicStyle getStyleType]) : @[],
103107
@([UnderlineStyle getStyleType]) : @[],
104108
@([StrikethroughStyle getStyleType]) : @[],
109+
@([ColorStyle getStyleType]): @[],
105110
@([InlineCodeStyle getStyleType]) : @[@([LinkStyle getStyleType]), @([MentionStyle getStyleType])],
106111
@([LinkStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType]), @([MentionStyle getStyleType])],
107112
@([MentionStyle getStyleType]): @[@([InlineCodeStyle getStyleType]), @([LinkStyle getStyleType])],
@@ -120,6 +125,7 @@ - (void)setDefaults {
120125
@([ItalicStyle getStyleType]) : @[@([CodeBlockStyle getStyleType])],
121126
@([UnderlineStyle getStyleType]) : @[@([CodeBlockStyle getStyleType])],
122127
@([StrikethroughStyle getStyleType]) : @[@([CodeBlockStyle getStyleType])],
128+
@([ColorStyle getStyleType]): @[],
123129
@([InlineCodeStyle getStyleType]) : @[@([CodeBlockStyle getStyleType])],
124130
@([LinkStyle getStyleType]): @[@([CodeBlockStyle getStyleType])],
125131
@([MentionStyle getStyleType]): @[@([CodeBlockStyle getStyleType])],
@@ -713,6 +719,7 @@ - (void)tryUpdatingActiveStyles {
713719
.isItalic = [_activeStyles containsObject: @([ItalicStyle getStyleType])],
714720
.isUnderline = [_activeStyles containsObject: @([UnderlineStyle getStyleType])],
715721
.isStrikeThrough = [_activeStyles containsObject: @([StrikethroughStyle getStyleType])],
722+
.isColored = [_activeStyles containsObject: @([ColorStyle getStyleType])],
716723
.isInlineCode = [_activeStyles containsObject: @([InlineCodeStyle getStyleType])],
717724
.isLink = [_activeStyles containsObject: @([LinkStyle getStyleType])],
718725
.isMention = [_activeStyles containsObject: @([MentionStyle getStyleType])],
@@ -740,7 +747,7 @@ - (void)tryUpdatingActiveStyles {
740747
_recentlyActiveMentionParams = detectedMentionParams;
741748
_recentlyActiveMentionRange = detectedMentionRange;
742749
}
743-
750+
[self emitCurrentSelectionColorIfChanged];
744751
// emit onChangeHtml event if needed
745752
[self tryEmittingOnChangeHtmlEvent];
746753
}
@@ -763,6 +770,10 @@ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args {
763770
[self toggleRegularStyle: [UnderlineStyle getStyleType]];
764771
} else if([commandName isEqualToString:@"toggleStrikeThrough"]) {
765772
[self toggleRegularStyle: [StrikethroughStyle getStyleType]];
773+
} else if([commandName isEqualToString:@"toggleColor"]) {
774+
NSString *colorText = (NSString *)args[0];
775+
UIColor *color = [UIColor colorFromString: colorText];
776+
[self toggleColorStyle: color];
766777
} else if([commandName isEqualToString:@"toggleInlineCode"]) {
767778
[self toggleRegularStyle: [InlineCodeStyle getStyleType]];
768779
} else if([commandName isEqualToString:@"addLink"]) {
@@ -847,6 +858,49 @@ - (void)emitOnLinkDetectedEvent:(NSString *)text url:(NSString *)url range:(NSRa
847858
}
848859
}
849860

861+
- (void)emitCurrentSelectionColorIfChanged {
862+
NSRange selRange = textView.selectedRange;
863+
UIColor *uniformColor = nil;
864+
865+
if (selRange.length == 0) {
866+
id colorAttr = textView.typingAttributes[NSForegroundColorAttributeName];
867+
uniformColor = colorAttr ? (UIColor *)colorAttr : [config primaryColor];
868+
} else {
869+
// Selection range: check for uniform color
870+
__block UIColor *firstColor = nil;
871+
__block BOOL hasMultiple = NO;
872+
873+
[textView.textStorage enumerateAttribute:NSForegroundColorAttributeName
874+
inRange:selRange
875+
options:0
876+
usingBlock:^(id _Nullable value, NSRange range, BOOL *_Nonnull stop) {
877+
UIColor *thisColor = value ? (UIColor *)value : [config primaryColor];
878+
if (firstColor == nil) {
879+
firstColor = thisColor;
880+
} else if (![firstColor isEqual:thisColor]) {
881+
hasMultiple = YES;
882+
*stop = YES;
883+
}
884+
}];
885+
886+
if (!hasMultiple && firstColor != nil) {
887+
uniformColor = firstColor;
888+
}
889+
}
890+
891+
NSString *hexColor = uniformColor ? [uniformColor hexString] : [config.primaryColor hexString];
892+
893+
if(![_recentlyEmittedColor isEqual: hexColor]) {
894+
auto emitter = [self getEventEmitter];
895+
if(emitter != nullptr) {
896+
emitter->onColorChangeInSelection({
897+
.color = [hexColor toCppString]
898+
});
899+
}
900+
_recentlyEmittedColor = hexColor;
901+
}
902+
}
903+
850904
- (void)emitOnMentionDetectedEvent:(NSString *)text indicator:(NSString *)indicator attributes:(NSString *)attributes {
851905
auto emitter = [self getEventEmitter];
852906
if(emitter != nullptr) {
@@ -896,6 +950,13 @@ - (void)tryEmittingOnChangeHtmlEvent {
896950

897951
// MARK: - Styles manipulation
898952

953+
- (void)toggleColorStyle:(UIColor *)color {
954+
ColorStyle *colorStyle = stylesDict[@(Colored)];
955+
956+
[colorStyle applyStyle:textView.selectedRange color: color];
957+
[self anyTextMayHaveBeenModified];
958+
}
959+
899960
- (void)toggleRegularStyle:(StyleType)type {
900961
id<BaseStyleProtocol> styleClass = stylesDict[@(type)];
901962

@@ -1181,6 +1242,7 @@ - (void)textViewDidBeginEditing:(UITextView *)textView {
11811242
.end = static_cast<int>(textView.selectedRange.location + textView.selectedRange.length),
11821243
.text = [textAtSelection toCppString]
11831244
});
1245+
[self emitCurrentSelectionColorIfChanged];
11841246
}
11851247
// manage selection changes since textViewDidChangeSelection sometimes doesn't run on focus
11861248
[self manageSelectionBasedChanges];

0 commit comments

Comments
 (0)