enriched-text-input 1.0.2 → 1.0.3
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.
- package/example/App.tsx +72 -14
- package/index.ts +2 -3
- package/package.json +5 -3
- package/src/RichTextInput.tsx +138 -269
- package/src/Toolbar.tsx +54 -5
package/example/App.tsx
CHANGED
|
@@ -1,23 +1,74 @@
|
|
|
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';
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
function Comment({ children }) {
|
|
7
|
+
return (
|
|
8
|
+
<Text style={{
|
|
9
|
+
backgroundColor: "rgba(255, 203, 0, .12)",
|
|
10
|
+
textDecorationLine: "underline",
|
|
11
|
+
textDecorationColor: "rgba(255, 203, 0, .35)",
|
|
12
|
+
}}>
|
|
13
|
+
{children}
|
|
14
|
+
</Text>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
5
17
|
|
|
6
18
|
export default function App() {
|
|
19
|
+
const [rawValue, setRawValue] = useState("");
|
|
7
20
|
const richTextInputRef = useRef(null);
|
|
8
21
|
|
|
22
|
+
const customPatterns = [
|
|
23
|
+
...PATTERNS,
|
|
24
|
+
{ style: "comment", regex: null, render: Comment }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const handleComment = () => {
|
|
28
|
+
richTextInputRef.current?.toggleStyle("comment");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handleGetRichText = () => {
|
|
32
|
+
const richText = richTextInputRef.current?.getRichText();
|
|
33
|
+
console.log(richText);
|
|
34
|
+
}
|
|
35
|
+
|
|
9
36
|
return (
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
<KeyboardAvoidingView style={styles.container} behavior="padding">
|
|
38
|
+
<View style={{ flex: 1 }}>
|
|
39
|
+
{/* <TextInput
|
|
40
|
+
style={{ fontSize: 20, padding: 16 }}
|
|
41
|
+
value={rawValue}
|
|
42
|
+
onChangeText={(text) => setRawValue(text)}
|
|
43
|
+
/>
|
|
44
|
+
<Button
|
|
45
|
+
title='Set rich text string'
|
|
46
|
+
onPress={() => richTextInputRef.current?.setValue(rawValue)}
|
|
47
|
+
/> */}
|
|
48
|
+
<RichTextInput
|
|
49
|
+
ref={richTextInputRef}
|
|
50
|
+
patterns={customPatterns}/>
|
|
51
|
+
|
|
52
|
+
<Button
|
|
53
|
+
title='Get rich text string (check console)'
|
|
54
|
+
onPress={handleGetRichText}
|
|
55
|
+
/>
|
|
56
|
+
</View>
|
|
57
|
+
<View style={{ alignSelf: "end"}}>
|
|
58
|
+
<Toolbar richTextInputRef={richTextInputRef}>
|
|
59
|
+
<Toolbar.Bold />
|
|
60
|
+
<Toolbar.Italic />
|
|
61
|
+
<Toolbar.Underline />
|
|
62
|
+
<Toolbar.Strikethrough />
|
|
63
|
+
<Toolbar.Code />
|
|
64
|
+
<TouchableOpacity style={styles.toolbarButton} onPress={handleComment}>
|
|
65
|
+
<FontAwesome6 name="comment-alt" size={16} color="black" />
|
|
66
|
+
</TouchableOpacity>
|
|
67
|
+
|
|
68
|
+
<Toolbar.Keyboard />
|
|
69
|
+
</Toolbar>
|
|
70
|
+
</View>
|
|
71
|
+
</KeyboardAvoidingView>
|
|
21
72
|
);
|
|
22
73
|
}
|
|
23
74
|
|
|
@@ -27,4 +78,11 @@ const styles = StyleSheet.create({
|
|
|
27
78
|
backgroundColor: '#fff',
|
|
28
79
|
paddingTop: 120
|
|
29
80
|
},
|
|
81
|
+
toolbarButton: {
|
|
82
|
+
height: 50,
|
|
83
|
+
width: 50,
|
|
84
|
+
display: "flex",
|
|
85
|
+
justifyContent: "center",
|
|
86
|
+
alignItems: "center"
|
|
87
|
+
}
|
|
30
88
|
});
|
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,
|
|
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.
|
|
3
|
+
"version": "1.0.3",
|
|
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
|
}
|
package/src/RichTextInput.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { useState, useImperativeHandle, useRef, useEffect } from "react";
|
|
1
|
+
import { useState, useImperativeHandle, useRef, useEffect, ReactElement, JSX } from "react";
|
|
2
2
|
import { TextInput, Text, StyleSheet, View, Linking } from "react-native";
|
|
3
|
+
import { get } from "react-native/Libraries/TurboModule/TurboModuleRegistry";
|
|
3
4
|
|
|
4
5
|
interface Token {
|
|
5
6
|
text: string;
|
|
@@ -17,6 +18,7 @@ interface Annotations {
|
|
|
17
18
|
italic: boolean;
|
|
18
19
|
lineThrough: boolean;
|
|
19
20
|
underline: boolean;
|
|
21
|
+
underlineLineThrough: boolean;
|
|
20
22
|
code: boolean;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -28,15 +30,26 @@ interface RichTextMatch {
|
|
|
28
30
|
expression: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
interface Pattern {
|
|
34
|
+
regex: string;
|
|
35
|
+
style: string;
|
|
36
|
+
render: any
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
interface RichTextInputProps {
|
|
32
|
-
ref: any
|
|
40
|
+
ref: any;
|
|
41
|
+
patterns?: Pattern[]
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
const PATTERNS = [
|
|
36
|
-
{ style: "bold", regex: "\\*([^*]+)\\*", render:
|
|
37
|
-
{ style: "italic", regex: "_([^_]+)_", render:
|
|
38
|
-
{ style: "lineThrough", regex: "~([^~]+)~", render:
|
|
39
|
-
{ style: "code", regex: "`([^`]+)`", render:
|
|
44
|
+
export const PATTERNS : Pattern[] = [
|
|
45
|
+
{ style: "bold", regex: "\\*([^*]+)\\*", render: Bold },
|
|
46
|
+
{ style: "italic", regex: "_([^_]+)_", render: Italic },
|
|
47
|
+
{ style: "lineThrough", regex: "~([^~]+)~", render: Strikethrough },
|
|
48
|
+
{ style: "code", regex: "`([^`]+)`", render: Code },
|
|
49
|
+
{ style: "underline", regex: "__([^_]+)__", render: Underline },
|
|
50
|
+
{ style: "heading", regex: null, render: Heading },
|
|
51
|
+
{ style: "subHeading", regex: null, render: SubHeading },
|
|
52
|
+
{ style: "subSubHeading", regex: null, render: SubSubHeading }
|
|
40
53
|
];
|
|
41
54
|
|
|
42
55
|
function insertAt(str, index, substring) {
|
|
@@ -69,7 +82,7 @@ function findMatch(str: string, regexExpression: string) : RichTextMatch | null
|
|
|
69
82
|
: null;
|
|
70
83
|
}
|
|
71
84
|
|
|
72
|
-
function getRequiredLiterals(regexString) {
|
|
85
|
+
function getRequiredLiterals(regexString: string) {
|
|
73
86
|
// Strip leading/trailing slashes and flags (if user passed /.../ form)
|
|
74
87
|
regexString = regexString.replace(/^\/|\/[a-z]*$/g, "");
|
|
75
88
|
|
|
@@ -106,15 +119,19 @@ function getRequiredLiterals(regexString) {
|
|
|
106
119
|
};
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
/**
|
|
123
|
+
* If prev token contains new annotation, negate prev. Else, use new annotation.
|
|
124
|
+
*/
|
|
125
|
+
function concileAnnotations(prevAnnotations, newAnnotations) {
|
|
126
|
+
let updatedAnnotations = { ...prevAnnotations };
|
|
127
|
+
|
|
128
|
+
for (const key of Object.keys(newAnnotations)) {
|
|
129
|
+
newAnnotations[key]
|
|
130
|
+
? updatedAnnotations[key] = !updatedAnnotations[key]
|
|
131
|
+
: updatedAnnotations[key] = newAnnotations[key];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return updatedAnnotations;
|
|
118
135
|
}
|
|
119
136
|
|
|
120
137
|
// Returns string modifications
|
|
@@ -145,19 +162,13 @@ const parseRichTextString = (richTextString: string, patterns: { regex: string,
|
|
|
145
162
|
let tokens = initalTokens || [
|
|
146
163
|
{
|
|
147
164
|
text: richTextString,
|
|
148
|
-
annotations: {
|
|
149
|
-
bold: false,
|
|
150
|
-
italic: false,
|
|
151
|
-
lineThrough: false,
|
|
152
|
-
underline: false,
|
|
153
|
-
code: false
|
|
154
|
-
}
|
|
165
|
+
annotations: {}
|
|
155
166
|
}
|
|
156
167
|
];
|
|
157
168
|
let plain_text = tokens.reduce((acc, curr) => acc + curr.text, "");
|
|
158
169
|
|
|
159
170
|
for (const pattern of patterns) {
|
|
160
|
-
let match = findMatch(plain_text, pattern.regex);
|
|
171
|
+
let match = pattern.regex ? findMatch(plain_text, pattern.regex) : null;
|
|
161
172
|
|
|
162
173
|
if (match) {
|
|
163
174
|
const { result: splittedTokens } = splitTokens(
|
|
@@ -186,12 +197,26 @@ const parseRichTextString = (richTextString: string, patterns: { regex: string,
|
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
// Returns a rich text string
|
|
189
|
-
const parseTokens = (tokens) => {
|
|
190
|
-
|
|
200
|
+
const parseTokens = (tokens: Token[], patterns: Pattern[]) => {
|
|
201
|
+
return tokens.map(token => {
|
|
202
|
+
const { text, annotations } = token;
|
|
203
|
+
const wrappers = [];
|
|
204
|
+
|
|
205
|
+
Object.keys(annotations).forEach(key => {
|
|
206
|
+
// If annotation has a truthy value, add the corresponding wrapper.
|
|
207
|
+
if (annotations[key]) wrappers.push(getRequiredLiterals(patterns.find(p => p.style === key).regex));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return wrappers.reduce(
|
|
211
|
+
(children, Wrapper) => `${Wrapper.opening}${children}${Wrapper.closing}`,
|
|
212
|
+
text
|
|
213
|
+
);
|
|
214
|
+
}).join("");
|
|
191
215
|
}
|
|
192
216
|
|
|
193
217
|
// Inserts a token at the given index
|
|
194
218
|
// Only when start === end
|
|
219
|
+
// To-do: Instead of recieving annotations and text it could recieve a token.
|
|
195
220
|
function insertToken(tokens: Token[], index: number, annotations: Annotations, text = "" ) {
|
|
196
221
|
const updatedTokens = [...tokens];
|
|
197
222
|
|
|
@@ -227,30 +252,27 @@ function insertToken(tokens: Token[], index: number, annotations: Annotations, t
|
|
|
227
252
|
// Middle token is the selected text
|
|
228
253
|
let middleToken = {
|
|
229
254
|
text: text,
|
|
230
|
-
annotations: concileAnnotations(startToken.annotations, annotations)
|
|
255
|
+
annotations: concileAnnotations(startToken.annotations, annotations) // prevAnnotations + newAnnotations
|
|
231
256
|
}
|
|
232
257
|
|
|
233
258
|
let lastToken = {
|
|
234
259
|
text: startToken.text.slice(startIndex , startToken.text.length),
|
|
235
260
|
annotations: startToken.annotations
|
|
236
261
|
}
|
|
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
262
|
|
|
244
263
|
updatedTokens.splice(startTokenIndex, 1, firstToken, middleToken, lastToken);
|
|
245
|
-
|
|
246
264
|
return {
|
|
247
265
|
result: updatedTokens.filter(token => token.text.length > 0)
|
|
248
266
|
};
|
|
249
267
|
}
|
|
250
268
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Updates token content (add, remove, replace)
|
|
271
|
+
* Note: need to support cross-token updates.
|
|
272
|
+
* It's actually updating just the text of tokens
|
|
273
|
+
* To-do: Separate the logic of finding the corresponding token into another function.
|
|
274
|
+
* Instead of recieving a diff it could recieve an array of tokens to update.
|
|
275
|
+
*/
|
|
254
276
|
const updateTokens = (tokens: Token[], diff: Diff) => {
|
|
255
277
|
let updatedTokens = [...tokens];
|
|
256
278
|
const plain_text = tokens.reduce((acc, curr) => acc + curr.text, "");
|
|
@@ -412,9 +434,11 @@ const updateTokens = (tokens: Token[], diff: Diff) => {
|
|
|
412
434
|
}
|
|
413
435
|
}
|
|
414
436
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Updates annotations and splits tokens if necessary. Only when start !== end.
|
|
439
|
+
* To-do: Add support for multiple annotations. [done].
|
|
440
|
+
* To-do: Separate the logic of finding the corresponding token into another function.
|
|
441
|
+
*/
|
|
418
442
|
const splitTokens = (
|
|
419
443
|
tokens: Token[],
|
|
420
444
|
start: number,
|
|
@@ -475,6 +499,11 @@ const splitTokens = (
|
|
|
475
499
|
let middleToken = {
|
|
476
500
|
// The replace method is used to remove the opening and closing rich text literal chars when parsing.
|
|
477
501
|
text: startToken.text.slice(startIndex, endIndex).replace(withReplacement, ""),
|
|
502
|
+
/**
|
|
503
|
+
* We need to concile previous annotations with new ones.
|
|
504
|
+
* Eg. If we are applying bold to middle token but start token already has bold, we need to toggle bold off.
|
|
505
|
+
* But if we are applying bold to middle token but start token does not have bold, we need to toggle bold on.
|
|
506
|
+
*/
|
|
478
507
|
annotations: concileAnnotations(startToken.annotations, annotations)
|
|
479
508
|
}
|
|
480
509
|
|
|
@@ -560,11 +589,11 @@ const concatTokens = (tokens: Token[]) => {
|
|
|
560
589
|
|
|
561
590
|
const prevToken = concatenedTokens[concatenedTokens.length - 1];
|
|
562
591
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
592
|
+
/**
|
|
593
|
+
* If prev token has all the same annotations as current token, we add curent token text to prev token
|
|
594
|
+
* and continue looping without adding current token to concatened tokens array.
|
|
595
|
+
*/
|
|
596
|
+
if (Object.keys(prevToken.annotations).every(key => prevToken.annotations[key] === token.annotations[key])) {
|
|
568
597
|
prevToken.text += token.text;
|
|
569
598
|
continue;
|
|
570
599
|
}
|
|
@@ -575,16 +604,20 @@ const concatTokens = (tokens: Token[]) => {
|
|
|
575
604
|
return concatenedTokens;
|
|
576
605
|
}
|
|
577
606
|
|
|
578
|
-
|
|
607
|
+
interface TokenProps {
|
|
608
|
+
token: Token;
|
|
609
|
+
patterns: Pattern[]
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function Token(props: TokenProps) : JSX.Element {
|
|
613
|
+
const { token, patterns } = props;
|
|
579
614
|
const { text, annotations } = token;
|
|
580
615
|
const wrappers = [];
|
|
581
616
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (annotations.lineThrough) wrappers.push(Strikethrough);
|
|
587
|
-
if (annotations.code) wrappers.push(Code);
|
|
617
|
+
Object.keys(annotations).forEach(key => {
|
|
618
|
+
// If annotation has a truthy value, add the corresponding wrapper.
|
|
619
|
+
if (annotations[key]) wrappers.push(patterns.find(p => p.style === key).render);
|
|
620
|
+
});
|
|
588
621
|
|
|
589
622
|
return wrappers.reduce(
|
|
590
623
|
(children, Wrapper) => <Wrapper>{children}</Wrapper>,
|
|
@@ -628,9 +661,28 @@ function UnderlineStrikethrough({ children }) {
|
|
|
628
661
|
)
|
|
629
662
|
}
|
|
630
663
|
|
|
664
|
+
function Heading({ children }) {
|
|
665
|
+
return (
|
|
666
|
+
<Text style={styles.heading}>{children}</Text>
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function SubHeading({ children }) {
|
|
671
|
+
return (
|
|
672
|
+
<Text style={styles.subHeading}>{children}</Text>
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function SubSubHeading({ children }) {
|
|
677
|
+
return (
|
|
678
|
+
<Text style={styles.subSubHeading}>{children}</Text>
|
|
679
|
+
)
|
|
680
|
+
}
|
|
681
|
+
|
|
631
682
|
export default function RichTextInput(props: RichTextInputProps) {
|
|
632
683
|
const {
|
|
633
|
-
ref
|
|
684
|
+
ref,
|
|
685
|
+
patterns = PATTERNS
|
|
634
686
|
} = props;
|
|
635
687
|
|
|
636
688
|
const inputRef = useRef<TextInput>(null);
|
|
@@ -645,7 +697,7 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
645
697
|
code: false
|
|
646
698
|
}
|
|
647
699
|
}]);
|
|
648
|
-
|
|
700
|
+
|
|
649
701
|
useEffect(() => {
|
|
650
702
|
if (tokens.length === 0) {
|
|
651
703
|
setTokens([{
|
|
@@ -672,13 +724,7 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
672
724
|
const [toSplit, setToSplit] = useState({
|
|
673
725
|
start: 0,
|
|
674
726
|
end: 0,
|
|
675
|
-
annotations: {
|
|
676
|
-
bold: false,
|
|
677
|
-
italic: false,
|
|
678
|
-
lineThrough: false,
|
|
679
|
-
underline: false,
|
|
680
|
-
code: false
|
|
681
|
-
}
|
|
727
|
+
annotations: {}
|
|
682
728
|
});
|
|
683
729
|
|
|
684
730
|
const handleSelectionChange = ({ nativeEvent }) => {
|
|
@@ -690,7 +736,7 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
690
736
|
|
|
691
737
|
let match : RichTextMatch | null = null;
|
|
692
738
|
|
|
693
|
-
for (const pattern of
|
|
739
|
+
for (const pattern of patterns) {
|
|
694
740
|
match = findMatch(nextText, pattern.regex);
|
|
695
741
|
if (match) break;
|
|
696
742
|
}
|
|
@@ -698,7 +744,7 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
698
744
|
if (match) {
|
|
699
745
|
// Check token containing match
|
|
700
746
|
// If token already haves this annotation, do not format and perform a simple updateToken.
|
|
701
|
-
const annotation =
|
|
747
|
+
const annotation = patterns.find(p => p.regex === match.expression);
|
|
702
748
|
const { result } = splitTokens(
|
|
703
749
|
tokens,
|
|
704
750
|
match.start,
|
|
@@ -715,10 +761,7 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
715
761
|
return;
|
|
716
762
|
}
|
|
717
763
|
|
|
718
|
-
if (diff.start === toSplit.start
|
|
719
|
-
&& diff.start === toSplit.end
|
|
720
|
-
&& diff.added.length > 0
|
|
721
|
-
&& Object.values(toSplit.annotations).includes(true)) {
|
|
764
|
+
if (Object.values(toSplit.annotations).some(Boolean) && diff.start === toSplit.start && diff.start === toSplit.end) {
|
|
722
765
|
const { result } = insertToken(
|
|
723
766
|
tokens,
|
|
724
767
|
diff.start,
|
|
@@ -726,24 +769,19 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
726
769
|
diff.added
|
|
727
770
|
);
|
|
728
771
|
const plain_text = result.map(t => t.text).join("");
|
|
729
|
-
setTokens(
|
|
772
|
+
setTokens(concatTokens(result));
|
|
730
773
|
|
|
731
774
|
// Reset
|
|
732
775
|
setToSplit({
|
|
733
776
|
start: 0,
|
|
734
777
|
end: 0,
|
|
735
|
-
annotations: {
|
|
736
|
-
bold: false,
|
|
737
|
-
italic: false,
|
|
738
|
-
lineThrough: false,
|
|
739
|
-
underline: false,
|
|
740
|
-
code: false
|
|
741
|
-
}
|
|
778
|
+
annotations: {}
|
|
742
779
|
});
|
|
743
780
|
prevTextRef.current = plain_text;
|
|
744
781
|
return;
|
|
745
782
|
}
|
|
746
783
|
|
|
784
|
+
// Default update
|
|
747
785
|
const { updatedTokens, plain_text} = updateTokens(tokens, diff);
|
|
748
786
|
|
|
749
787
|
setTokens([...concatTokens(updatedTokens)]);
|
|
@@ -754,33 +792,21 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
754
792
|
|
|
755
793
|
setValue(value: string) {
|
|
756
794
|
// To keep styles, parsing should be done before setting value
|
|
757
|
-
const { tokens, plain_text } = parseRichTextString(value,
|
|
795
|
+
const { tokens, plain_text } = parseRichTextString(value, patterns);
|
|
758
796
|
setTokens([...concatTokens(tokens)]);
|
|
759
797
|
prevTextRef.current = plain_text;
|
|
760
798
|
},
|
|
761
|
-
|
|
799
|
+
getRichText() {
|
|
800
|
+
return parseTokens(tokens, patterns);
|
|
801
|
+
},
|
|
802
|
+
toggleStyle(style: string) {
|
|
762
803
|
const { start, end } = selectionRef.current;
|
|
763
804
|
|
|
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
805
|
if (start === end) {
|
|
777
806
|
setToSplit({
|
|
778
807
|
start,
|
|
779
808
|
end,
|
|
780
|
-
annotations: {
|
|
781
|
-
...toSplit.annotations,
|
|
782
|
-
bold: true
|
|
783
|
-
}
|
|
809
|
+
annotations: concileAnnotations(toSplit.annotations, { [style]: true })
|
|
784
810
|
});
|
|
785
811
|
return;
|
|
786
812
|
}
|
|
@@ -792,184 +818,15 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
792
818
|
setToSplit({
|
|
793
819
|
start: end,
|
|
794
820
|
end: end,
|
|
795
|
-
annotations: {
|
|
796
|
-
...toSplit.annotations,
|
|
797
|
-
bold: true
|
|
798
|
-
}
|
|
821
|
+
annotations: concileAnnotations(toSplit.annotations, { [style]: true })
|
|
799
822
|
})
|
|
800
823
|
}
|
|
801
824
|
|
|
802
|
-
const { result } = splitTokens(tokens, start, end, {
|
|
825
|
+
const { result } = splitTokens(tokens, start, end, { [style]: true });
|
|
803
826
|
setTokens([...concatTokens(result)]);
|
|
804
|
-
requestAnimationFrame(() =>
|
|
805
|
-
|
|
806
|
-
|
|
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));
|
|
847
|
-
},
|
|
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));
|
|
889
|
-
},
|
|
890
|
-
toggleUnderline() {
|
|
891
|
-
const { start, end } = selectionRef.current;
|
|
892
|
-
|
|
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
|
-
if (start === end) {
|
|
906
|
-
setToSplit({
|
|
907
|
-
start,
|
|
908
|
-
end,
|
|
909
|
-
annotations: {
|
|
910
|
-
...toSplit.annotations,
|
|
911
|
-
underline: true
|
|
912
|
-
}
|
|
913
|
-
});
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
|
|
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 });
|
|
929
|
-
setTokens([...concatTokens(result)]);
|
|
930
|
-
requestAnimationFrame(() => inputRef.current.setSelection(start, end));
|
|
931
|
-
},
|
|
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
|
-
});
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const { result } = splitTokens(tokens, start, end, { code: true });
|
|
971
|
-
setTokens([...concatTokens(result)]);
|
|
972
|
-
requestAnimationFrame(() => inputRef.current.setSelection(start, end));
|
|
827
|
+
requestAnimationFrame(() => {
|
|
828
|
+
inputRef.current.setSelection(start, end);
|
|
829
|
+
})
|
|
973
830
|
}
|
|
974
831
|
}));
|
|
975
832
|
|
|
@@ -985,7 +842,7 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
985
842
|
onChangeText={handleOnChangeText}
|
|
986
843
|
>
|
|
987
844
|
<Text style={styles.text}>
|
|
988
|
-
{tokens.map((token, i) => <Token key={i} token={token} />)}
|
|
845
|
+
{tokens.map((token, i) => <Token key={i} token={token} patterns={patterns}/>)}
|
|
989
846
|
</Text>
|
|
990
847
|
</TextInput>
|
|
991
848
|
</View>
|
|
@@ -1037,5 +894,17 @@ const styles = StyleSheet.create({
|
|
|
1037
894
|
padding: 20,
|
|
1038
895
|
height: 24,
|
|
1039
896
|
backgroundColor: "blue"
|
|
897
|
+
},
|
|
898
|
+
heading: {
|
|
899
|
+
fontSize: 32,
|
|
900
|
+
fontWeight: "bold"
|
|
901
|
+
},
|
|
902
|
+
subHeading: {
|
|
903
|
+
fontSize: 28,
|
|
904
|
+
fontWeight: "bold"
|
|
905
|
+
},
|
|
906
|
+
subSubHeading: {
|
|
907
|
+
fontSize: 24,
|
|
908
|
+
fontWeight: "bold"
|
|
1040
909
|
}
|
|
1041
910
|
});
|
package/src/Toolbar.tsx
CHANGED
|
@@ -49,7 +49,7 @@ Toolbar.Bold = () => {
|
|
|
49
49
|
const richTextInputRef = useToolbarContext();
|
|
50
50
|
|
|
51
51
|
const handleBold = () => {
|
|
52
|
-
richTextInputRef.current.
|
|
52
|
+
richTextInputRef.current.toggleStyle("bold");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
return (
|
|
@@ -63,7 +63,7 @@ Toolbar.Italic = () => {
|
|
|
63
63
|
const richTextInputRef = useToolbarContext();
|
|
64
64
|
|
|
65
65
|
const handleItalic = () => {
|
|
66
|
-
richTextInputRef.current.
|
|
66
|
+
richTextInputRef.current.toggleStyle("italic");
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
return (
|
|
@@ -77,7 +77,7 @@ Toolbar.Strikethrough = () => {
|
|
|
77
77
|
const richTextInputRef = useToolbarContext();
|
|
78
78
|
|
|
79
79
|
const handleLineThrough = () => {
|
|
80
|
-
richTextInputRef.current.
|
|
80
|
+
richTextInputRef.current.toggleStyle("lineThrough");
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
return (
|
|
@@ -91,7 +91,7 @@ Toolbar.Underline = () => {
|
|
|
91
91
|
const richTextInputRef = useToolbarContext();
|
|
92
92
|
|
|
93
93
|
const handleUnderline = () => {
|
|
94
|
-
richTextInputRef.current.
|
|
94
|
+
richTextInputRef.current.toggleStyle("underline");
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
return (
|
|
@@ -101,11 +101,56 @@ Toolbar.Underline = () => {
|
|
|
101
101
|
)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
Toolbar.Heading = () => {
|
|
105
|
+
const richTextInputRef = useToolbarContext();
|
|
106
|
+
|
|
107
|
+
const handleHeading = () => {
|
|
108
|
+
richTextInputRef.current.toggleStyle("heading");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<TouchableOpacity style={[styles.toolbarButton, styles.heading]} onPress={handleHeading}>
|
|
113
|
+
<FontAwesome6 name="heading" size={16} color="black" />
|
|
114
|
+
<FontAwesome6 name="1" size={16} color="black" />
|
|
115
|
+
</TouchableOpacity>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Toolbar.SubHeading = () => {
|
|
120
|
+
const richTextInputRef = useToolbarContext();
|
|
121
|
+
|
|
122
|
+
const handleSubHeading = () => {
|
|
123
|
+
richTextInputRef.current.toggleStyle("subHeading");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<TouchableOpacity style={[styles.toolbarButton, styles.heading]} onPress={handleSubHeading}>
|
|
128
|
+
<FontAwesome6 name="heading" size={16} color="black" />
|
|
129
|
+
<FontAwesome6 name="2" size={16} color="black" />
|
|
130
|
+
</TouchableOpacity>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Toolbar.SubSubHeading = () => {
|
|
135
|
+
const richTextInputRef = useToolbarContext();
|
|
136
|
+
|
|
137
|
+
const handleSubSubHeading = () => {
|
|
138
|
+
richTextInputRef.current.toggleStyle("subSubHeading");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<TouchableOpacity style={[styles.toolbarButton, styles.heading]} onPress={handleSubSubHeading}>
|
|
143
|
+
<FontAwesome6 name="heading" size={16} color="black" />
|
|
144
|
+
<FontAwesome6 name="3" size={16} color="black" />
|
|
145
|
+
</TouchableOpacity>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
104
149
|
Toolbar.Code = () => {
|
|
105
150
|
const richTextInputRef = useToolbarContext();
|
|
106
151
|
|
|
107
152
|
const handleCode = () => {
|
|
108
|
-
richTextInputRef.current.
|
|
153
|
+
richTextInputRef.current.toggleStyle("code");
|
|
109
154
|
}
|
|
110
155
|
|
|
111
156
|
return (
|
|
@@ -156,5 +201,9 @@ const styles = StyleSheet.create({
|
|
|
156
201
|
keyboardArrowContainer: {
|
|
157
202
|
position: "absolute",
|
|
158
203
|
bottom: 13
|
|
204
|
+
},
|
|
205
|
+
heading: {
|
|
206
|
+
flexDirection: "row",
|
|
207
|
+
gap: 4
|
|
159
208
|
}
|
|
160
209
|
});
|