enriched-text-input 1.0.3 → 1.0.5

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.
@@ -1,6 +1,6 @@
1
- import { useState, useImperativeHandle, useRef, useEffect, ReactElement, JSX } from "react";
2
- import { TextInput, Text, StyleSheet, View, Linking } from "react-native";
3
- import { get } from "react-native/Libraries/TurboModule/TurboModuleRegistry";
1
+ import { useState, useImperativeHandle, useRef, useEffect, JSX } from "react";
2
+ import { TextInput, Text, StyleSheet, View, TextInputProps } from "react-native";
3
+ import { markdownStyles } from "./markdownStyles";
4
4
 
5
5
  interface Token {
6
6
  text: string;
@@ -14,12 +14,7 @@ interface Diff {
14
14
  }
15
15
 
16
16
  interface Annotations {
17
- bold: boolean;
18
- italic: boolean;
19
- lineThrough: boolean;
20
- underline: boolean;
21
- underlineLineThrough: boolean;
22
- code: boolean;
17
+ [key: string]: boolean | string | null
23
18
  }
24
19
 
25
20
  interface RichTextMatch {
@@ -27,30 +22,36 @@ interface RichTextMatch {
27
22
  content: string;
28
23
  start: number;
29
24
  end: number;
25
+ pattern: Pattern;
26
+ /** @deprecated */
30
27
  expression: string;
31
28
  }
32
29
 
33
30
  interface Pattern {
34
31
  regex: string;
35
32
  style: string;
36
- render: any
33
+ render: any;
34
+ opening?: string;
35
+ closing?: string;
37
36
  }
38
37
 
39
- interface RichTextInputProps {
38
+ interface EnrichedTextInputProps {
40
39
  ref: any;
41
- patterns?: Pattern[]
40
+ stylePatterns?: Pattern[];
41
+ placeholder?: string;
42
+ multiline?: boolean;
43
+ defaultValue?: string | Token[];
44
+ onValueChange?: () => void;
45
+ onSelectionChange?: () => void;
42
46
  }
43
-
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 }
53
- ];
47
+
48
+ /**
49
+ * Note: maybe instead of using regex we could just define an "opening" and "closing" char.
50
+ * If both are defined we look for a match that looks like {opening}{content}{closing}.
51
+ * If just opening is defined, we look for a match that looks like {opening}{content}.
52
+ * Closing can not be defined if opening is not defined.
53
+ */
54
+ export const defaultStylePatterns : Pattern[] = markdownStyles;
54
55
 
55
56
  function insertAt(str, index, substring) {
56
57
  // Clamp index into valid boundaries
@@ -68,59 +69,82 @@ function replaceAt(str, index, substring, length) {
68
69
  return str.slice(0, i) + substring + str.slice(i + length);
69
70
  }
70
71
 
71
- function findMatch(str: string, regexExpression: string) : RichTextMatch | null {
72
- const regex = new RegExp(regexExpression);
73
- const match = regex.exec(str);
74
- return match
75
- ? {
76
- raw: match[0],
77
- content: match[1],
78
- start: match.index,
79
- end: match.index + match[0].length,
80
- expression: regexExpression
72
+ function findTokens(
73
+ /** The tokens to search over. */
74
+ tokens: Token[],
75
+ /** start position of selection.*/
76
+ start: number,
77
+ /** end position of selection.*/
78
+ end?: number
79
+ ) {
80
+
81
+ if (end) {
82
+ // To-do: search for all tokens between start and end
83
+ return { result: null };
81
84
  }
82
- : null;
83
- }
84
85
 
85
- function getRequiredLiterals(regexString: string) {
86
- // Strip leading/trailing slashes and flags (if user passed /.../ form)
87
- regexString = regexString.replace(/^\/|\/[a-z]*$/g, "");
86
+ let startIndex = start;
87
+ let startToken;
88
+ for (const token of tokens) {
89
+ if (startIndex <= token.text.length) {
90
+ startToken = token;
91
+ break;
92
+ }
93
+ startIndex -= token.text.length;
94
+ }
88
95
 
89
- // Remove ^ and $ anchors
90
- regexString = regexString.replace(/^\^|\$$/g, "");
96
+ const startTokenIndex = tokens.indexOf(startToken);
91
97
 
92
- // 1. Find the first literal before any group or operator
93
- const beforeGroup = regexString.match(/^((?:\\.|[^[(])+)/);
94
- let openLiteral = null;
98
+ return { result: [startToken] };
99
+ }
95
100
 
96
- if (beforeGroup) {
97
- const part = beforeGroup[1];
98
- const litMatch = part.match(/\\(.)|([^\\])/); // first literal
99
- if (litMatch) {
100
- openLiteral = litMatch[1] ?? litMatch[2];
101
- }
102
- }
101
+ /**
102
+ * To-do: Add support for openings and closings that are conformed by two or more chars (e.g. **, __, <b>, etc.)
103
+ */
104
+ function findMatchV2(str: string, patterns: Pattern[]) : RichTextMatch | null {
105
+ let match = null;
106
+ let copyOfString = str;
103
107
 
104
- // 2. Detect a closing literal after a capturing group (optional)
105
- let closeLiteral = null;
106
- const afterGroup = regexString.match(/\)([^).]+)/);
107
- if (afterGroup) {
108
- const part = afterGroup[1];
109
- const litMatch = part.match(/\\(.)|([^\\])/);
110
- if (litMatch) {
111
- closeLiteral = litMatch[1] ?? litMatch[2];
108
+ for (const pattern of patterns) {
109
+ let evenCount = 0;
110
+
111
+ for (const char of copyOfString) {
112
+ /** Cases where both opening and closing chars are defined (*...*, _..._, etc.)*/
113
+ if (pattern.opening && pattern.closing) {
114
+ if (evenCount < 2 && char === pattern.opening) {
115
+ evenCount++;
116
+ }
117
+ if (evenCount === 2 && char === pattern.closing) {
118
+ const openingIndex = copyOfString.indexOf(pattern.opening);
119
+ const closingIndex = copyOfString.indexOf(pattern.closing, openingIndex + 1);
120
+
121
+ match = {
122
+ raw: copyOfString.slice(openingIndex, closingIndex + 1),
123
+ content: copyOfString.slice(openingIndex + 1, closingIndex),
124
+ start: openingIndex,
125
+ end: closingIndex,
126
+ pattern,
127
+ /** @deprecated */
128
+ expression: pattern.regex
129
+ };
130
+ break;
131
+ }
132
+ }
133
+
134
+ /** Cases where only opening char is defined (@, #, etc.) */
135
+ }
112
136
  }
113
- }
114
137
 
115
- // Return both if available, otherwise just the opening literal
116
- return {
117
- opening: openLiteral,
118
- closing: closeLiteral,
119
- };
138
+ return match;
120
139
  }
121
140
 
122
141
  /**
123
142
  * If prev token contains new annotation, negate prev. Else, use new annotation.
143
+ *
144
+ * @example
145
+ * prev: { bold: true }
146
+ * new: { bold: false }
147
+ * result: { bold: false }
124
148
  */
125
149
  function concileAnnotations(prevAnnotations, newAnnotations) {
126
150
  let updatedAnnotations = { ...prevAnnotations };
@@ -157,58 +181,73 @@ function diffStrings(prev, next) : Diff {
157
181
  };
158
182
  }
159
183
 
160
- // Returns an array of tokens
161
- const parseRichTextString = (richTextString: string, patterns: { regex: string, style: string }[], initalTokens = null) => {
162
- let tokens = initalTokens || [
184
+ /**
185
+ * [Needs refactoring]
186
+ * Parse rich text string into tokens.
187
+ */
188
+ const parseRichTextString = (richTextString: string, patterns: Pattern[], initialTokens?: Token[])
189
+ : { tokens: Token[], plain_text: string } => {
190
+ let copyOfString = richTextString;
191
+ let tokens : Token[] = initialTokens || [
163
192
  {
164
- text: richTextString,
193
+ text: copyOfString,
165
194
  annotations: {}
166
195
  }
167
196
  ];
168
- let plain_text = tokens.reduce((acc, curr) => acc + curr.text, "");
169
197
 
170
198
  for (const pattern of patterns) {
171
- let match = pattern.regex ? findMatch(plain_text, pattern.regex) : null;
172
-
173
- if (match) {
174
- const { result: splittedTokens } = splitTokens(
175
- tokens,
176
- match.start,
177
- match.end - 1,
178
- { [pattern.style]: true },
179
- getRequiredLiterals(match.expression).opening
180
- );
181
- tokens = splittedTokens;
182
- plain_text = splittedTokens.reduce((acc, curr) => acc + curr.text, "");
183
-
184
- const parsed = parseRichTextString(tokens, patterns, tokens);
185
-
186
- return {
187
- tokens: parsed.tokens,
188
- plain_text: parsed.plain_text
199
+ let evenCount = 0;
200
+
201
+ for (const char of copyOfString) {
202
+ /** Cases where both opening and closing chars are defined (*...*, _..._, etc.)*/
203
+ if (pattern.opening && pattern.closing) {
204
+ if (evenCount < 2 && char === pattern.opening) {
205
+ evenCount++;
206
+ }
207
+ if (evenCount === 2 && char === pattern.closing) {
208
+ const openingIndex = copyOfString.indexOf(pattern.opening);
209
+ const closingIndex = copyOfString.indexOf(pattern.closing, openingIndex + 1);
210
+
211
+ copyOfString = copyOfString.slice(0, openingIndex) + copyOfString.slice(closingIndex);
212
+ const { result, plain_text } = splitTokens(tokens, openingIndex, closingIndex, { [pattern.style]: true }, pattern.opening);
213
+ tokens = result;
214
+ copyOfString = plain_text;
215
+ }
189
216
  }
217
+
218
+ /** Cases where only opening char is defined (@, #, etc.) */
190
219
  }
191
220
  }
192
221
 
193
222
  return {
194
- tokens: tokens.filter(token => token.text.length > 0),
195
- plain_text: plain_text
196
- }
223
+ tokens,
224
+ plain_text: copyOfString
225
+ };
197
226
  }
198
227
 
199
- // Returns a rich text string
228
+ /**
229
+ * Parse tokens into rich text string.
230
+ * To-do: Find a way to group consequitive tokens with a same annotation inside one single
231
+ * wrapper.
232
+ *
233
+ * @example
234
+ * Tokens: [{ text: "Hello", annotations: { bold: true } }, { text: "World", annotations: { bold: true, italic: true } }]
235
+ * Current output: *Hello* *_World_*
236
+ * Desired output: *Hello _World_*
237
+ */
200
238
  const parseTokens = (tokens: Token[], patterns: Pattern[]) => {
201
239
  return tokens.map(token => {
202
240
  const { text, annotations } = token;
241
+ // Rich text wrappers (opening and closing chars)
203
242
  const wrappers = [];
204
243
 
205
- Object.keys(annotations).forEach(key => {
244
+ patterns.forEach(pattern => {
206
245
  // If annotation has a truthy value, add the corresponding wrapper.
207
- if (annotations[key]) wrappers.push(getRequiredLiterals(patterns.find(p => p.style === key).regex));
246
+ if (annotations[pattern.style]) wrappers.push(pattern.opening);
208
247
  });
209
248
 
210
249
  return wrappers.reduce(
211
- (children, Wrapper) => `${Wrapper.opening}${children}${Wrapper.closing}`,
250
+ (children, wrapper) => `${wrapper}${children}${wrapper}`,
212
251
  text
213
252
  );
214
253
  }).join("");
@@ -268,7 +307,6 @@ function insertToken(tokens: Token[], index: number, annotations: Annotations, t
268
307
 
269
308
  /**
270
309
  * Updates token content (add, remove, replace)
271
- * Note: need to support cross-token updates.
272
310
  * It's actually updating just the text of tokens
273
311
  * To-do: Separate the logic of finding the corresponding token into another function.
274
312
  * Instead of recieving a diff it could recieve an array of tokens to update.
@@ -402,6 +440,7 @@ const updateTokens = (tokens: Token[], diff: Diff) => {
402
440
  * Remove:
403
441
  * - For more than two tokens, works.
404
442
  * - For two tokens, does not work properly.
443
+ * (right now remove for more than two tokens works properly. Anyway, it might need better testing).
405
444
  */
406
445
  if (diff.removed.length > 0) {
407
446
  const firstToken = selectedTokens[0];
@@ -436,7 +475,6 @@ const updateTokens = (tokens: Token[], diff: Diff) => {
436
475
 
437
476
  /**
438
477
  * Updates annotations and splits tokens if necessary. Only when start !== end.
439
- * To-do: Add support for multiple annotations. [done].
440
478
  * To-do: Separate the logic of finding the corresponding token into another function.
441
479
  */
442
480
  const splitTokens = (
@@ -514,7 +552,10 @@ const splitTokens = (
514
552
  }
515
553
 
516
554
  updatedTokens.splice(startTokenIndex, 1, firstToken, middleToken, lastToken)
517
- return { result: updatedTokens.filter(token => token.text.length > 0) };
555
+ return {
556
+ result: updatedTokens.filter(token => token.text.length > 0),
557
+ plain_text: updatedTokens.reduce((acc, curr) => acc + curr.text, "")
558
+ };
518
559
  }
519
560
 
520
561
  // Cross-token selection
@@ -573,11 +614,14 @@ const splitTokens = (
573
614
  }
574
615
 
575
616
  updatedTokens = updatedTokens.slice(0, startTokenIndex).concat([firstToken, secondToken, ...updatedMiddleTokens, secondToLastToken, lastToken]).concat(updatedTokens.slice(endTokenIndex + 1));
576
- return { result: updatedTokens.filter(token => token.text.length > 0) };
617
+ return {
618
+ result: updatedTokens.filter(token => token.text.length > 0),
619
+ plain_text: updatedTokens.reduce((acc, curr) => acc + curr.text, "")
620
+ };
577
621
  }
578
622
  }
579
623
 
580
- // Concats tokens containing similar annotations
624
+ // Concats tokens containing same annotations
581
625
  const concatTokens = (tokens: Token[]) => {
582
626
  let concatenedTokens = [];
583
627
 
@@ -593,7 +637,8 @@ const concatTokens = (tokens: Token[]) => {
593
637
  * If prev token has all the same annotations as current token, we add curent token text to prev token
594
638
  * and continue looping without adding current token to concatened tokens array.
595
639
  */
596
- if (Object.keys(prevToken.annotations).every(key => prevToken.annotations[key] === token.annotations[key])) {
640
+ const prevTokenAnnotations = Object.keys(prevToken.annotations);
641
+ if (prevTokenAnnotations.length > 0 && prevTokenAnnotations.every(key => prevToken.annotations[key] === token.annotations[key])) {
597
642
  prevToken.text += token.text;
598
643
  continue;
599
644
  }
@@ -614,75 +659,25 @@ function Token(props: TokenProps) : JSX.Element {
614
659
  const { text, annotations } = token;
615
660
  const wrappers = [];
616
661
 
617
- Object.keys(annotations).forEach(key => {
662
+ patterns.forEach(pattern => {
618
663
  // If annotation has a truthy value, add the corresponding wrapper.
619
- if (annotations[key]) wrappers.push(patterns.find(p => p.style === key).render);
664
+ if (annotations[pattern.style]) wrappers.push(pattern.render);
620
665
  });
621
-
622
666
  return wrappers.reduce(
623
667
  (children, Wrapper) => <Wrapper>{children}</Wrapper>,
624
668
  text
625
669
  );
626
670
  }
627
671
 
628
- function Code({ children }) {
629
- return (
630
- <Text style={styles.code}>{children}</Text>
631
- )
632
- }
633
-
634
- function Bold({ children }) {
635
- return (
636
- <Text style={styles.bold}>{children}</Text>
637
- )
638
- }
639
-
640
- function Italic({ children }) {
641
- return (
642
- <Text style={styles.italic}>{children}</Text>
643
- )
644
- }
645
-
646
- function Underline({ children }) {
647
- return (
648
- <Text style={styles.underline}>{children}</Text>
649
- )
650
- }
651
-
652
- function Strikethrough({ children }) {
653
- return (
654
- <Text style={styles.lineThrough}>{children}</Text>
655
- )
656
- }
657
-
658
- function UnderlineStrikethrough({ children }) {
659
- return (
660
- <Text style={styles.underlineLineThrough}>{children}</Text>
661
- )
662
- }
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
-
682
- export default function RichTextInput(props: RichTextInputProps) {
672
+ export default function EnrichedTextInput(props: EnrichedTextInputProps) {
683
673
  const {
684
674
  ref,
685
- patterns = PATTERNS
675
+ stylePatterns = defaultStylePatterns,
676
+ placeholder,
677
+ multiline = false,
678
+ defaultValue,
679
+ onSelectionChange,
680
+ onValueChange,
686
681
  } = props;
687
682
 
688
683
  const inputRef = useRef<TextInput>(null);
@@ -697,7 +692,6 @@ export default function RichTextInput(props: RichTextInputProps) {
697
692
  code: false
698
693
  }
699
694
  }]);
700
-
701
695
  useEffect(() => {
702
696
  if (tokens.length === 0) {
703
697
  setTokens([{
@@ -711,49 +705,67 @@ export default function RichTextInput(props: RichTextInputProps) {
711
705
  }
712
706
  }])
713
707
  }
708
+
709
+ onValueChange && onValueChange();
714
710
  }, [tokens]);
715
711
 
712
+ useEffect(( ) => {
713
+ if (defaultValue) {
714
+ if (Array.isArray(defaultValue)) {
715
+ // Maybe check if tokens structure is valid before setting.
716
+ setTokens(defaultValue);
717
+ return;
718
+ }
719
+ // To keep styles, parsing should be done before setting defaultValue
720
+ const { tokens, plain_text } = parseRichTextString(defaultValue, stylePatterns);
721
+ setTokens(tokens);
722
+ prevTextRef.current = plain_text;
723
+ }
724
+ }, []);
725
+
716
726
  /**
717
727
  * Prev text should not contain matching rich text formats.
718
728
  * Those should be spliced once the corresponding tokens are created.
719
729
  */
720
730
  const prevTextRef = useRef(tokens.map(t => t.text).join(""));
721
731
 
722
- // Find a better name
723
- // To-do: Allow for multiple styles at once.
732
+ /**
733
+ * To-do: Find a better name.
734
+ * toSplit state is used to toggle styles when selection length === 0 (start === end).
735
+ * Eg, if user is typing with no styles applied and then presses the "bold" button with no text selected,
736
+ * the text to be inserted after that press should be styled as bold.
737
+ * The same happens to toggle of a style. If user is typing in "bold" and presses the "bold" button again,
738
+ * the text to be inserted after that press should not be styled as bold.
739
+ */
724
740
  const [toSplit, setToSplit] = useState({
725
741
  start: 0,
726
742
  end: 0,
727
743
  annotations: {}
728
744
  });
729
-
745
+ /* console.log("toSplit", toSplit); */
730
746
  const handleSelectionChange = ({ nativeEvent }) => {
731
747
  selectionRef.current = nativeEvent.selection;
748
+ onSelectionChange && onSelectionChange(nativeEvent);
732
749
  }
733
750
 
734
751
  const handleOnChangeText = (nextText: string) => {
735
752
  const diff = diffStrings(prevTextRef.current, nextText);
736
753
 
737
- let match : RichTextMatch | null = null;
738
-
739
- for (const pattern of patterns) {
740
- match = findMatch(nextText, pattern.regex);
741
- if (match) break;
742
- }
754
+ const match = findMatchV2(nextText, stylePatterns);
755
+ /* console.log("MATCH:", match); */
743
756
 
757
+ // Note: refactor to use new parseRichText function instead of regex
744
758
  if (match) {
745
759
  // Check token containing match
746
760
  // If token already haves this annotation, do not format and perform a simple updateToken.
747
- const annotation = patterns.find(p => p.regex === match.expression);
748
- const { result } = splitTokens(
761
+ const { result, plain_text } = splitTokens(
749
762
  tokens,
750
763
  match.start,
751
- match.end - 1,
752
- { [annotation.style]: true },
764
+ match.end/* - 1 */, // I don't remember why the -1
765
+ { [match.pattern.style]: true },
753
766
  // Get the rich text opening char to replace it
754
- getRequiredLiterals(match.expression).opening
767
+ match.pattern.opening
755
768
  );
756
- const plain_text = result.reduce((acc, curr) => acc + curr.text, "");
757
769
 
758
770
  setTokens([...concatTokens(result)]);
759
771
  prevTextRef.current = plain_text;
@@ -761,6 +773,7 @@ export default function RichTextInput(props: RichTextInputProps) {
761
773
  return;
762
774
  }
763
775
 
776
+
764
777
  if (Object.values(toSplit.annotations).some(Boolean) && diff.start === toSplit.start && diff.start === toSplit.end) {
765
778
  const { result } = insertToken(
766
779
  tokens,
@@ -789,17 +802,56 @@ export default function RichTextInput(props: RichTextInputProps) {
789
802
  }
790
803
 
791
804
  useImperativeHandle(ref, () => ({
792
-
793
- setValue(value: string) {
805
+ /**
806
+ * Sets the TextInput's value as a rich text string or an array of tokens.
807
+ */
808
+ setValue(value: string | Token[]) {
809
+ if (Array.isArray(value)) {
810
+ // Maybe check if tokens structure is valid before setting.
811
+ setTokens(value);
812
+ return;
813
+ }
794
814
  // To keep styles, parsing should be done before setting value
795
- const { tokens, plain_text } = parseRichTextString(value, patterns);
796
- setTokens([...concatTokens(tokens)]);
815
+ const { tokens, plain_text } = parseRichTextString(value, stylePatterns);
816
+ setTokens(tokens);
797
817
  prevTextRef.current = plain_text;
798
818
  },
799
- getRichText() {
800
- return parseTokens(tokens, patterns);
819
+ /**
820
+ * Sets the TextInput's selection.
821
+ */
822
+ setSelection(start: number, end: number) {
823
+ inputRef.current.setSelection(start, end);
824
+ },
825
+ /**
826
+ * Focuses the TextInput.
827
+ */
828
+ focus() {
829
+ inputRef.current.focus();
830
+ },
831
+ blur() {
832
+ inputRef.current.blur();
833
+ },
834
+ /**
835
+ * Returns the TextInput's value as a rich text string matching the patterns
836
+ * for each style defined in the patterns prop. If a style does not define an
837
+ * opening and closing char, it is ignored.
838
+ */
839
+ getRawValue() {
840
+ return tokens.map(t => t.text).join("");
841
+ },
842
+ getRichTextValue() {
843
+ return parseTokens(tokens, stylePatterns);
844
+ },
845
+ /**
846
+ * Returns the text input's value as an array of tokens.
847
+ */
848
+ getTokenizedValue() : Token[] {
849
+ return tokens;
801
850
  },
802
- toggleStyle(style: string) {
851
+ /**
852
+ * Toggles a given style. The style prop must match the name of a pattern.
853
+ */
854
+ toggleStyle(style: keyof Token["annotations"]) {
803
855
  const { start, end } = selectionRef.current;
804
856
 
805
857
  if (start === end) {
@@ -811,38 +863,39 @@ export default function RichTextInput(props: RichTextInputProps) {
811
863
  return;
812
864
  }
813
865
 
814
- /**
815
- * This prevents that when a portion of text is set to bold, the next text inserted after it is not bold.
816
- */
817
- if (start < end) {
818
- setToSplit({
819
- start: end,
820
- end: end,
821
- annotations: concileAnnotations(toSplit.annotations, { [style]: true })
822
- })
823
- }
824
-
825
866
  const { result } = splitTokens(tokens, start, end, { [style]: true });
826
867
  setTokens([...concatTokens(result)]);
827
868
  requestAnimationFrame(() => {
828
869
  inputRef.current.setSelection(start, end);
829
- })
870
+ });
871
+ },
872
+ /**
873
+ * Returns the active styles for the current selection.
874
+ */
875
+ getActiveStyles() {
876
+ // Check for styles of the token at the current cursor position.
877
+ const { result } = findTokens(tokens, selectionRef.current.start);
878
+
879
+ if (result[0].annotations) {
880
+ return Object.keys(result[0].annotations).filter(key => result[0].annotations[key]);
881
+ }
882
+
883
+ return [];
830
884
  }
831
885
  }));
832
886
 
833
887
  return (
834
888
  <View style={{ position: "relative" }}>
835
889
  <TextInput
836
- multiline={true}
837
890
  ref={inputRef}
838
- autoComplete="off"
839
891
  style={styles.textInput}
840
- placeholder="Rich text input"
892
+ placeholder={placeholder}
893
+ multiline={multiline}
841
894
  onSelectionChange={handleSelectionChange}
842
895
  onChangeText={handleOnChangeText}
843
896
  >
844
897
  <Text style={styles.text}>
845
- {tokens.map((token, i) => <Token key={i} token={token} patterns={patterns}/>)}
898
+ {tokens.map((token, i) => <Token key={i} token={token} patterns={stylePatterns}/>)}
846
899
  </Text>
847
900
  </TextInput>
848
901
  </View>
@@ -858,53 +911,5 @@ const styles = StyleSheet.create({
858
911
  },
859
912
  text: {
860
913
  color: "black",
861
- },
862
- bold: {
863
- fontWeight: 'bold',
864
- },
865
- italic: {
866
- fontStyle: "italic"
867
- },
868
- lineThrough: {
869
- textDecorationLine: "line-through"
870
- },
871
- underline: {
872
- textDecorationLine: "underline",
873
- },
874
- underlineLineThrough: {
875
- textDecorationLine: "underline line-through"
876
- },
877
- codeContainer: {
878
- backgroundColor: "lightgray",
879
- paddingHorizontal: 4,
880
- borderRadius: 4,
881
- height: 24,
882
- position: "absolute",
883
- top: 10
884
- },
885
- code: {
886
- fontFamily: "ui-monospace",
887
- color: "#EB5757",
888
- fontSize: 20,
889
- backgroundColor: "rgba(135, 131, 120, .15)"
890
- },
891
- highlight: {
892
- width: "100%",
893
- position: "absolute",
894
- padding: 20,
895
- height: 24,
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"
909
914
  }
910
915
  });