enriched-text-input 1.0.4 → 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.
- package/API_REFERENCE.md +119 -1
- package/CONTRIBUTING.md +33 -0
- package/README.md +52 -45
- package/example/App.tsx +5 -28
- package/index.ts +3 -2
- package/package.json +1 -1
- package/src/{RichTextInput.tsx → EnrichedTextInput.tsx} +57 -130
- package/src/components/StyledText.tsx +106 -0
- package/src/markdownStyles.ts +12 -0
package/API_REFERENCE.md
CHANGED
|
@@ -1 +1,119 @@
|
|
|
1
|
-
# API Reference
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## Props
|
|
4
|
+
|
|
5
|
+
### `ref`
|
|
6
|
+
|
|
7
|
+
A React ref that lets you call any ref methods on the input.
|
|
8
|
+
|
|
9
|
+
### `patterns`
|
|
10
|
+
|
|
11
|
+
An array of style patterns for the input. Use this prop to define which styles should be available for the input to use.
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
interface Pattern {
|
|
15
|
+
name: string;
|
|
16
|
+
render: React.Component;
|
|
17
|
+
opening: string | null;
|
|
18
|
+
closing: string | null;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A pattern can have either both opening and closing enclosures defined or just the opening enclosure, but cannot have only the closing enclosure defined.
|
|
23
|
+
|
|
24
|
+
### `defaultValue`
|
|
25
|
+
|
|
26
|
+
Provides an initial value for the input. Can be a string or an array of tokens. If it’s a string and it matches any style defined in the `patterns` prop, proper styles will be applied.
|
|
27
|
+
|
|
28
|
+
### `onSelectionChange`
|
|
29
|
+
|
|
30
|
+
Callback that is called when the text input selection is changed.
|
|
31
|
+
|
|
32
|
+
### `onValueChange`
|
|
33
|
+
|
|
34
|
+
Callback that is called when the text input's value changes. You can use this callback to call ref methods such as `.getRawValue()`, `.getRichTextValue()` or `.getTokenizedValue()` to get the text input’s value in your preferred
|
|
35
|
+
|
|
36
|
+
### `onDebounceValueChange`
|
|
37
|
+
|
|
38
|
+
Same as `onValueChange` but with an applied debouncing effect.
|
|
39
|
+
|
|
40
|
+
## Ref methods
|
|
41
|
+
|
|
42
|
+
### `.setValue()`
|
|
43
|
+
|
|
44
|
+
```jsx
|
|
45
|
+
setValue: (value: string | Tokens[]) => void;
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Sets the value of the input.
|
|
49
|
+
|
|
50
|
+
- `value: string | Tokens[]` - the value to set the input. If it’s a valid rich text string, the corresponding styling will be applied.
|
|
51
|
+
|
|
52
|
+
### `.setSelection()`
|
|
53
|
+
|
|
54
|
+
```jsx
|
|
55
|
+
setSelection: (start: number, end: number) => void;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Sets the selection of the input.
|
|
59
|
+
|
|
60
|
+
- `start: number` - the start index of the input’s selection.
|
|
61
|
+
- `end: number` - the end index of the input’s selection.
|
|
62
|
+
|
|
63
|
+
### `.focus()`
|
|
64
|
+
|
|
65
|
+
```jsx
|
|
66
|
+
focus: () => void;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Focuses the input;
|
|
70
|
+
|
|
71
|
+
### `.blur()`
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
blur: () => void;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Blurs the input.
|
|
78
|
+
|
|
79
|
+
### `.toggleStyle()`
|
|
80
|
+
|
|
81
|
+
```jsx
|
|
82
|
+
toggleStyle: (style: string) => void;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Toggles a style at the cursor’s position.
|
|
86
|
+
|
|
87
|
+
- `style: string` - the name of a pattern to toggle.
|
|
88
|
+
|
|
89
|
+
### `.getActiveStyle()`
|
|
90
|
+
|
|
91
|
+
```jsx
|
|
92
|
+
getActiveStyle: () => string[] | [];
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Returns the active styles for the current selection.
|
|
96
|
+
|
|
97
|
+
### `.getRawValue()`
|
|
98
|
+
|
|
99
|
+
```jsx
|
|
100
|
+
getRawValue: () => string;
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Returns the input’s value as a raw string without rich text enclosures.
|
|
104
|
+
|
|
105
|
+
### `.getRichTextValue()`
|
|
106
|
+
|
|
107
|
+
```jsx
|
|
108
|
+
getRichTextValue: () => string;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Returns the text input’s value as a rich text string matching the patterns for each style defined in the patterns prop. If a style does not define an opening and closing char, it is ignored.
|
|
112
|
+
|
|
113
|
+
### `.getTokenizedValue()`
|
|
114
|
+
|
|
115
|
+
```jsx
|
|
116
|
+
getTokenizedValue: () => Tokens[];
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Returns the text input's value as an array of tokens.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Clone this repo
|
|
4
|
+
|
|
5
|
+
1. Fork and clone your Github froked repo:
|
|
6
|
+
```
|
|
7
|
+
git clone https://github.com/<github_username>/enriched-text-input.git
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
2. Go to cloned repo directory:
|
|
11
|
+
```
|
|
12
|
+
cd enriched-text-input
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Install dependencies
|
|
16
|
+
|
|
17
|
+
1. Install the dependencies in the root of the repo:
|
|
18
|
+
```
|
|
19
|
+
npm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
2. Go to the example project and install dependencies:
|
|
23
|
+
```
|
|
24
|
+
cd example && npm install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
3. After that you can start the project with:
|
|
28
|
+
```
|
|
29
|
+
cd example && npm start
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Create a pull request
|
|
33
|
+
After making any changes, open a pull request. Once you submit your pull request, it will get reviewed.
|
package/README.md
CHANGED
|
@@ -1,37 +1,36 @@
|
|
|
1
|
-
[](https://discord.gg/DRmNp34bFE)
|
|
2
|
-
|
|
3
1
|
# enriched-text-input
|
|
4
2
|
|
|
5
3
|
> [!Note]
|
|
6
|
-
|
|
4
|
+
This library is still a work in progress. Expect breaking changes.
|
|
5
|
+
>
|
|
7
6
|
|
|
8
|
-
Proof of concept for a JavaScript only rich-text TextInput component for React Native.
|
|
9
|
-
The main idea is to render `<Text>` views as children of `<TextInput>`.
|
|
10
|
-
It will only support text styling since it's not possible to render images inside `Text` views in React Native. [Try it on Expo Snack](https://snack.expo.dev/@patosala/enriched-text-input).
|
|
7
|
+
Proof of concept for a JavaScript only rich-text TextInput component for React Native. The main idea is to render `<Text>` views as children of `<TextInput>`. It will only support text styling since it's not possible to render images inside `Text` views in React Native. [Try it on Expo Snack](https://snack.expo.dev/@patosala/enriched-text-input).
|
|
11
8
|
|
|
12
9
|
## Motivation
|
|
10
|
+
|
|
13
11
|
The field for rich-text in react native is still a bit green. Current libraries that add support for rich-text in react native applications are either WebViews wrapping libraries for the web, limiting customization, or require native code which drops support for Expo Go and react-native-web.
|
|
14
12
|
|
|
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
|
|
13
|
+
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
14
|
|
|
17
15
|
## Installation
|
|
18
|
-
|
|
16
|
+
|
|
17
|
+
```bash
|
|
19
18
|
npm install enriched-text-input
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
## Usage
|
|
23
|
-
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
24
|
import { useRef } from 'react';
|
|
25
25
|
import { StyleSheet, View } from 'react-native';
|
|
26
|
-
|
|
27
|
-
import { RichTextInput, Toolbar } from 'enriched-text-input';
|
|
26
|
+
import { EnrichedTextInput, Toolbar } from 'enriched-text-input';
|
|
28
27
|
|
|
29
28
|
export default function App() {
|
|
30
29
|
const richTextInputRef = useRef(null);
|
|
31
30
|
|
|
32
31
|
return (
|
|
33
32
|
<View style={styles.container}>
|
|
34
|
-
<
|
|
33
|
+
<EnrichedTextInput ref={richTextInputRef}/>
|
|
35
34
|
<Toolbar richTextInputRef={richTextInputRef}>
|
|
36
35
|
<Toolbar.Bold />
|
|
37
36
|
<Toolbar.Italic />
|
|
@@ -54,50 +53,58 @@ const styles = StyleSheet.create({
|
|
|
54
53
|
```
|
|
55
54
|
|
|
56
55
|
## 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).
|
|
58
56
|
|
|
59
|
-
|
|
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 yet supported).
|
|
58
|
+
|
|
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
60
|
|
|
61
61
|
## Features
|
|
62
62
|
|
|
63
|
-
- [x]
|
|
64
|
-
- [ ]
|
|
65
|
-
- [ ]
|
|
66
|
-
- [
|
|
67
|
-
- [x]
|
|
68
|
-
- [
|
|
69
|
-
- [ ] Custom methods and event handlers (setValue, onStartMention, onStyleChange, etc).
|
|
70
|
-
- [ ] Headings.
|
|
63
|
+
- [x] Inline markdown styles (**bold**, *italic*, underline, ~~strikethrough~~ and `inline code`).
|
|
64
|
+
- [ ] Paragraph styles (headings, lists, quotes, etc).
|
|
65
|
+
- [ ] Live rich-text parsing.
|
|
66
|
+
- [ ] Links and mentions.
|
|
67
|
+
- [x] Custom inline styles.
|
|
68
|
+
- [x] Custom methods and event handlers (setValue, onStartMention, onStyleChange, etc).
|
|
71
69
|
|
|
72
|
-
##
|
|
73
|
-
- Inline images.
|
|
74
|
-
- Only `Text`component styles are supported.
|
|
70
|
+
## API Reference
|
|
75
71
|
|
|
76
|
-
|
|
72
|
+
[API Reference](https://github.com/PatoSala/enriched-text-input/blob/main/API_REFERENCE.md)
|
|
77
73
|
|
|
78
|
-
|
|
74
|
+
## Style patterns
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
git clone https://github.com/<github_username>/react-native-rich-text.git
|
|
83
|
-
```
|
|
76
|
+
Style patterns are the styles that you provide the input with for it to know how it should display certain portions of the text. You can check their structure in the [API reference](https://github.com/PatoSala/enriched-text-input/blob/main/API_REFERENCE.md#stylePatterns).
|
|
84
77
|
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
cd react-native-rich-text
|
|
88
|
-
```
|
|
78
|
+
By default `enriched-text-input` uses a set of markdown styles (such as **bold**, *italic*, underline, ~~strikethrough~~ and `inline code`) for you to use out of the box, but you can provide your own custom styles through the `stylePatterns` prop. Keep in mind that when using this prop `enriched-text-input` will ignore any default style patterns so that only the ones you provided will be valid.
|
|
89
79
|
|
|
90
|
-
|
|
80
|
+
If you want you can provide `enriched-text-input` with both default style patterns and any additional style patterns you define:
|
|
91
81
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
npm install
|
|
95
|
-
```
|
|
82
|
+
```bash
|
|
83
|
+
import { EnrichedTextInput, markdownStyles } from "enriched-text-input";
|
|
96
84
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
85
|
+
const customStyles = [
|
|
86
|
+
{
|
|
87
|
+
name: "comment",
|
|
88
|
+
opening: null,
|
|
89
|
+
closing: null,
|
|
90
|
+
render: Comment
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
<EnrichedTextInput
|
|
95
|
+
stylePattrns={[...markdownStyles, ...customStyles]}
|
|
96
|
+
/>
|
|
100
97
|
```
|
|
101
98
|
|
|
102
|
-
##
|
|
103
|
-
|
|
99
|
+
## Applying styles
|
|
100
|
+
|
|
101
|
+
To apply styles you can either toggle them using the `ref` method `.toggleStyle()` which accepts as a parameter the name of a style pattern, or you can use rich-text enclosures while typing. This enclosures are defined within the `stylePatterns` prop and corresponding styles will get applied when a matching pattern is found inside the input.
|
|
102
|
+
|
|
103
|
+
## Known limitations
|
|
104
|
+
|
|
105
|
+
- Inline images.
|
|
106
|
+
- Only `Text` component styles are supported.
|
|
107
|
+
|
|
108
|
+
## Contributing
|
|
109
|
+
|
|
110
|
+
[Contributing guide](https://github.com/PatoSala/enriched-text-input/blob/main/CONTRIBUTING.md)
|
package/example/App.tsx
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
import { useRef, useState } from 'react';
|
|
2
|
-
import { StyleSheet, View, KeyboardAvoidingView, Text,
|
|
3
|
-
import {
|
|
4
|
-
import { RichTextInput, Toolbar, PATTERNS } from 'enriched-text-input';
|
|
2
|
+
import { StyleSheet, View, KeyboardAvoidingView, Text, Button, TextInput } from 'react-native';
|
|
3
|
+
import { EnrichedTextInput, Toolbar, markdownStyles } from 'enriched-text-input';
|
|
5
4
|
import * as Clipboard from 'expo-clipboard';
|
|
6
5
|
|
|
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
|
-
}
|
|
18
|
-
|
|
19
6
|
export default function App() {
|
|
20
7
|
const [rawValue, setRawValue] = useState("");
|
|
21
8
|
const [richTextStringValue, setRichTextStringValue] = useState("");
|
|
@@ -23,17 +10,8 @@ export default function App() {
|
|
|
23
10
|
console.log("ACTIVE STYLES:", activeStyles);
|
|
24
11
|
const richTextInputRef = useRef(null);
|
|
25
12
|
|
|
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
13
|
const handleGetRichText = () => {
|
|
36
|
-
const richText = richTextInputRef.current?.
|
|
14
|
+
const richText = richTextInputRef.current?.getRichTextValue();
|
|
37
15
|
|
|
38
16
|
setRichTextStringValue(richText);
|
|
39
17
|
}
|
|
@@ -57,11 +35,10 @@ export default function App() {
|
|
|
57
35
|
onPress={() => richTextInputRef.current?.setValue(rawValue)}
|
|
58
36
|
/>
|
|
59
37
|
|
|
60
|
-
<
|
|
38
|
+
<EnrichedTextInput
|
|
61
39
|
ref={richTextInputRef}
|
|
62
|
-
patterns={customPatterns}
|
|
63
|
-
autoComplete="off"
|
|
64
40
|
placeholder="Rich text"
|
|
41
|
+
onValueChange={() => console.log("VALUE CHANGE", richTextInputRef.current?.getRichTextValue())}
|
|
65
42
|
multiline={true}
|
|
66
43
|
/>
|
|
67
44
|
<Button
|
package/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import EnrichedTextInput from "./src/EnrichedTextInput";
|
|
2
|
+
import { markdownStyles } from "./src/markdownStyles";
|
|
2
3
|
import Toolbar from "./src/Toolbar";
|
|
3
4
|
|
|
4
|
-
export {
|
|
5
|
+
export { EnrichedTextInput, markdownStyles, Toolbar };
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useImperativeHandle, useRef, useEffect, JSX } from "react";
|
|
2
2
|
import { TextInput, Text, StyleSheet, View, TextInputProps } from "react-native";
|
|
3
|
+
import { markdownStyles } from "./markdownStyles";
|
|
3
4
|
|
|
4
5
|
interface Token {
|
|
5
6
|
text: string;
|
|
@@ -34,9 +35,14 @@ interface Pattern {
|
|
|
34
35
|
closing?: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
interface
|
|
38
|
+
interface EnrichedTextInputProps {
|
|
38
39
|
ref: any;
|
|
39
|
-
|
|
40
|
+
stylePatterns?: Pattern[];
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
multiline?: boolean;
|
|
43
|
+
defaultValue?: string | Token[];
|
|
44
|
+
onValueChange?: () => void;
|
|
45
|
+
onSelectionChange?: () => void;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/**
|
|
@@ -45,16 +51,7 @@ interface RichTextInputProps {
|
|
|
45
51
|
* If just opening is defined, we look for a match that looks like {opening}{content}.
|
|
46
52
|
* Closing can not be defined if opening is not defined.
|
|
47
53
|
*/
|
|
48
|
-
export const
|
|
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 }
|
|
57
|
-
];
|
|
54
|
+
export const defaultStylePatterns : Pattern[] = markdownStyles;
|
|
58
55
|
|
|
59
56
|
function insertAt(str, index, substring) {
|
|
60
57
|
// Clamp index into valid boundaries
|
|
@@ -82,7 +79,7 @@ function findTokens(
|
|
|
82
79
|
) {
|
|
83
80
|
|
|
84
81
|
if (end) {
|
|
85
|
-
//
|
|
82
|
+
// To-do: search for all tokens between start and end
|
|
86
83
|
return { result: null };
|
|
87
84
|
}
|
|
88
85
|
|
|
@@ -143,6 +140,11 @@ function findMatchV2(str: string, patterns: Pattern[]) : RichTextMatch | null {
|
|
|
143
140
|
|
|
144
141
|
/**
|
|
145
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 }
|
|
146
148
|
*/
|
|
147
149
|
function concileAnnotations(prevAnnotations, newAnnotations) {
|
|
148
150
|
let updatedAnnotations = { ...prevAnnotations };
|
|
@@ -180,6 +182,7 @@ function diffStrings(prev, next) : Diff {
|
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
/**
|
|
185
|
+
* [Needs refactoring]
|
|
183
186
|
* Parse rich text string into tokens.
|
|
184
187
|
*/
|
|
185
188
|
const parseRichTextString = (richTextString: string, patterns: Pattern[], initialTokens?: Token[])
|
|
@@ -224,6 +227,13 @@ const parseRichTextString = (richTextString: string, patterns: Pattern[], initia
|
|
|
224
227
|
|
|
225
228
|
/**
|
|
226
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_*
|
|
227
237
|
*/
|
|
228
238
|
const parseTokens = (tokens: Token[], patterns: Pattern[]) => {
|
|
229
239
|
return tokens.map(token => {
|
|
@@ -430,6 +440,7 @@ const updateTokens = (tokens: Token[], diff: Diff) => {
|
|
|
430
440
|
* Remove:
|
|
431
441
|
* - For more than two tokens, works.
|
|
432
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).
|
|
433
444
|
*/
|
|
434
445
|
if (diff.removed.length > 0) {
|
|
435
446
|
const firstToken = selectedTokens[0];
|
|
@@ -658,71 +669,15 @@ function Token(props: TokenProps) : JSX.Element {
|
|
|
658
669
|
);
|
|
659
670
|
}
|
|
660
671
|
|
|
661
|
-
function
|
|
662
|
-
return (
|
|
663
|
-
<Text style={styles.code}>{children}</Text>
|
|
664
|
-
)
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function Bold({ children }) {
|
|
668
|
-
return (
|
|
669
|
-
<Text style={styles.bold}>{children}</Text>
|
|
670
|
-
)
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function Italic({ children }) {
|
|
674
|
-
return (
|
|
675
|
-
<Text style={styles.italic}>{children}</Text>
|
|
676
|
-
)
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function Underline({ children }) {
|
|
680
|
-
return (
|
|
681
|
-
<Text style={styles.underline}>{children}</Text>
|
|
682
|
-
)
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function Strikethrough({ children }) {
|
|
686
|
-
return (
|
|
687
|
-
<Text style={styles.lineThrough}>{children}</Text>
|
|
688
|
-
)
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function UnderlineStrikethrough({ children }) {
|
|
692
|
-
return (
|
|
693
|
-
<Text style={styles.underlineLineThrough}>{children}</Text>
|
|
694
|
-
)
|
|
695
|
-
}
|
|
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
|
-
|
|
715
|
-
export default function RichTextInput(props: RichTextInputProps) {
|
|
672
|
+
export default function EnrichedTextInput(props: EnrichedTextInputProps) {
|
|
716
673
|
const {
|
|
717
674
|
ref,
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
value,
|
|
675
|
+
stylePatterns = defaultStylePatterns,
|
|
676
|
+
placeholder,
|
|
677
|
+
multiline = false,
|
|
722
678
|
defaultValue,
|
|
723
|
-
onChangeText,
|
|
724
679
|
onSelectionChange,
|
|
725
|
-
|
|
680
|
+
onValueChange,
|
|
726
681
|
} = props;
|
|
727
682
|
|
|
728
683
|
const inputRef = useRef<TextInput>(null);
|
|
@@ -750,8 +705,24 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
750
705
|
}
|
|
751
706
|
}])
|
|
752
707
|
}
|
|
708
|
+
|
|
709
|
+
onValueChange && onValueChange();
|
|
753
710
|
}, [tokens]);
|
|
754
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
|
+
|
|
755
726
|
/**
|
|
756
727
|
* Prev text should not contain matching rich text formats.
|
|
757
728
|
* Those should be spliced once the corresponding tokens are created.
|
|
@@ -780,7 +751,7 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
780
751
|
const handleOnChangeText = (nextText: string) => {
|
|
781
752
|
const diff = diffStrings(prevTextRef.current, nextText);
|
|
782
753
|
|
|
783
|
-
const match = findMatchV2(nextText,
|
|
754
|
+
const match = findMatchV2(nextText, stylePatterns);
|
|
784
755
|
/* console.log("MATCH:", match); */
|
|
785
756
|
|
|
786
757
|
// Note: refactor to use new parseRichText function instead of regex
|
|
@@ -841,7 +812,7 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
841
812
|
return;
|
|
842
813
|
}
|
|
843
814
|
// To keep styles, parsing should be done before setting value
|
|
844
|
-
const { tokens, plain_text } = parseRichTextString(value,
|
|
815
|
+
const { tokens, plain_text } = parseRichTextString(value, stylePatterns);
|
|
845
816
|
setTokens(tokens);
|
|
846
817
|
prevTextRef.current = plain_text;
|
|
847
818
|
},
|
|
@@ -865,13 +836,16 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
865
836
|
* for each style defined in the patterns prop. If a style does not define an
|
|
866
837
|
* opening and closing char, it is ignored.
|
|
867
838
|
*/
|
|
868
|
-
|
|
869
|
-
return
|
|
839
|
+
getRawValue() {
|
|
840
|
+
return tokens.map(t => t.text).join("");
|
|
841
|
+
},
|
|
842
|
+
getRichTextValue() {
|
|
843
|
+
return parseTokens(tokens, stylePatterns);
|
|
870
844
|
},
|
|
871
845
|
/**
|
|
872
|
-
* Returns the
|
|
846
|
+
* Returns the text input's value as an array of tokens.
|
|
873
847
|
*/
|
|
874
|
-
|
|
848
|
+
getTokenizedValue() : Token[] {
|
|
875
849
|
return tokens;
|
|
876
850
|
},
|
|
877
851
|
/**
|
|
@@ -915,12 +889,13 @@ export default function RichTextInput(props: RichTextInputProps) {
|
|
|
915
889
|
<TextInput
|
|
916
890
|
ref={inputRef}
|
|
917
891
|
style={styles.textInput}
|
|
892
|
+
placeholder={placeholder}
|
|
893
|
+
multiline={multiline}
|
|
918
894
|
onSelectionChange={handleSelectionChange}
|
|
919
895
|
onChangeText={handleOnChangeText}
|
|
920
|
-
{...rest}
|
|
921
896
|
>
|
|
922
897
|
<Text style={styles.text}>
|
|
923
|
-
{tokens.map((token, i) => <Token key={i} token={token} patterns={
|
|
898
|
+
{tokens.map((token, i) => <Token key={i} token={token} patterns={stylePatterns}/>)}
|
|
924
899
|
</Text>
|
|
925
900
|
</TextInput>
|
|
926
901
|
</View>
|
|
@@ -936,53 +911,5 @@ const styles = StyleSheet.create({
|
|
|
936
911
|
},
|
|
937
912
|
text: {
|
|
938
913
|
color: "black",
|
|
939
|
-
},
|
|
940
|
-
bold: {
|
|
941
|
-
fontWeight: 'bold',
|
|
942
|
-
},
|
|
943
|
-
italic: {
|
|
944
|
-
fontStyle: "italic"
|
|
945
|
-
},
|
|
946
|
-
lineThrough: {
|
|
947
|
-
textDecorationLine: "line-through"
|
|
948
|
-
},
|
|
949
|
-
underline: {
|
|
950
|
-
textDecorationLine: "underline",
|
|
951
|
-
},
|
|
952
|
-
underlineLineThrough: {
|
|
953
|
-
textDecorationLine: "underline line-through"
|
|
954
|
-
},
|
|
955
|
-
codeContainer: {
|
|
956
|
-
backgroundColor: "lightgray",
|
|
957
|
-
paddingHorizontal: 4,
|
|
958
|
-
borderRadius: 4,
|
|
959
|
-
height: 24,
|
|
960
|
-
position: "absolute",
|
|
961
|
-
top: 10
|
|
962
|
-
},
|
|
963
|
-
code: {
|
|
964
|
-
fontFamily: "ui-monospace",
|
|
965
|
-
color: "#EB5757",
|
|
966
|
-
fontSize: 20,
|
|
967
|
-
backgroundColor: "rgba(135, 131, 120, .15)"
|
|
968
|
-
},
|
|
969
|
-
highlight: {
|
|
970
|
-
width: "100%",
|
|
971
|
-
position: "absolute",
|
|
972
|
-
padding: 20,
|
|
973
|
-
height: 24,
|
|
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"
|
|
987
914
|
}
|
|
988
915
|
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Text, StyleSheet } from "react-native";
|
|
2
|
+
|
|
3
|
+
export function Code({ children }) {
|
|
4
|
+
return (
|
|
5
|
+
<Text style={styles.code}>{children}</Text>
|
|
6
|
+
)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Bold({ children }) {
|
|
10
|
+
return (
|
|
11
|
+
<Text style={styles.bold}>{children}</Text>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Italic({ children }) {
|
|
16
|
+
return (
|
|
17
|
+
<Text style={styles.italic}>{children}</Text>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Underline({ children }) {
|
|
22
|
+
return (
|
|
23
|
+
<Text style={styles.underline}>{children}</Text>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Strikethrough({ children }) {
|
|
28
|
+
return (
|
|
29
|
+
<Text style={styles.lineThrough}>{children}</Text>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function UnderlineStrikethrough({ children }) {
|
|
34
|
+
return (
|
|
35
|
+
<Text style={styles.underlineLineThrough}>{children}</Text>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function Heading({ children }) {
|
|
40
|
+
return (
|
|
41
|
+
<Text style={styles.heading}>{children}</Text>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function SubHeading({ children }) {
|
|
46
|
+
return (
|
|
47
|
+
<Text style={styles.subHeading}>{children}</Text>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function SubSubHeading({ children }) {
|
|
52
|
+
return (
|
|
53
|
+
<Text style={styles.subSubHeading}>{children}</Text>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const styles = StyleSheet.create({
|
|
58
|
+
bold: {
|
|
59
|
+
fontWeight: 'bold',
|
|
60
|
+
},
|
|
61
|
+
italic: {
|
|
62
|
+
fontStyle: "italic"
|
|
63
|
+
},
|
|
64
|
+
lineThrough: {
|
|
65
|
+
textDecorationLine: "line-through"
|
|
66
|
+
},
|
|
67
|
+
underline: {
|
|
68
|
+
textDecorationLine: "underline",
|
|
69
|
+
},
|
|
70
|
+
underlineLineThrough: {
|
|
71
|
+
textDecorationLine: "underline line-through"
|
|
72
|
+
},
|
|
73
|
+
codeContainer: {
|
|
74
|
+
backgroundColor: "lightgray",
|
|
75
|
+
paddingHorizontal: 4,
|
|
76
|
+
borderRadius: 4,
|
|
77
|
+
height: 24,
|
|
78
|
+
position: "absolute",
|
|
79
|
+
top: 10
|
|
80
|
+
},
|
|
81
|
+
code: {
|
|
82
|
+
fontFamily: "ui-monospace",
|
|
83
|
+
color: "#EB5757",
|
|
84
|
+
fontSize: 20,
|
|
85
|
+
backgroundColor: "rgba(135, 131, 120, .15)"
|
|
86
|
+
},
|
|
87
|
+
highlight: {
|
|
88
|
+
width: "100%",
|
|
89
|
+
position: "absolute",
|
|
90
|
+
padding: 20,
|
|
91
|
+
height: 24,
|
|
92
|
+
backgroundColor: "blue"
|
|
93
|
+
},
|
|
94
|
+
heading: {
|
|
95
|
+
fontSize: 32,
|
|
96
|
+
fontWeight: "bold"
|
|
97
|
+
},
|
|
98
|
+
subHeading: {
|
|
99
|
+
fontSize: 28,
|
|
100
|
+
fontWeight: "bold"
|
|
101
|
+
},
|
|
102
|
+
subSubHeading: {
|
|
103
|
+
fontSize: 24,
|
|
104
|
+
fontWeight: "bold"
|
|
105
|
+
}
|
|
106
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Bold, Code, Heading, Italic, SubHeading, SubSubHeading, Strikethrough, Underline } from "./components/StyledText";
|
|
2
|
+
|
|
3
|
+
export const markdownStyles = [
|
|
4
|
+
{ style: "bold", regex: "\\*([^*]+)\\*", render: Bold, opening: "*", closing: "*" },
|
|
5
|
+
{ style: "italic", regex: "_([^_]+)_", render: Italic, opening: "_", closing: "_" },
|
|
6
|
+
{ style: "lineThrough", regex: "~([^~]+)~", render: Strikethrough, opening: "~", closing: "~" },
|
|
7
|
+
{ style: "code", regex: "`([^`]+)`", render: Code, opening: "`", closing: "`" },
|
|
8
|
+
{ style: "underline", regex: "__([^_]+)__", render: Underline, opening: "__", closing: "__" },
|
|
9
|
+
{ style: "heading", regex: null, render: Heading, opening: "#", closing: null },
|
|
10
|
+
{ style: "subHeading", regex: null, render: SubHeading, opening: "##", closing: null },
|
|
11
|
+
{ style: "subSubHeading", regex: null, render: SubSubHeading, opening: "###", closing: null }
|
|
12
|
+
];
|