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 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
- import { RichTextInput, Toolbar } from 'enriched-text-input';
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
- <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>
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, 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.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
  }
@@ -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: <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 }} /> },
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
- 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
- };
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
- // Updates token content (add, remove, replace)
252
- // Note: need to support cross-token updates.
253
- // It's actually updating just the text of tokens
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
- // Updates annotations and splits tokens if necessary
416
- // Only when start !== end
417
- // To-do: Add support for multiple annotations
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
- 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) {
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
- function Token({ token }) {
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
- 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);
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
- console.log(tokens);
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 PATTERNS) {
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 = PATTERNS.find(p => p.regex === match.expression);
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([...concatTokens(result)]);
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, PATTERNS);
795
+ const { tokens, plain_text } = parseRichTextString(value, patterns);
758
796
  setTokens([...concatTokens(tokens)]);
759
797
  prevTextRef.current = plain_text;
760
798
  },
761
- toggleBold() {
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, { bold: true });
825
+ const { result } = splitTokens(tokens, start, end, { [style]: true });
803
826
  setTokens([...concatTokens(result)]);
804
- requestAnimationFrame(() => inputRef.current.setSelection(start, end));
805
- },
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));
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.toggleBold();
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.toggleItalic();
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.toggleLineThrough();
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.toggleUnderline();
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.toggleCode();
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
  });