enriched-text-input 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ # API Reference
package/README.md CHANGED
@@ -14,21 +14,6 @@ The field for rich-text in react native is still a bit green. Current libraries
14
14
 
15
15
  In theory, by only using JavaScript we are able to provide better cross-platform compatibility and the possibility to style elements however you want as long as they follow react-native's `Text` supported styles.
16
16
 
17
- ## Features
18
-
19
- - [x] Basic text formatting (__bold__, _italic_, underline, ~~strikethrough~~ and `codeblocks`).
20
- - [x] Rich text format parsing.
21
- - [ ] Links and mentions.
22
- - [ ] Custom styling.
23
- - [ ] Custom rich text patterns.
24
- - [ ] Exposed event handlers (onSubmit, onChange, onBlur, onFocus, etc).
25
- - [ ] Custom methods and event handlers (setValue, onStartMention, onStyleChange, etc).
26
- - [ ] Headings.
27
-
28
- ## Known limitations
29
- - Inline images.
30
- - Only `Text`component styles are supported.
31
-
32
17
  ## Installation
33
18
  ```
34
19
  npm install enriched-text-input
@@ -66,9 +51,27 @@ const styles = StyleSheet.create({
66
51
  paddingTop: 120
67
52
  },
68
53
  });
54
+ ```
69
55
 
56
+ ## Current state
57
+ At the moment [1/1/2026] `enriched-text-input` works great for things such as small rich-text inputs (Eg. an input for a messaging app with rich-text support) but not for creating whole rich-text editors. This is because inline styles that do not break line are working as expected (Eg. bold, italic or underline work great but styles such as headings break line so they are currently not supported).
70
58
 
71
- ```
59
+ Live parsing of rich text symbols (such as wrapping words in asterisks `*`) is still a work in progress an not working correctly but you can toggle styles through the ref api of the `EnrichedTextInput` (or use the provided `Toolbar` component as shown in the example usage).
60
+
61
+ ## Features
62
+
63
+ - [x] Basic text formatting (__bold__, _italic_, underline, ~~strikethrough~~ and `inline code`).
64
+ - [ ] Rich text format parsing.
65
+ - [ ] Links and mentions.
66
+ - [x] Custom styling.
67
+ - [x] Custom rich text patterns.
68
+ - [ ] Exposed event handlers (onSubmit, onChange, onBlur, onFocus, etc).
69
+ - [ ] Custom methods and event handlers (setValue, onStartMention, onStyleChange, etc).
70
+ - [ ] Headings.
71
+
72
+ ## Known limitations
73
+ - Inline images.
74
+ - Only `Text`component styles are supported.
72
75
 
73
76
  ## Contributing
74
77
 
package/example/App.tsx CHANGED
@@ -1,23 +1,100 @@
1
- import { useRef } from 'react';
2
- import { StyleSheet, View } from 'react-native';
1
+ import { useRef, useState } from 'react';
2
+ import { StyleSheet, View, KeyboardAvoidingView, Text, TouchableOpacity, Button, TextInput } from 'react-native';
3
+ import { FontAwesome6 } from '@expo/vector-icons';
4
+ import { RichTextInput, Toolbar, PATTERNS } from 'enriched-text-input';
5
+ import * as Clipboard from 'expo-clipboard';
3
6
 
4
- import { RichTextInput, Toolbar } from 'enriched-text-input';
7
+ function Comment({ children }) {
8
+ return (
9
+ <Text style={{
10
+ backgroundColor: "rgba(255, 203, 0, .12)",
11
+ textDecorationLine: "underline",
12
+ textDecorationColor: "rgba(255, 203, 0, .35)",
13
+ }}>
14
+ {children}
15
+ </Text>
16
+ )
17
+ }
5
18
 
6
19
  export default function App() {
20
+ const [rawValue, setRawValue] = useState("");
21
+ const [richTextStringValue, setRichTextStringValue] = useState("");
22
+ const [activeStyles, setActiveStyles] = useState([]);
23
+ console.log("ACTIVE STYLES:", activeStyles);
7
24
  const richTextInputRef = useRef(null);
8
25
 
26
+ const customPatterns = [
27
+ ...PATTERNS,
28
+ { style: "comment", regex: null, render: Comment }
29
+ ];
30
+
31
+ const handleComment = () => {
32
+ richTextInputRef.current?.toggleStyle("comment");
33
+ }
34
+
35
+ const handleGetRichText = () => {
36
+ const richText = richTextInputRef.current?.getRichTextString();
37
+
38
+ setRichTextStringValue(richText);
39
+ }
40
+
41
+ const handleCopyToClipboard = async (text: string) => {
42
+ await Clipboard.setStringAsync(text);
43
+ }
44
+
9
45
  return (
10
- <View style={styles.container}>
11
- <RichTextInput ref={richTextInputRef}/>
12
- <Toolbar richTextInputRef={richTextInputRef}>
13
- <Toolbar.Bold />
14
- <Toolbar.Italic />
15
- <Toolbar.Underline />
16
- <Toolbar.Strikethrough />
17
- <Toolbar.Code />
18
- <Toolbar.Keyboard />
19
- </Toolbar>
20
- </View>
46
+ <KeyboardAvoidingView style={styles.container} behavior="padding">
47
+ <View style={{ flex: 1 }}>
48
+ <TextInput
49
+ multiline
50
+ style={{ fontSize: 20, padding: 16 }}
51
+ value={rawValue}
52
+ onChangeText={(text) => setRawValue(text)}
53
+ placeholder='Raw text'
54
+ />
55
+ <Button
56
+ title='Set rich text string'
57
+ onPress={() => richTextInputRef.current?.setValue(rawValue)}
58
+ />
59
+
60
+ <RichTextInput
61
+ ref={richTextInputRef}
62
+ patterns={customPatterns}
63
+ autoComplete="off"
64
+ placeholder="Rich text"
65
+ multiline={true}
66
+ />
67
+ <Button
68
+ title='Get rich text string'
69
+ onPress={handleGetRichText}
70
+ />
71
+ <Text style={{ padding: 16, fontSize: 20, color: richTextStringValue ? "black" : "#b3b3b3" }}>{richTextStringValue ? richTextStringValue : "Get rich text output will appear here!"}</Text>
72
+
73
+ <View style={{ flexDirection: "row", justifyContent: "center"}}>
74
+ <Button
75
+ title='Clear'
76
+ onPress={() => setRichTextStringValue("")}
77
+ />
78
+ <Button
79
+ title='Copy'
80
+ onPress={() => handleCopyToClipboard(richTextStringValue)}
81
+ />
82
+ </View>
83
+ </View>
84
+ <View style={{ alignSelf: "end"}}>
85
+ <Toolbar richTextInputRef={richTextInputRef}>
86
+ <Toolbar.Bold/>
87
+ <Toolbar.Italic />
88
+ <Toolbar.Underline />
89
+ <Toolbar.Strikethrough />
90
+ <Toolbar.Code />
91
+ {/* <TouchableOpacity style={styles.toolbarButton} onPress={handleComment}>
92
+ <FontAwesome6 name="comment-alt" size={16} color="black" />
93
+ </TouchableOpacity> */}
94
+ <Toolbar.Keyboard />
95
+ </Toolbar>
96
+ </View>
97
+ </KeyboardAvoidingView>
21
98
  );
22
99
  }
23
100
 
@@ -27,4 +104,11 @@ const styles = StyleSheet.create({
27
104
  backgroundColor: '#fff',
28
105
  paddingTop: 120
29
106
  },
107
+ toolbarButton: {
108
+ height: 50,
109
+ width: 50,
110
+ display: "flex",
111
+ justifyContent: "center",
112
+ alignItems: "center"
113
+ }
30
114
  });
@@ -10,11 +10,12 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@expo/vector-icons": "^15.0.3",
13
+ "enriched-text-input": "file:../",
13
14
  "expo": "~54.0.25",
15
+ "expo-clipboard": "~8.0.8",
14
16
  "expo-status-bar": "~3.0.8",
15
17
  "react": "19.1.0",
16
- "react-native": "0.81.5",
17
- "enriched-text-input": "file:../"
18
+ "react-native": "0.81.5"
18
19
  },
19
20
  "private": true,
20
21
  "devDependencies": {
package/index.ts CHANGED
@@ -1,5 +1,4 @@
1
- import RichTextInput from "./src/RichTextInput";
2
- import { Code } from "./src/RichTextInput";
1
+ import RichTextInput, { PATTERNS } from "./src/RichTextInput";
3
2
  import Toolbar from "./src/Toolbar";
4
3
 
5
- export { RichTextInput, Toolbar, Code };
4
+ export { RichTextInput, PATTERNS, Toolbar };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enriched-text-input",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "JavaScript only rich text input component for React Native. Compatible with Expo Go.",
5
5
  "keywords": [
6
6
  "rich-text",
@@ -28,7 +28,8 @@
28
28
  "example"
29
29
  ],
30
30
  "scripts": {
31
- "test": "echo \"Error: no test specified\" && exit 1"
31
+ "test": "echo \"Error: no test specified\" && exit 1",
32
+ "release": "release-it"
32
33
  },
33
34
  "dependencies": {
34
35
  "@expo/vector-icons": "^15.0.3",
@@ -37,6 +38,7 @@
37
38
  "react-native": "0.81.5"
38
39
  },
39
40
  "devDependencies": {
40
- "metro-react-native-babel-preset": "^0.77.0"
41
+ "metro-react-native-babel-preset": "^0.77.0",
42
+ "release-it": "^19.0.6"
41
43
  }
42
44
  }
@@ -1,5 +1,5 @@
1
- import { useState, useImperativeHandle, useRef, useEffect } from "react";
2
- import { TextInput, Text, StyleSheet, View, Linking } from "react-native";
1
+ import { useState, useImperativeHandle, useRef, useEffect, JSX } from "react";
2
+ import { TextInput, Text, StyleSheet, View, TextInputProps } from "react-native";
3
3
 
4
4
  interface Token {
5
5
  text: string;
@@ -13,11 +13,7 @@ interface Diff {
13
13
  }
14
14
 
15
15
  interface Annotations {
16
- bold: boolean;
17
- italic: boolean;
18
- lineThrough: boolean;
19
- underline: boolean;
20
- code: boolean;
16
+ [key: string]: boolean | string | null
21
17
  }
22
18
 
23
19
  interface RichTextMatch {
@@ -25,18 +21,39 @@ interface RichTextMatch {
25
21
  content: string;
26
22
  start: number;
27
23
  end: number;
24
+ pattern: Pattern;
25
+ /** @deprecated */
28
26
  expression: string;
29
27
  }
30
28
 
29
+ interface Pattern {
30
+ regex: string;
31
+ style: string;
32
+ render: any;
33
+ opening?: string;
34
+ closing?: string;
35
+ }
36
+
31
37
  interface RichTextInputProps {
32
- ref: any
38
+ ref: any;
39
+ patterns?: Pattern[]
33
40
  }
34
-
35
- const PATTERNS = [
36
- { style: "bold", regex: "\\*([^*]+)\\*", render: <Text style={{ fontWeight: "bold" }} /> },
37
- { style: "italic", regex: "_([^_]+)_", render: <Text style={{ fontStyle: "italic" }} /> },
38
- { style: "lineThrough", regex: "~([^~]+)~", render: <Text style={{ textDecorationLine: "line-through" }} /> },
39
- { style: "code", regex: "`([^`]+)`", render: <Text style={{ fontFamily: "ui-monospace", backgroundColor: "lightgray", color: "red", paddingHorizontal: 6 }} /> },
41
+
42
+ /**
43
+ * Note: maybe instead of using regex we could just define an "opening" and "closing" char.
44
+ * If both are defined we look for a match that looks like {opening}{content}{closing}.
45
+ * If just opening is defined, we look for a match that looks like {opening}{content}.
46
+ * Closing can not be defined if opening is not defined.
47
+ */
48
+ export const PATTERNS : Pattern[] = [
49
+ { style: "bold", regex: "\\*([^*]+)\\*", render: Bold, opening: "*", closing: "*" },
50
+ { style: "italic", regex: "_([^_]+)_", render: Italic, opening: "_", closing: "_" },
51
+ { style: "lineThrough", regex: "~([^~]+)~", render: Strikethrough, opening: "~", closing: "~" },
52
+ { style: "code", regex: "`([^`]+)`", render: Code, opening: "`", closing: "`" },
53
+ { style: "underline", regex: "__([^_]+)__", render: Underline, opening: "__", closing: "__" },
54
+ { style: "heading", regex: null, render: Heading, opening: "#", closing: null },
55
+ { style: "subHeading", regex: null, render: SubHeading, opening: "##", closing: null },
56
+ { style: "subSubHeading", regex: null, render: SubSubHeading, opening: "###", closing: null }
40
57
  ];
41
58
 
42
59
  function insertAt(str, index, substring) {
@@ -55,66 +72,88 @@ function replaceAt(str, index, substring, length) {
55
72
  return str.slice(0, i) + substring + str.slice(i + length);
56
73
  }
57
74
 
58
- function findMatch(str: string, regexExpression: string) : RichTextMatch | null {
59
- const regex = new RegExp(regexExpression);
60
- const match = regex.exec(str);
61
- return match
62
- ? {
63
- raw: match[0],
64
- content: match[1],
65
- start: match.index,
66
- end: match.index + match[0].length,
67
- expression: regexExpression
75
+ function findTokens(
76
+ /** The tokens to search over. */
77
+ tokens: Token[],
78
+ /** start position of selection.*/
79
+ start: number,
80
+ /** end position of selection.*/
81
+ end?: number
82
+ ) {
83
+
84
+ if (end) {
85
+ // Search for all tokens between start and end
86
+ return { result: null };
87
+ }
88
+
89
+ let startIndex = start;
90
+ let startToken;
91
+ for (const token of tokens) {
92
+ if (startIndex <= token.text.length) {
93
+ startToken = token;
94
+ break;
95
+ }
96
+ startIndex -= token.text.length;
68
97
  }
69
- : null;
70
- }
71
98
 
72
- function getRequiredLiterals(regexString) {
73
- // Strip leading/trailing slashes and flags (if user passed /.../ form)
74
- regexString = regexString.replace(/^\/|\/[a-z]*$/g, "");
99
+ const startTokenIndex = tokens.indexOf(startToken);
75
100
 
76
- // Remove ^ and $ anchors
77
- regexString = regexString.replace(/^\^|\$$/g, "");
101
+ return { result: [startToken] };
102
+ }
78
103
 
79
- // 1. Find the first literal before any group or operator
80
- const beforeGroup = regexString.match(/^((?:\\.|[^[(])+)/);
81
- let openLiteral = null;
104
+ /**
105
+ * To-do: Add support for openings and closings that are conformed by two or more chars (e.g. **, __, <b>, etc.)
106
+ */
107
+ function findMatchV2(str: string, patterns: Pattern[]) : RichTextMatch | null {
108
+ let match = null;
109
+ let copyOfString = str;
82
110
 
83
- if (beforeGroup) {
84
- const part = beforeGroup[1];
85
- const litMatch = part.match(/\\(.)|([^\\])/); // first literal
86
- if (litMatch) {
87
- openLiteral = litMatch[1] ?? litMatch[2];
88
- }
89
- }
111
+ for (const pattern of patterns) {
112
+ let evenCount = 0;
113
+
114
+ for (const char of copyOfString) {
115
+ /** Cases where both opening and closing chars are defined (*...*, _..._, etc.)*/
116
+ if (pattern.opening && pattern.closing) {
117
+ if (evenCount < 2 && char === pattern.opening) {
118
+ evenCount++;
119
+ }
120
+ if (evenCount === 2 && char === pattern.closing) {
121
+ const openingIndex = copyOfString.indexOf(pattern.opening);
122
+ const closingIndex = copyOfString.indexOf(pattern.closing, openingIndex + 1);
123
+
124
+ match = {
125
+ raw: copyOfString.slice(openingIndex, closingIndex + 1),
126
+ content: copyOfString.slice(openingIndex + 1, closingIndex),
127
+ start: openingIndex,
128
+ end: closingIndex,
129
+ pattern,
130
+ /** @deprecated */
131
+ expression: pattern.regex
132
+ };
133
+ break;
134
+ }
135
+ }
90
136
 
91
- // 2. Detect a closing literal after a capturing group (optional)
92
- let closeLiteral = null;
93
- const afterGroup = regexString.match(/\)([^).]+)/);
94
- if (afterGroup) {
95
- const part = afterGroup[1];
96
- const litMatch = part.match(/\\(.)|([^\\])/);
97
- if (litMatch) {
98
- closeLiteral = litMatch[1] ?? litMatch[2];
137
+ /** Cases where only opening char is defined (@, #, etc.) */
138
+ }
99
139
  }
100
- }
101
140
 
102
- // Return both if available, otherwise just the opening literal
103
- return {
104
- opening: openLiteral,
105
- closing: closeLiteral,
106
- };
141
+ return match;
107
142
  }
108
143
 
109
- function concileAnnotations(prevAnnotations, nextAnnotations) {
110
- return {
111
- bold: nextAnnotations.bold ? !prevAnnotations.bold : prevAnnotations.bold,
112
- italic: nextAnnotations.italic ? !prevAnnotations.italic : prevAnnotations.italic,
113
- lineThrough: nextAnnotations.lineThrough ? !prevAnnotations.lineThrough : prevAnnotations.lineThrough,
114
- underline: nextAnnotations.underline ? !prevAnnotations.underline : prevAnnotations.underline,
115
- code: nextAnnotations.code ? !prevAnnotations.code : prevAnnotations.code,
116
- /* color: nextAnnotations.color */
117
- };
144
+ /**
145
+ * If prev token contains new annotation, negate prev. Else, use new annotation.
146
+ */
147
+ function concileAnnotations(prevAnnotations, newAnnotations) {
148
+ let updatedAnnotations = { ...prevAnnotations };
149
+
150
+ for (const key of Object.keys(newAnnotations)) {
151
+ newAnnotations[key]
152
+ ? updatedAnnotations[key] = !updatedAnnotations[key]
153
+ : updatedAnnotations[key] = newAnnotations[key];
154
+ }
155
+
156
+ return updatedAnnotations;
118
157
  }
119
158
 
120
159
  // Returns string modifications
@@ -140,58 +179,73 @@ function diffStrings(prev, next) : Diff {
140
179
  };
141
180
  }
142
181
 
143
- // Returns an array of tokens
144
- const parseRichTextString = (richTextString: string, patterns: { regex: string, style: string }[], initalTokens = null) => {
145
- let tokens = initalTokens || [
182
+ /**
183
+ * Parse rich text string into tokens.
184
+ */
185
+ const parseRichTextString = (richTextString: string, patterns: Pattern[], initialTokens?: Token[])
186
+ : { tokens: Token[], plain_text: string } => {
187
+ let copyOfString = richTextString;
188
+ let tokens : Token[] = initialTokens || [
146
189
  {
147
- text: richTextString,
148
- annotations: {
149
- bold: false,
150
- italic: false,
151
- lineThrough: false,
152
- underline: false,
153
- code: false
154
- }
190
+ text: copyOfString,
191
+ annotations: {}
155
192
  }
156
193
  ];
157
- let plain_text = tokens.reduce((acc, curr) => acc + curr.text, "");
158
194
 
159
195
  for (const pattern of patterns) {
160
- let match = findMatch(plain_text, pattern.regex);
161
-
162
- if (match) {
163
- const { result: splittedTokens } = splitTokens(
164
- tokens,
165
- match.start,
166
- match.end - 1,
167
- { [pattern.style]: true },
168
- getRequiredLiterals(match.expression).opening
169
- );
170
- tokens = splittedTokens;
171
- plain_text = splittedTokens.reduce((acc, curr) => acc + curr.text, "");
172
-
173
- const parsed = parseRichTextString(tokens, patterns, tokens);
174
-
175
- return {
176
- tokens: parsed.tokens,
177
- plain_text: parsed.plain_text
196
+ let evenCount = 0;
197
+
198
+ for (const char of copyOfString) {
199
+ /** Cases where both opening and closing chars are defined (*...*, _..._, etc.)*/
200
+ if (pattern.opening && pattern.closing) {
201
+ if (evenCount < 2 && char === pattern.opening) {
202
+ evenCount++;
203
+ }
204
+ if (evenCount === 2 && char === pattern.closing) {
205
+ const openingIndex = copyOfString.indexOf(pattern.opening);
206
+ const closingIndex = copyOfString.indexOf(pattern.closing, openingIndex + 1);
207
+
208
+ copyOfString = copyOfString.slice(0, openingIndex) + copyOfString.slice(closingIndex);
209
+ const { result, plain_text } = splitTokens(tokens, openingIndex, closingIndex, { [pattern.style]: true }, pattern.opening);
210
+ tokens = result;
211
+ copyOfString = plain_text;
212
+ }
178
213
  }
214
+
215
+ /** Cases where only opening char is defined (@, #, etc.) */
179
216
  }
180
217
  }
181
218
 
182
219
  return {
183
- tokens: tokens.filter(token => token.text.length > 0),
184
- plain_text: plain_text
185
- }
220
+ tokens,
221
+ plain_text: copyOfString
222
+ };
186
223
  }
187
224
 
188
- // Returns a rich text string
189
- const parseTokens = (tokens) => {
190
-
225
+ /**
226
+ * Parse tokens into rich text string.
227
+ */
228
+ const parseTokens = (tokens: Token[], patterns: Pattern[]) => {
229
+ return tokens.map(token => {
230
+ const { text, annotations } = token;
231
+ // Rich text wrappers (opening and closing chars)
232
+ const wrappers = [];
233
+
234
+ patterns.forEach(pattern => {
235
+ // If annotation has a truthy value, add the corresponding wrapper.
236
+ if (annotations[pattern.style]) wrappers.push(pattern.opening);
237
+ });
238
+
239
+ return wrappers.reduce(
240
+ (children, wrapper) => `${wrapper}${children}${wrapper}`,
241
+ text
242
+ );
243
+ }).join("");
191
244
  }
192
245
 
193
246
  // Inserts a token at the given index
194
247
  // Only when start === end
248
+ // To-do: Instead of recieving annotations and text it could recieve a token.
195
249
  function insertToken(tokens: Token[], index: number, annotations: Annotations, text = "" ) {
196
250
  const updatedTokens = [...tokens];
197
251
 
@@ -227,30 +281,26 @@ function insertToken(tokens: Token[], index: number, annotations: Annotations, t
227
281
  // Middle token is the selected text
228
282
  let middleToken = {
229
283
  text: text,
230
- annotations: concileAnnotations(startToken.annotations, annotations)
284
+ annotations: concileAnnotations(startToken.annotations, annotations) // prevAnnotations + newAnnotations
231
285
  }
232
286
 
233
287
  let lastToken = {
234
288
  text: startToken.text.slice(startIndex , startToken.text.length),
235
289
  annotations: startToken.annotations
236
290
  }
237
-
238
- /**
239
- * Note: the following conditionals are to prevent empty tokens.
240
- * It would be ideal if instead of catching empty tokens we could write the correct insert logic to prevent them.
241
- * Maybe use a filter instead?
242
- */
243
291
 
244
292
  updatedTokens.splice(startTokenIndex, 1, firstToken, middleToken, lastToken);
245
-
246
293
  return {
247
294
  result: updatedTokens.filter(token => token.text.length > 0)
248
295
  };
249
296
  }
250
297
 
251
- // Updates token content (add, remove, replace)
252
- // Note: need to support cross-token updates.
253
- // It's actually updating just the text of tokens
298
+ /**
299
+ * Updates token content (add, remove, replace)
300
+ * It's actually updating just the text of tokens
301
+ * To-do: Separate the logic of finding the corresponding token into another function.
302
+ * Instead of recieving a diff it could recieve an array of tokens to update.
303
+ */
254
304
  const updateTokens = (tokens: Token[], diff: Diff) => {
255
305
  let updatedTokens = [...tokens];
256
306
  const plain_text = tokens.reduce((acc, curr) => acc + curr.text, "");
@@ -412,9 +462,10 @@ const updateTokens = (tokens: Token[], diff: Diff) => {
412
462
  }
413
463
  }
414
464
 
415
- // Updates annotations and splits tokens if necessary
416
- // Only when start !== end
417
- // To-do: Add support for multiple annotations
465
+ /**
466
+ * Updates annotations and splits tokens if necessary. Only when start !== end.
467
+ * To-do: Separate the logic of finding the corresponding token into another function.
468
+ */
418
469
  const splitTokens = (
419
470
  tokens: Token[],
420
471
  start: number,
@@ -475,6 +526,11 @@ const splitTokens = (
475
526
  let middleToken = {
476
527
  // The replace method is used to remove the opening and closing rich text literal chars when parsing.
477
528
  text: startToken.text.slice(startIndex, endIndex).replace(withReplacement, ""),
529
+ /**
530
+ * We need to concile previous annotations with new ones.
531
+ * Eg. If we are applying bold to middle token but start token already has bold, we need to toggle bold off.
532
+ * But if we are applying bold to middle token but start token does not have bold, we need to toggle bold on.
533
+ */
478
534
  annotations: concileAnnotations(startToken.annotations, annotations)
479
535
  }
480
536
 
@@ -485,7 +541,10 @@ const splitTokens = (
485
541
  }
486
542
 
487
543
  updatedTokens.splice(startTokenIndex, 1, firstToken, middleToken, lastToken)
488
- return { result: updatedTokens.filter(token => token.text.length > 0) };
544
+ return {
545
+ result: updatedTokens.filter(token => token.text.length > 0),
546
+ plain_text: updatedTokens.reduce((acc, curr) => acc + curr.text, "")
547
+ };
489
548
  }
490
549
 
491
550
  // Cross-token selection
@@ -544,11 +603,14 @@ const splitTokens = (
544
603
  }
545
604
 
546
605
  updatedTokens = updatedTokens.slice(0, startTokenIndex).concat([firstToken, secondToken, ...updatedMiddleTokens, secondToLastToken, lastToken]).concat(updatedTokens.slice(endTokenIndex + 1));
547
- return { result: updatedTokens.filter(token => token.text.length > 0) };
606
+ return {
607
+ result: updatedTokens.filter(token => token.text.length > 0),
608
+ plain_text: updatedTokens.reduce((acc, curr) => acc + curr.text, "")
609
+ };
548
610
  }
549
611
  }
550
612
 
551
- // Concats tokens containing similar annotations
613
+ // Concats tokens containing same annotations
552
614
  const concatTokens = (tokens: Token[]) => {
553
615
  let concatenedTokens = [];
554
616
 
@@ -560,11 +622,12 @@ const concatTokens = (tokens: Token[]) => {
560
622
 
561
623
  const prevToken = concatenedTokens[concatenedTokens.length - 1];
562
624
 
563
- if (prevToken.annotations.bold === token.annotations.bold &&
564
- prevToken.annotations.italic === token.annotations.italic &&
565
- prevToken.annotations.lineThrough === token.annotations.lineThrough &&
566
- prevToken.annotations.underline === token.annotations.underline &&
567
- prevToken.annotations.code === token.annotations.code) {
625
+ /**
626
+ * If prev token has all the same annotations as current token, we add curent token text to prev token
627
+ * and continue looping without adding current token to concatened tokens array.
628
+ */
629
+ const prevTokenAnnotations = Object.keys(prevToken.annotations);
630
+ if (prevTokenAnnotations.length > 0 && prevTokenAnnotations.every(key => prevToken.annotations[key] === token.annotations[key])) {
568
631
  prevToken.text += token.text;
569
632
  continue;
570
633
  }
@@ -575,17 +638,20 @@ const concatTokens = (tokens: Token[]) => {
575
638
  return concatenedTokens;
576
639
  }
577
640
 
578
- function Token({ token }) {
641
+ interface TokenProps {
642
+ token: Token;
643
+ patterns: Pattern[]
644
+ }
645
+
646
+ function Token(props: TokenProps) : JSX.Element {
647
+ const { token, patterns } = props;
579
648
  const { text, annotations } = token;
580
649
  const wrappers = [];
581
650
 
582
- if (annotations.bold) wrappers.push(Bold);
583
- if (annotations.italic) wrappers.push(Italic);
584
- if (annotations.underline && annotations.lineThrough) wrappers.push(UnderlineStrikethrough);
585
- if (annotations.underline) wrappers.push(Underline);
586
- if (annotations.lineThrough) wrappers.push(Strikethrough);
587
- if (annotations.code) wrappers.push(Code);
588
-
651
+ patterns.forEach(pattern => {
652
+ // If annotation has a truthy value, add the corresponding wrapper.
653
+ if (annotations[pattern.style]) wrappers.push(pattern.render);
654
+ });
589
655
  return wrappers.reduce(
590
656
  (children, Wrapper) => <Wrapper>{children}</Wrapper>,
591
657
  text
@@ -628,9 +694,35 @@ function UnderlineStrikethrough({ children }) {
628
694
  )
629
695
  }
630
696
 
697
+ function Heading({ children }) {
698
+ return (
699
+ <Text style={styles.heading}>{children}</Text>
700
+ )
701
+ }
702
+
703
+ function SubHeading({ children }) {
704
+ return (
705
+ <Text style={styles.subHeading}>{children}</Text>
706
+ )
707
+ }
708
+
709
+ function SubSubHeading({ children }) {
710
+ return (
711
+ <Text style={styles.subSubHeading}>{children}</Text>
712
+ )
713
+ }
714
+
631
715
  export default function RichTextInput(props: RichTextInputProps) {
632
716
  const {
633
- ref
717
+ ref,
718
+ patterns = PATTERNS,
719
+
720
+ /** TextInput props */
721
+ value,
722
+ defaultValue,
723
+ onChangeText,
724
+ onSelectionChange,
725
+ ...rest
634
726
  } = props;
635
727
 
636
728
  const inputRef = useRef<TextInput>(null);
@@ -645,7 +737,6 @@ export default function RichTextInput(props: RichTextInputProps) {
645
737
  code: false
646
738
  }
647
739
  }]);
648
- console.log(tokens);
649
740
  useEffect(() => {
650
741
  if (tokens.length === 0) {
651
742
  setTokens([{
@@ -667,47 +758,43 @@ export default function RichTextInput(props: RichTextInputProps) {
667
758
  */
668
759
  const prevTextRef = useRef(tokens.map(t => t.text).join(""));
669
760
 
670
- // Find a better name
671
- // To-do: Allow for multiple styles at once.
761
+ /**
762
+ * To-do: Find a better name.
763
+ * toSplit state is used to toggle styles when selection length === 0 (start === end).
764
+ * Eg, if user is typing with no styles applied and then presses the "bold" button with no text selected,
765
+ * the text to be inserted after that press should be styled as bold.
766
+ * The same happens to toggle of a style. If user is typing in "bold" and presses the "bold" button again,
767
+ * the text to be inserted after that press should not be styled as bold.
768
+ */
672
769
  const [toSplit, setToSplit] = useState({
673
770
  start: 0,
674
771
  end: 0,
675
- annotations: {
676
- bold: false,
677
- italic: false,
678
- lineThrough: false,
679
- underline: false,
680
- code: false
681
- }
772
+ annotations: {}
682
773
  });
683
-
774
+ /* console.log("toSplit", toSplit); */
684
775
  const handleSelectionChange = ({ nativeEvent }) => {
685
776
  selectionRef.current = nativeEvent.selection;
777
+ onSelectionChange && onSelectionChange(nativeEvent);
686
778
  }
687
779
 
688
780
  const handleOnChangeText = (nextText: string) => {
689
781
  const diff = diffStrings(prevTextRef.current, nextText);
690
782
 
691
- let match : RichTextMatch | null = null;
692
-
693
- for (const pattern of PATTERNS) {
694
- match = findMatch(nextText, pattern.regex);
695
- if (match) break;
696
- }
783
+ const match = findMatchV2(nextText, patterns);
784
+ /* console.log("MATCH:", match); */
697
785
 
786
+ // Note: refactor to use new parseRichText function instead of regex
698
787
  if (match) {
699
788
  // Check token containing match
700
789
  // If token already haves this annotation, do not format and perform a simple updateToken.
701
- const annotation = PATTERNS.find(p => p.regex === match.expression);
702
- const { result } = splitTokens(
790
+ const { result, plain_text } = splitTokens(
703
791
  tokens,
704
792
  match.start,
705
- match.end - 1,
706
- { [annotation.style]: true },
793
+ match.end/* - 1 */, // I don't remember why the -1
794
+ { [match.pattern.style]: true },
707
795
  // Get the rich text opening char to replace it
708
- getRequiredLiterals(match.expression).opening
796
+ match.pattern.opening
709
797
  );
710
- const plain_text = result.reduce((acc, curr) => acc + curr.text, "");
711
798
 
712
799
  setTokens([...concatTokens(result)]);
713
800
  prevTextRef.current = plain_text;
@@ -715,10 +802,8 @@ export default function RichTextInput(props: RichTextInputProps) {
715
802
  return;
716
803
  }
717
804
 
718
- if (diff.start === toSplit.start
719
- && diff.start === toSplit.end
720
- && diff.added.length > 0
721
- && Object.values(toSplit.annotations).includes(true)) {
805
+
806
+ if (Object.values(toSplit.annotations).some(Boolean) && diff.start === toSplit.start && diff.start === toSplit.end) {
722
807
  const { result } = insertToken(
723
808
  tokens,
724
809
  diff.start,
@@ -726,24 +811,19 @@ export default function RichTextInput(props: RichTextInputProps) {
726
811
  diff.added
727
812
  );
728
813
  const plain_text = result.map(t => t.text).join("");
729
- setTokens([...concatTokens(result)]);
814
+ setTokens(concatTokens(result));
730
815
 
731
816
  // Reset
732
817
  setToSplit({
733
818
  start: 0,
734
819
  end: 0,
735
- annotations: {
736
- bold: false,
737
- italic: false,
738
- lineThrough: false,
739
- underline: false,
740
- code: false
741
- }
820
+ annotations: {}
742
821
  });
743
822
  prevTextRef.current = plain_text;
744
823
  return;
745
824
  }
746
825
 
826
+ // Default update
747
827
  const { updatedTokens, plain_text} = updateTokens(tokens, diff);
748
828
 
749
829
  setTokens([...concatTokens(updatedTokens)]);
@@ -751,241 +831,96 @@ export default function RichTextInput(props: RichTextInputProps) {
751
831
  }
752
832
 
753
833
  useImperativeHandle(ref, () => ({
754
-
755
- setValue(value: string) {
834
+ /**
835
+ * Sets the TextInput's value as a rich text string or an array of tokens.
836
+ */
837
+ setValue(value: string | Token[]) {
838
+ if (Array.isArray(value)) {
839
+ // Maybe check if tokens structure is valid before setting.
840
+ setTokens(value);
841
+ return;
842
+ }
756
843
  // To keep styles, parsing should be done before setting value
757
- const { tokens, plain_text } = parseRichTextString(value, PATTERNS);
758
- setTokens([...concatTokens(tokens)]);
844
+ const { tokens, plain_text } = parseRichTextString(value, patterns);
845
+ setTokens(tokens);
759
846
  prevTextRef.current = plain_text;
760
847
  },
761
- toggleBold() {
762
- const { start, end } = selectionRef.current;
763
-
764
- if (start === end && toSplit.annotations.bold) {
765
- setToSplit({
766
- start,
767
- end,
768
- annotations: {
769
- ...toSplit.annotations,
770
- bold: false
771
- }
772
- });
773
- return;
774
- }
775
-
776
- if (start === end) {
777
- setToSplit({
778
- start,
779
- end,
780
- annotations: {
781
- ...toSplit.annotations,
782
- bold: true
783
- }
784
- });
785
- return;
786
- }
787
-
788
- /**
789
- * This prevents that when a portion of text is set to bold, the next text inserted after it is not bold.
790
- */
791
- if (start < end) {
792
- setToSplit({
793
- start: end,
794
- end: end,
795
- annotations: {
796
- ...toSplit.annotations,
797
- bold: true
798
- }
799
- })
800
- }
801
-
802
- const { result } = splitTokens(tokens, start, end, { bold: true });
803
- setTokens([...concatTokens(result)]);
804
- requestAnimationFrame(() => inputRef.current.setSelection(start, end));
848
+ /**
849
+ * Sets the TextInput's selection.
850
+ */
851
+ setSelection(start: number, end: number) {
852
+ inputRef.current.setSelection(start, end);
805
853
  },
806
- toggleItalic() {
807
- const { start, end } = selectionRef.current;
808
-
809
- if (start === end && toSplit.annotations.italic ) {
810
- setToSplit({
811
- start,
812
- end,
813
- annotations: {
814
- ...toSplit.annotations,
815
- italic: false
816
- }
817
- });
818
- return;
819
- }
820
-
821
- if (start === end) {
822
- setToSplit({
823
- start,
824
- end,
825
- annotations: {
826
- ...toSplit.annotations,
827
- italic: true
828
- }
829
- });
830
- return;
831
- }
832
-
833
- if (start < end) {
834
- setToSplit({
835
- start: end,
836
- end: end,
837
- annotations: {
838
- ...toSplit.annotations,
839
- italic: true
840
- }
841
- });
842
- }
843
-
844
- const { result } = splitTokens(tokens, start, end, { italic: true });
845
- setTokens([...concatTokens(result)]);
846
- requestAnimationFrame(() => inputRef.current.setSelection(start, end));
854
+ /**
855
+ * Focuses the TextInput.
856
+ */
857
+ focus() {
858
+ inputRef.current.focus();
847
859
  },
848
- toggleLineThrough() {
849
- const { start, end } = selectionRef.current;
850
-
851
- if (start === end && toSplit.annotations.lineThrough) {
852
- setToSplit({
853
- start,
854
- end,
855
- annotations: {
856
- ...toSplit.annotations,
857
- lineThrough: false
858
- }
859
- });
860
- return;
861
- }
862
-
863
- if (start === end) {
864
- setToSplit({
865
- start,
866
- end,
867
- annotations: {
868
- ...toSplit.annotations,
869
- lineThrough: true
870
- }
871
- });
872
- return;
873
- }
874
-
875
- if (start < end) {
876
- setToSplit({
877
- start: end,
878
- end: end,
879
- annotations: {
880
- ...toSplit.annotations,
881
- lineThrough: true
882
- }
883
- })
884
- }
885
-
886
- const { result } = splitTokens(tokens, start, end, { lineThrough: true });
887
- setTokens([...concatTokens(result)]);
888
- requestAnimationFrame(() => inputRef.current.setSelection(start, end));
860
+ blur() {
861
+ inputRef.current.blur();
862
+ },
863
+ /**
864
+ * Returns the TextInput's value as a rich text string matching the patterns
865
+ * for each style defined in the patterns prop. If a style does not define an
866
+ * opening and closing char, it is ignored.
867
+ */
868
+ getRichTextString() {
869
+ return parseTokens(tokens, patterns);
870
+ },
871
+ /**
872
+ * Returns the TextInput's value as an array of tokens with annotations.
873
+ */
874
+ getTokenizedString() : Token[] {
875
+ return tokens;
889
876
  },
890
- toggleUnderline() {
877
+ /**
878
+ * Toggles a given style. The style prop must match the name of a pattern.
879
+ */
880
+ toggleStyle(style: keyof Token["annotations"]) {
891
881
  const { start, end } = selectionRef.current;
892
882
 
893
- if (start === end && toSplit.annotations.underline) {
894
- setToSplit({
895
- start: 0,
896
- end: 0,
897
- annotations: {
898
- ...toSplit.annotations,
899
- underline: false
900
- }
901
- });
902
- return;
903
- }
904
-
905
883
  if (start === end) {
906
884
  setToSplit({
907
885
  start,
908
886
  end,
909
- annotations: {
910
- ...toSplit.annotations,
911
- underline: true
912
- }
887
+ annotations: concileAnnotations(toSplit.annotations, { [style]: true })
913
888
  });
914
889
  return;
915
890
  }
916
891
 
917
- if (start < end) {
918
- setToSplit({
919
- start: end,
920
- end: end,
921
- annotations: {
922
- ...toSplit.annotations,
923
- underline: true
924
- }
925
- })
926
- }
927
-
928
- const { result } = splitTokens(tokens, start, end, { underline: true });
892
+ const { result } = splitTokens(tokens, start, end, { [style]: true });
929
893
  setTokens([...concatTokens(result)]);
930
- requestAnimationFrame(() => inputRef.current.setSelection(start, end));
894
+ requestAnimationFrame(() => {
895
+ inputRef.current.setSelection(start, end);
896
+ });
931
897
  },
932
- toggleCode() {
933
- const { start, end } = selectionRef.current;
934
-
935
- if (start === end && toSplit.annotations.code ) {
936
- setToSplit({
937
- start: 0,
938
- end: 0,
939
- annotations: {
940
- ...toSplit.annotations,
941
- code: false
942
- }
943
- });
944
- return;
945
- }
946
-
947
- if (start === end) {
948
- setToSplit({
949
- start,
950
- end,
951
- annotations: {
952
- ...toSplit.annotations,
953
- code: true
954
- }
955
- });
956
- return;
957
- }
958
-
959
- if (start < end) {
960
- setToSplit({
961
- start: end,
962
- end: end,
963
- annotations: {
964
- ...toSplit.annotations,
965
- code: true
966
- }
967
- });
898
+ /**
899
+ * Returns the active styles for the current selection.
900
+ */
901
+ getActiveStyles() {
902
+ // Check for styles of the token at the current cursor position.
903
+ const { result } = findTokens(tokens, selectionRef.current.start);
904
+
905
+ if (result[0].annotations) {
906
+ return Object.keys(result[0].annotations).filter(key => result[0].annotations[key]);
968
907
  }
969
908
 
970
- const { result } = splitTokens(tokens, start, end, { code: true });
971
- setTokens([...concatTokens(result)]);
972
- requestAnimationFrame(() => inputRef.current.setSelection(start, end));
909
+ return [];
973
910
  }
974
911
  }));
975
912
 
976
913
  return (
977
914
  <View style={{ position: "relative" }}>
978
915
  <TextInput
979
- multiline={true}
980
916
  ref={inputRef}
981
- autoComplete="off"
982
917
  style={styles.textInput}
983
- placeholder="Rich text input"
984
918
  onSelectionChange={handleSelectionChange}
985
919
  onChangeText={handleOnChangeText}
920
+ {...rest}
986
921
  >
987
922
  <Text style={styles.text}>
988
- {tokens.map((token, i) => <Token key={i} token={token} />)}
923
+ {tokens.map((token, i) => <Token key={i} token={token} patterns={patterns}/>)}
989
924
  </Text>
990
925
  </TextInput>
991
926
  </View>
@@ -1037,5 +972,17 @@ const styles = StyleSheet.create({
1037
972
  padding: 20,
1038
973
  height: 24,
1039
974
  backgroundColor: "blue"
975
+ },
976
+ heading: {
977
+ fontSize: 32,
978
+ fontWeight: "bold"
979
+ },
980
+ subHeading: {
981
+ fontSize: 28,
982
+ fontWeight: "bold"
983
+ },
984
+ subSubHeading: {
985
+ fontSize: 24,
986
+ fontWeight: "bold"
1040
987
  }
1041
988
  });
package/src/Toolbar.tsx CHANGED
@@ -45,86 +45,132 @@ export default function Toolbar({
45
45
  );
46
46
  }
47
47
 
48
- Toolbar.Bold = () => {
48
+ Toolbar.Bold = ({ color = "black" }) => {
49
+ console.log("COLOR:", color);
49
50
  const richTextInputRef = useToolbarContext();
50
51
 
51
52
  const handleBold = () => {
52
- richTextInputRef.current.toggleBold();
53
+ richTextInputRef?.current?.toggleStyle("bold");
53
54
  }
54
55
 
55
56
  return (
56
57
  <TouchableOpacity style={styles.toolbarButton} onPress={handleBold}>
57
- <FontAwesome6 name="bold" size={16} color="black" />
58
+ <FontAwesome6 name="bold" size={16} color={color} />
58
59
  </TouchableOpacity>
59
60
  )
60
61
  }
61
62
 
62
- Toolbar.Italic = () => {
63
+ Toolbar.Italic = ({ color = "black" }) => {
63
64
  const richTextInputRef = useToolbarContext();
64
65
 
65
66
  const handleItalic = () => {
66
- richTextInputRef.current.toggleItalic();
67
+ richTextInputRef?.current?.toggleStyle("italic");
67
68
  }
68
69
 
69
70
  return (
70
71
  <TouchableOpacity style={styles.toolbarButton} onPress={handleItalic}>
71
- <FontAwesome6 name="italic" size={16} color="black" />
72
+ <FontAwesome6 name="italic" size={16} color={color} />
72
73
  </TouchableOpacity>
73
74
  )
74
75
  }
75
76
 
76
- Toolbar.Strikethrough = () => {
77
+ Toolbar.Strikethrough = ({ color = "black" }) => {
77
78
  const richTextInputRef = useToolbarContext();
78
79
 
79
80
  const handleLineThrough = () => {
80
- richTextInputRef.current.toggleLineThrough();
81
+ richTextInputRef?.current?.toggleStyle("lineThrough");
81
82
  }
82
83
 
83
84
  return (
84
85
  <TouchableOpacity style={styles.toolbarButton} onPress={handleLineThrough}>
85
- <FontAwesome6 name="strikethrough" size={16} color="black" />
86
+ <FontAwesome6 name="strikethrough" size={16} color={color} />
86
87
  </TouchableOpacity>
87
88
  )
88
89
  }
89
90
 
90
- Toolbar.Underline = () => {
91
+ Toolbar.Underline = ({ color = "black" }) => {
91
92
  const richTextInputRef = useToolbarContext();
92
93
 
93
94
  const handleUnderline = () => {
94
- richTextInputRef.current.toggleUnderline();
95
+ richTextInputRef.current.toggleStyle("underline");
95
96
  }
96
97
 
97
98
  return (
98
99
  <TouchableOpacity style={styles.toolbarButton} onPress={handleUnderline}>
99
- <FontAwesome6 name="underline" size={16} color="black" />
100
+ <FontAwesome6 name="underline" size={16} color={color} />
100
101
  </TouchableOpacity>
101
102
  )
102
103
  }
103
104
 
104
- Toolbar.Code = () => {
105
+ Toolbar.Heading = () => {
106
+ const richTextInputRef = useToolbarContext();
107
+
108
+ const handleHeading = () => {
109
+ richTextInputRef.current.toggleStyle("heading");
110
+ }
111
+
112
+ return (
113
+ <TouchableOpacity style={[styles.toolbarButton, styles.heading]} onPress={handleHeading}>
114
+ <FontAwesome6 name="heading" size={16} color={color} />
115
+ <FontAwesome6 name="1" size={16} color={color} />
116
+ </TouchableOpacity>
117
+ )
118
+ }
119
+
120
+ Toolbar.SubHeading = () => {
121
+ const richTextInputRef = useToolbarContext();
122
+
123
+ const handleSubHeading = () => {
124
+ richTextInputRef.current.toggleStyle("subHeading");
125
+ }
126
+
127
+ return (
128
+ <TouchableOpacity style={[styles.toolbarButton, styles.heading]} onPress={handleSubHeading}>
129
+ <FontAwesome6 name="heading" size={16} color={color} />
130
+ <FontAwesome6 name="2" size={16} color={color} />
131
+ </TouchableOpacity>
132
+ )
133
+ }
134
+
135
+ Toolbar.SubSubHeading = () => {
136
+ const richTextInputRef = useToolbarContext();
137
+
138
+ const handleSubSubHeading = () => {
139
+ richTextInputRef.current.toggleStyle("subSubHeading");
140
+ }
141
+
142
+ return (
143
+ <TouchableOpacity style={[styles.toolbarButton, styles.heading]} onPress={handleSubSubHeading}>
144
+ <FontAwesome6 name="heading" size={16} color={color} />
145
+ <FontAwesome6 name="3" size={16} color={color} />
146
+ </TouchableOpacity>
147
+ )
148
+ }
149
+
150
+ Toolbar.Code = ({ color = "black" }) => {
105
151
  const richTextInputRef = useToolbarContext();
106
152
 
107
153
  const handleCode = () => {
108
- richTextInputRef.current.toggleCode();
154
+ richTextInputRef?.current?.toggleStyle("code");
109
155
  }
110
156
 
111
157
  return (
112
158
  <TouchableOpacity style={styles.toolbarButton} onPress={handleCode}>
113
- <FontAwesome6 name="code" size={16} color="black" />
159
+ <FontAwesome6 name="code" size={16} color={color} />
114
160
  </TouchableOpacity>
115
161
  )
116
162
  }
117
163
 
118
- Toolbar.Keyboard = () => {
164
+ Toolbar.Keyboard = ({ color = "black" }) => {
119
165
  const handleKeyboardDismiss = () => {
120
166
  Keyboard.dismiss();
121
167
  }
122
168
 
123
169
  return (
124
170
  <TouchableOpacity style={[styles.toolbarButton, styles.keyboardDown]} onPress={handleKeyboardDismiss}>
125
- <FontAwesome6 name="keyboard" size={16} color="black" />
171
+ <FontAwesome6 name="keyboard" size={16} color={color} />
126
172
  <View style={styles.keyboardArrowContainer}>
127
- <FontAwesome6 name="chevron-down" size={8} color="black"/>
173
+ <FontAwesome6 name="chevron-down" size={8} color={color}/>
128
174
  </View>
129
175
  </TouchableOpacity>
130
176
  )
@@ -156,5 +202,9 @@ const styles = StyleSheet.create({
156
202
  keyboardArrowContainer: {
157
203
  position: "absolute",
158
204
  bottom: 13
205
+ },
206
+ heading: {
207
+ flexDirection: "row",
208
+ gap: 4
159
209
  }
160
210
  });