├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── expo-demo
├── .gitignore
├── .watchmanconfig
├── App.js
├── app.json
├── assets
│ ├── icon.png
│ └── splash.png
├── babel.config.js
├── package.json
├── yarn-error.log
└── yarn.lock
├── images
└── demo-img.jpg
├── index.d.ts
├── index.js
├── package.json
├── src
├── CNRichTextEditor.js
├── CNRichTextView.js
├── CNSeperator.js
├── CNStyledText.js
├── CNTextInput.js
├── CNToolbar.js
├── CNToolbarIcon.js
├── CNToolbarSetIcon.js
└── Convertors.js
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "parserOptions": {
5 | "ecmaVersion": 9
6 | },
7 | "env": {
8 | "es6": true
9 | }
10 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p12
6 | *.key
7 | *.mobileprovision
8 |
9 |
10 | # Android Studio
11 | **/.idea/libraries
12 | **/.idea/workspace.xml
13 | **/.idea/gradle.xml
14 | **/.idea/misc.xml
15 | **/.idea/modules.xml
16 | **/.idea/vcs.xml
17 | /android/.idea/caches
18 | *.iml
19 | .gradle
20 | /demo/android/app/build
21 | /demo/android/build
22 | /demo/android/captures
23 | /demo/android/local.properties
24 | /demo/android/tools/build
25 | /demo/android/ReactAndroid/build
26 | /demo/android/app/libs/ReactAndroid-temp
27 | /demo/android/versioned-react-native/build
28 | /demo/android/versioned-react-native/local.properties
29 | /demo/android/versioned-react-native/ReactAndroid
30 | ReactAndroid-temp.aar
31 | *.apk
32 |
33 |
34 | # Xcode
35 | .DS_Store
36 | /demo/ios/Pods
37 | /demo/ios/fefet
38 | /demo/ios/fefet/Supporting
39 | /demo/ios/fefet.*
40 | /demo/ios/fefet/Supporting/EXBuildConstants.json
41 | /demo/ios/fefet/Supporting/EXBuildConstants.plist
42 | /demo/ios/fefet/Supporting/EXBuildConstants.plist.bak
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Narbe Hemat Siraki
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-cn-richtext-editor
2 |
3 | > ## Deprecated. Use [react-native-cn-quill](https://github.com/imnapo/react-native-cn-quill#readme) instead.
4 |
5 | Richtext editor for react native
6 |
7 |
8 |
9 | ## Installation
10 |
11 |
12 | #### Install using npm:
13 |
14 | ```
15 | npm i react-native-cn-richtext-editor
16 | ```
17 | #### Install using yarn:
18 |
19 | ```
20 | yarn add react-native-cn-richtext-editor
21 | ```
22 |
23 | ### Usage
24 |
25 | Here is a simple overview of our components usage.
26 |
27 | ``` jsx
28 | import React, { Component } from 'react';
29 | import { View, StyleSheet, Keyboard
30 | , TouchableWithoutFeedback, Text
31 | , KeyboardAvoidingView } from 'react-native';
32 |
33 | import CNRichTextEditor , { CNToolbar, getInitialObject , getDefaultStyles } from "react-native-cn-richtext-editor";
34 |
35 | const defaultStyles = getDefaultStyles();
36 |
37 | class App extends Component {
38 |
39 | constructor(props) {
40 | super(props);
41 |
42 | this.state = {
43 | selectedTag : 'body',
44 | selectedStyles : [],
45 | value: [getInitialObject()]
46 | };
47 |
48 | this.editor = null;
49 | }
50 |
51 | onStyleKeyPress = (toolType) => {
52 | this.editor.applyToolbar(toolType);
53 | }
54 |
55 | onSelectedTagChanged = (tag) => {
56 | this.setState({
57 | selectedTag: tag
58 | })
59 | }
60 |
61 | onSelectedStyleChanged = (styles) => {
62 | this.setState({
63 | selectedStyles: styles,
64 | })
65 | }
66 |
67 | onValueChanged = (value) => {
68 | this.setState({
69 | value: value
70 | });
71 | }
72 |
73 |
74 | render() {
75 | return (
76 |
88 |
89 |
90 | this.editor = input}
92 | onSelectedTagChanged={this.onSelectedTagChanged}
93 | onSelectedStyleChanged={this.onSelectedStyleChanged}
94 | value={this.state.value}
95 | style={{ backgroundColor : '#fff'}}
96 | styleList={defaultStyles}
97 | onValueChanged={this.onValueChanged}
98 | />
99 |
100 |
101 |
102 |
105 |
106 |
123 | image
124 |
125 | }]
126 | },
127 | {
128 | type: 'tool',
129 | iconArray: [{
130 | toolTypeText: 'bold',
131 | buttonTypes: 'style',
132 | iconComponent:
133 |
134 | bold
135 |
136 | }]
137 | },
138 | {
139 | type: 'seperator'
140 | },
141 | {
142 | type: 'tool',
143 | iconArray: [
144 | {
145 | toolTypeText: 'body',
146 | buttonTypes: 'tag',
147 | iconComponent:
148 |
149 | body
150 |
151 | },
152 | ]
153 | },
154 | {
155 | type: 'tool',
156 | iconArray: [
157 | {
158 | toolTypeText: 'ul',
159 | buttonTypes: 'tag',
160 | iconComponent:
161 |
162 | ul
163 |
164 | }
165 | ]
166 | },
167 | {
168 | type: 'tool',
169 | iconArray: [
170 | {
171 | toolTypeText: 'ol',
172 | buttonTypes: 'tag',
173 | iconComponent:
174 |
175 | ol
176 |
177 | }
178 | ]
179 | },
180 | ]}
181 | selectedTag={this.state.selectedTag}
182 | selectedStyles={this.state.selectedStyles}
183 | onStyleKeyPress={this.onStyleKeyPress}
184 | />
185 |
186 |
187 | );
188 | }
189 |
190 | }
191 |
192 | var styles = StyleSheet.create({
193 | main: {
194 | flex: 1,
195 | marginTop: 10,
196 | paddingLeft: 30,
197 | paddingRight: 30,
198 | paddingBottom: 1,
199 | alignItems: 'stretch',
200 | },
201 | toolbarButton: {
202 | fontSize: 20,
203 | width: 28,
204 | height: 28,
205 | textAlign: 'center'
206 | },
207 | italicButton: {
208 | fontStyle: 'italic'
209 | },
210 | boldButton: {
211 | fontWeight: 'bold'
212 | },
213 | underlineButton: {
214 | textDecorationLine: 'underline'
215 | },
216 | lineThroughButton: {
217 | textDecorationLine: 'line-through'
218 | },
219 | });
220 |
221 |
222 | export default App;
223 |
224 | ```
225 |
226 | ## More Advanced TextEditor
227 | You need to put more effort :) to use more advanced features of CNRichTextEditor such as:
228 | - Image Uploading
229 | - Highlighting Text
230 | - Change Text Color
231 |
232 | Actually we did not implement 'Toolbar buttons and menus' and 'Image Uploading Process' because it totally depends on using expo or pure react-native and also what other packages you prefer to use.
233 |
234 | To see an example of how to implement more advanced feature of this editor please check this [Link](https://github.com/imnapo/react-native-cn-richtext-editor/blob/master/expo-demo/App.js).
235 |
236 | Also be noticed that this example is writen with expo and required 'react-native-popup-menu' package.
237 |
238 | ## API
239 |
240 | ### CNRichTextEditor
241 |
242 | #### Props
243 |
244 | | Name | Description | Required |
245 | | ------ | ----------- | ---- |
246 | | onSelectedTagChanged | this event triggers when selected tag of editor is changed. | No |
247 | | onSelectedStyleChanged | this event triggers when selected style of editor is changed. | No |
248 | | onValueChanged | this event triggers when value of editor is changed. | No |
249 | | onRemoveImage | this event triggers when an image is removed. Callback param in the form `{ url, id }`. | No |
250 | | value | an array object which keeps value of the editor | Yes |
251 | | styleList | an object consist of styles name and values (use getDefaultStyles function) | Yes |
252 | | ImageComponent | a React component (class or functional) which will be used to render images. Will be passed `style` and `source` props. | No |
253 | | style | Styles applied to the outermost component. | No |
254 | | textInputStyle | TextInput style | No |
255 | | contentContainerStyle | Styles applied to the scrollview content. | No |
256 | | onFocus | Callback that is called when one of text inputs are focused. | No |
257 | | onBlur | Callback that is called when one of text inputs are blurred. | No |
258 | | placeholder | The string that will be rendered before text input has been entered. | No |
259 | | textInputProps | An object containing additional props to be passed to the TextInput component| No |
260 |
261 | #### Instance methods
262 |
263 | | Name | Params | Description |
264 | | ------ | ---- | ----------- |
265 | | applyToolbar | `toolType` | Apply the given transformation to selected text. |
266 | | insertImage | `uri, id?, height?, width?` | Insert the provided image where cursor is positionned. |
267 | | focus | | Focus to the last `TextInput` |
268 |
269 | ### CNToolbar
270 |
271 | #### Props
272 |
273 | | Name | Required | Description |
274 | | ------ | ------ | ----------- |
275 | | selectedTag | Yes | selected tag of the editor |
276 | | selectedStyles | Yes | selected style of the editor |
277 | | onStyleKeyPress | Yes | this event triggers when user press one of toolbar keys |
278 | | size | No | font size of toolbar buttons |
279 | | bold | No | a component which renders as bold button (as of 1.0.41, this prop is deprecated) |
280 | | italic | No | a component which renders as italic button (as of 1.0.41, this prop is deprecated) |
281 | | underline | No | a component which renders as underline button (as of 1.0.41, this prop is deprecated) |
282 | | lineThrough | No | a component which renders as lineThrough button (as of 1.0.41, this prop is deprecated) |
283 | | body | No | a component which renders as body button (as of 1.0.41, this prop is deprecated) |
284 | | title | No | a component which renders as title button (as of 1.0.41, this prop is deprecated) |
285 | | ul | No | a component which renders as ul button (as of 1.0.41, this prop is deprecated) |
286 | | ol | No | a component which renders as ol button (as of 1.0.41, this prop is deprecated) |
287 | | image | No | a component which renders as image button (as of 1.0.41, this prop is deprecated) |
288 | | highlight | No | a component which renders as highlight button (as of 1.0.41, this prop is deprecated) |
289 | | foreColor | No | a component which renders as foreColor button (as of 1.0.41, this prop is deprecated) |
290 | | style | No | style applied to container |
291 | | color | No | default color passed to icon |
292 | | backgroundColor | No | default background color passed to icon |
293 | | selectedColor | No | color applied when icon is selected |
294 | | selectedBackgroundColor | No | background color applied when icon is selected |
295 | | iconContainerStyle | No | a style prop assigned to icon container |
296 | | iconSet | Yes | array of icons to display |
297 | | iconSetContainerStyle | No | a style props assigned to icon set container|
298 |
299 | ### CNRichTextView
300 |
301 | #### Props
302 |
303 | | Name | Required | Description |
304 | | ------ | ------ | ----------- |
305 | | text | Yes | html string (created by convertToHtmlString function |
306 | | style | No | style applied to container (req. {flex:1}) |
307 | | styleList | No | an object consist of styles name and values (use getDefaultStyles function) |
308 |
309 | ### Functions
310 |
311 | | Name | Param | Returns | Description |
312 | | ------ | ------ | ------ |----------- |
313 | | getInitialObject | - | javascript object | create a initial value for the editor. |
314 | | convertToHtmlString | array | string | this function converts value of editor to html string (use it to keep value as html in db) |
315 | | convertToObject | string | array | converts html back to array for RichTextEditor value (use this function only for html string created by convertToHtmlString function) |
316 | | getDefaultStyles | - | javascript object | creates required styles for the editor. |
317 |
318 | ## Expo Demo App
319 |
320 | Checkout the
321 | [expo-demo App](https://expo.io/@imnapo/expo-demo)
322 | on Expo which uses react-native-cn-richtext-editor components.
323 | If you are looking to test and run expo-demo App locally, click
324 | [here](https://github.com/imnapo/react-native-cn-richtext-editor/tree/master/expo-demo) to
325 | view the implementation & run it locally.
326 |
327 | ## License
328 |
329 | [MIT](https://github.com/imnapo/react-native-cn-richtext-editor/blob/master/LICENSE)
330 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const presets = [
2 | [
3 | '@babel/env',
4 | {
5 | targets: {
6 | edge: '17',
7 | firefox: '60',
8 | chrome: '67',
9 | safari: '11.1',
10 | },
11 | useBuiltIns: 'usage',
12 | },
13 | ],
14 | ];
15 |
16 | module.exports = { presets };
17 |
--------------------------------------------------------------------------------
/expo-demo/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p12
6 | *.key
7 | *.mobileprovision
8 |
--------------------------------------------------------------------------------
/expo-demo/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/expo-demo/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View, StyleSheet, Keyboard
3 | , TouchableWithoutFeedback, Text, Dimensions
4 | , KeyboardAvoidingView, Platform } from 'react-native';
5 | import { Permissions, ImagePicker } from 'expo';
6 | import { MaterialCommunityIcons } from '@expo/vector-icons';
7 | import CNRichTextEditor , { CNToolbar , getDefaultStyles, convertToObject } from "react-native-cn-richtext-editor";
8 |
9 | import {
10 | Menu,
11 | MenuOptions,
12 | MenuOption,
13 | MenuTrigger,
14 | MenuContext,
15 | MenuProvider,
16 | renderers
17 | } from 'react-native-popup-menu';
18 |
19 | const { SlideInMenu } = renderers;
20 |
21 | const IS_IOS = Platform.OS === 'ios';
22 | const { width, height } = Dimensions.get('window');
23 | const defaultStyles = getDefaultStyles();
24 |
25 | class App extends Component {
26 |
27 | constructor(props) {
28 | super(props);
29 | this.customStyles = {...defaultStyles, body: {fontSize: 12}, heading : {fontSize: 16}
30 | , title : {fontSize: 20}, ol : {fontSize: 12 }, ul: {fontSize: 12}, bold: {fontSize: 12, fontWeight: 'bold', color: ''}
31 | };
32 | this.state = {
33 | selectedTag : 'body',
34 | selectedColor : 'default',
35 | selectedHighlight: 'default',
36 | colors : ['red', 'green', 'blue'],
37 | highlights:['yellow_hl','pink_hl', 'orange_hl', 'green_hl','purple_hl','blue_hl'],
38 | selectedStyles : [],
39 | // value: [getInitialObject()] get empty editor
40 | value: convertToObject('
This is bold and italic text
'
41 | , this.customStyles)
42 | };
43 |
44 | this.editor = null;
45 |
46 | }
47 |
48 | onStyleKeyPress = (toolType) => {
49 |
50 | if (toolType == 'image') {
51 | return;
52 | }
53 | else {
54 | this.editor.applyToolbar(toolType);
55 | }
56 |
57 | }
58 |
59 | onSelectedTagChanged = (tag) => {
60 |
61 | this.setState({
62 | selectedTag: tag
63 | })
64 | }
65 |
66 | onSelectedStyleChanged = (styles) => {
67 | const colors = this.state.colors;
68 | const highlights = this.state.highlights;
69 | let sel = styles.filter(x=> colors.indexOf(x) >= 0);
70 |
71 | let hl = styles.filter(x=> highlights.indexOf(x) >= 0);
72 | this.setState({
73 | selectedStyles: styles,
74 | selectedColor : (sel.length > 0) ? sel[sel.length - 1] : 'default',
75 | selectedHighlight : (hl.length > 0) ? hl[hl.length - 1] : 'default',
76 | })
77 |
78 | }
79 |
80 | onValueChanged = (value) => {
81 | this.setState({
82 | value: value
83 | });
84 | }
85 |
86 | insertImage(url) {
87 |
88 | this.editor.insertImage(url);
89 | }
90 |
91 | askPermissionsAsync = async () => {
92 | const camera = await Permissions.askAsync(Permissions.CAMERA);
93 | const cameraRoll = await Permissions.askAsync(Permissions.CAMERA_ROLL);
94 |
95 | this.setState({
96 | hasCameraPermission: camera.status === 'granted',
97 | hasCameraRollPermission: cameraRoll.status === 'granted'
98 | });
99 | };
100 |
101 | useLibraryHandler = async () => {
102 | await this.askPermissionsAsync();
103 | let result = await ImagePicker.launchImageLibraryAsync({
104 | allowsEditing: true,
105 | aspect: [4, 4],
106 | base64: false,
107 | });
108 |
109 | this.insertImage(result.uri);
110 | };
111 |
112 | useCameraHandler = async () => {
113 | await this.askPermissionsAsync();
114 | let result = await ImagePicker.launchCameraAsync({
115 | allowsEditing: true,
116 | aspect: [4, 4],
117 | base64: false,
118 | });
119 | console.log(result);
120 |
121 | this.insertImage(result.uri);
122 | };
123 |
124 | onImageSelectorClicked = (value) => {
125 | if(value == 1) {
126 | this.useCameraHandler();
127 | }
128 | else if(value == 2) {
129 | this.useLibraryHandler();
130 | }
131 |
132 | }
133 |
134 | onColorSelectorClicked = (value) => {
135 |
136 | if(value === 'default') {
137 | this.editor.applyToolbar(this.state.selectedColor);
138 | }
139 | else {
140 | this.editor.applyToolbar(value);
141 |
142 | }
143 |
144 | this.setState({
145 | selectedColor: value
146 | });
147 | }
148 |
149 | onHighlightSelectorClicked = (value) => {
150 | if(value === 'default') {
151 | this.editor.applyToolbar(this.state.selectedHighlight);
152 | }
153 | else {
154 | this.editor.applyToolbar(value);
155 |
156 | }
157 |
158 | this.setState({
159 | selectedHighlight: value
160 | });
161 | }
162 |
163 | onRemoveImage = ({url, id}) => {
164 | // do what you have to do after removing an image
165 | console.log(`image removed (url : ${url})`);
166 |
167 | }
168 |
169 | renderImageSelector() {
170 | return (
171 |
195 | );
196 |
197 | }
198 |
199 | renderColorMenuOptions = () => {
200 |
201 | let lst = [];
202 |
203 | if(defaultStyles[this.state.selectedColor]) {
204 | lst = this.state.colors.filter(x => x !== this.state.selectedColor);
205 | lst.push('default');
206 | lst.push(this.state.selectedColor);
207 | }
208 | else {
209 | lst = this.state.colors.filter(x=> true);
210 | lst.push('default');
211 | }
212 |
213 | return (
214 |
215 | lst.map( (item) => {
216 | let color = defaultStyles[item] ? defaultStyles[item].color : 'black';
217 | return (
218 |
219 |
221 |
222 | );
223 | })
224 |
225 | );
226 | }
227 |
228 | renderHighlightMenuOptions = () => {
229 | let lst = [];
230 |
231 | if(defaultStyles[this.state.selectedHighlight]) {
232 | lst = this.state.highlights.filter(x => x !== this.state.selectedHighlight);
233 | lst.push('default');
234 | lst.push(this.state.selectedHighlight);
235 | }
236 | else {
237 | lst = this.state.highlights.filter(x=> true);
238 | lst.push('default');
239 | }
240 |
241 |
242 |
243 | return (
244 |
245 | lst.map( (item) => {
246 | let bgColor = defaultStyles[item] ? defaultStyles[item].backgroundColor : 'black';
247 | return (
248 |
249 |
251 |
252 | );
253 | })
254 |
255 | );
256 | }
257 |
258 | renderColorSelector() {
259 |
260 | let selectedColor = '#737373';
261 | if(defaultStyles[this.state.selectedColor])
262 | {
263 | selectedColor = defaultStyles[this.state.selectedColor].color;
264 | }
265 |
266 |
267 | return (
268 |
279 | );
280 | }
281 |
282 | renderHighlight() {
283 | let selectedColor = '#737373';
284 | if(defaultStyles[this.state.selectedHighlight])
285 | {
286 | selectedColor = defaultStyles[this.state.selectedHighlight].backgroundColor;
287 | }
288 | return (
289 |
299 | );
300 | }
301 |
302 | render() {
303 |
304 |
305 | return (
306 |
312 |
313 |
314 |
315 | this.editor = input}
317 | onSelectedTagChanged={this.onSelectedTagChanged}
318 | onSelectedStyleChanged={this.onSelectedStyleChanged}
319 | value={this.state.value}
320 | style={styles.editor}
321 | styleList={this.customStyles}
322 | foreColor='dimgray' // optional (will override default fore-color)
323 | onValueChanged={this.onValueChanged}
324 | onRemoveImage={this.onRemoveImage}
325 | />
326 |
327 |
328 |
329 |
330 |
331 |
348 | },
349 | {
350 | toolTypeText: 'italic',
351 | buttonTypes: 'style',
352 | iconComponent:
353 | },
354 | {
355 | toolTypeText: 'underline',
356 | buttonTypes: 'style',
357 | iconComponent:
358 | },
359 | {
360 | toolTypeText: 'lineThrough',
361 | buttonTypes: 'style',
362 | iconComponent:
363 | }
364 | ]
365 | },
366 | {
367 | type: 'seperator'
368 | },
369 | {
370 | type: 'tool',
371 | iconArray: [
372 | {
373 | toolTypeText: 'body',
374 | buttonTypes: 'tag',
375 | iconComponent:
376 |
377 | },
378 | {
379 | toolTypeText: 'title',
380 | buttonTypes: 'tag',
381 | iconComponent:
382 |
383 | },
384 | {
385 | toolTypeText: 'heading',
386 | buttonTypes: 'tag',
387 | iconComponent:
388 |
389 | },
390 | {
391 | toolTypeText: 'ul',
392 | buttonTypes: 'tag',
393 | iconComponent:
394 |
395 | },
396 | {
397 | toolTypeText: 'ol',
398 | buttonTypes: 'tag',
399 | iconComponent:
400 |
401 | }
402 | ]
403 | },
404 | {
405 | type: 'seperator'
406 | },
407 | {
408 | type: 'tool',
409 | iconArray: [
410 | {
411 | toolTypeText: 'image',
412 | iconComponent: this.renderImageSelector()
413 | },
414 | {
415 | toolTypeText: 'color',
416 | iconComponent: this.renderColorSelector()
417 | },
418 | {
419 | toolTypeText: 'highlight',
420 | iconComponent: this.renderHighlight()
421 | }]
422 | },
423 |
424 | ]}
425 | selectedTag={this.state.selectedTag}
426 | selectedStyles={this.state.selectedStyles}
427 | onStyleKeyPress={this.onStyleKeyPress}
428 | backgroundColor="aliceblue" // optional (will override default backgroundColor)
429 | color="gray" // optional (will override default color)
430 | selectedColor='white' // optional (will override default selectedColor)
431 | selectedBackgroundColor='deepskyblue' // optional (will override default selectedBackgroundColor)
432 | />
433 |
434 |
435 |
436 | );
437 | }
438 |
439 | }
440 |
441 | var styles = StyleSheet.create({
442 | root: {
443 | flex: 1,
444 | paddingTop: 20,
445 | backgroundColor:'#eee',
446 | flexDirection: 'column',
447 | justifyContent: 'flex-end',
448 | },
449 | main: {
450 | flex: 1,
451 | marginTop: 10,
452 | paddingLeft: 30,
453 | paddingRight: 30,
454 | paddingBottom: 1,
455 | alignItems: 'stretch',
456 | },
457 | editor: {
458 | backgroundColor : '#fff'
459 | },
460 | toolbarContainer: {
461 | minHeight: 35
462 | },
463 | menuOptionText: {
464 | textAlign: 'center',
465 | paddingTop: 5,
466 | paddingBottom: 5
467 | },
468 | divider: {
469 | marginVertical: 0,
470 | marginHorizontal: 0,
471 | borderBottomWidth: 1,
472 | borderColor: '#eee'
473 | }
474 | });
475 |
476 | const optionsStyles = {
477 | optionsContainer: {
478 | backgroundColor: 'yellow',
479 | padding: 0,
480 | width: 40,
481 | marginLeft: width - 40 - 30,
482 | alignItems: 'flex-end',
483 | },
484 | optionsWrapper: {
485 | //width: 40,
486 | backgroundColor: 'white',
487 | },
488 | optionWrapper: {
489 | //backgroundColor: 'yellow',
490 | margin: 2,
491 | },
492 | optionTouchable: {
493 | underlayColor: 'gold',
494 | activeOpacity: 70,
495 | },
496 | // optionText: {
497 | // color: 'brown',
498 | // },
499 | };
500 |
501 | const highlightOptionsStyles = {
502 | optionsContainer: {
503 | backgroundColor: 'transparent',
504 | padding: 0,
505 | width: 40,
506 | marginLeft: width - 40,
507 |
508 | alignItems: 'flex-end',
509 | },
510 | optionsWrapper: {
511 | //width: 40,
512 | backgroundColor: 'white',
513 | },
514 | optionWrapper: {
515 | //backgroundColor: 'yellow',
516 | margin: 2,
517 | },
518 | optionTouchable: {
519 | underlayColor: 'gold',
520 | activeOpacity: 70,
521 | },
522 | // optionText: {
523 | // color: 'brown',
524 | // },
525 | };
526 |
527 | export default App;
528 |
--------------------------------------------------------------------------------
/expo-demo/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "expo-demo",
4 | "description": "Demo Application for react-native-cn-richtext-editor",
5 | "slug": "expo-demo",
6 | "privacy": "public",
7 | "sdkVersion": "33.0.0",
8 | "platforms": [
9 | "ios",
10 | "android"
11 | ],
12 | "version": "1.0.11",
13 | "orientation": "portrait",
14 | "icon": "./assets/icon.png",
15 | "splash": {
16 | "image": "./assets/splash.png",
17 | "resizeMode": "contain",
18 | "backgroundColor": "#ffffff"
19 | },
20 | "updates": {
21 | "fallbackToCacheTimeout": 0
22 | },
23 | "assetBundlePatterns": [
24 | "**/*"
25 | ],
26 | "ios": {
27 | "supportsTablet": true
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/expo-demo/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imnapo/react-native-cn-richtext-editor/47ea96ecc8a36244b2407f42eb60d47bdd195893/expo-demo/assets/icon.png
--------------------------------------------------------------------------------
/expo-demo/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imnapo/react-native-cn-richtext-editor/47ea96ecc8a36244b2407f42eb60d47bdd195893/expo-demo/assets/splash.png
--------------------------------------------------------------------------------
/expo-demo/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/expo-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "eject": "expo eject"
8 | },
9 | "dependencies": {
10 | "expo": "^33.0.0",
11 | "lodash": "^4.17.15",
12 | "react": "16.8.3",
13 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz",
14 | "react-native-cn-richtext-editor": "^1.0.42",
15 | "react-native-popup-menu": "^0.15.6"
16 | },
17 | "devDependencies": {
18 | "babel-preset-expo": "^5.0.0"
19 | },
20 | "private": true
21 | }
22 |
--------------------------------------------------------------------------------
/images/demo-img.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imnapo/react-native-cn-richtext-editor/47ea96ecc8a36244b2407f42eb60d47bdd195893/images/demo-img.jpg
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | export as namespace CNRichTextEditor;
2 |
3 | import { Component, ReactNode } from "react";
4 | import { StyleProp, StyleSheet, TextStyle, ViewStyle } from "react-native";
5 |
6 | export interface CNRichTextEditorProps {
7 | onSelectedTagChanged?: (tag: string) => void;
8 | onSelectedStyleChanged?: (styles: string[]) => void;
9 | onValueChanged?: (value: object[]) => void;
10 | onRemoveImage?: (url: string, id: string) => void;
11 | value: ReturnType;
12 | styleList: any;
13 | ImageComponent?: React.ReactElement;
14 | style?: StyleProp;
15 | contentContainerStyle?: StyleProp;
16 | onFocus?: () => void;
17 | onBlur?: () => void;
18 | placeholder: string;
19 | textInputStyle?: StyleProp;
20 | }
21 |
22 | export default class CNRichTextEditor extends Component {
23 | applyToolbar(toolType: any): void;
24 | insertImage(uri: any, id?: any, height?: number, width?: number): void;
25 | focus(): void;
26 | }
27 |
28 | export interface CNToolbarProps {
29 | selectedTag: string;
30 | selectedStyles: string[];
31 | onStyleKeyPress: (toolType: any) => void;
32 | size?: number;
33 | iconSet?:any[];
34 | iconSetContainerStyle: StyleProp;
35 | style?: StyleProp;
36 | color?: string;
37 | backgroundColor?: string;
38 | selectedBackgroundColor?: string;
39 | iconContainerStyle?: StyleProp;
40 | }
41 |
42 | export class CNToolbar extends Component {}
43 |
44 | export interface CNRichTextViewProps {
45 | text: string;
46 | style?: StyleProp;
47 | styleList: ReturnType;
48 | }
49 |
50 | export class CNRichTextView extends Component {}
51 |
52 | export function getInitialObject(): any;
53 | export function convertToHtmlString(html: object[]): string;
54 | export function convertToObject(html: string): object[];
55 | export function getDefaultStyles(): ReturnType;
56 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import CNRichTextEditor from './src/CNRichTextEditor';
2 | import CNRichTextView from './src/CNRichTextView';
3 | import CNToolbar from './src/CNToolbar';
4 | import { convertToObject, convertToHtmlString, getInitialObject, getDefaultStyles} from './src/Convertors';
5 |
6 | export {CNRichTextEditor as default, CNRichTextEditor, CNToolbar, CNRichTextView};
7 | export {convertToHtmlString, convertToObject, getInitialObject, getDefaultStyles};
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-cn-richtext-editor",
3 | "version": "1.0.43",
4 | "description": "RichText Editor for React-Native",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "lint": "eslint src"
9 | },
10 | "dependencies": {
11 | "diff-match-patch": "^1.0.4",
12 | "immutability-helper": "^2.8.1",
13 | "lodash": "^4.17.15",
14 | "shortid": "^2.2.14",
15 | "xmldom": "^0.1.27"
16 | },
17 | "peerDependencies": {
18 | "react": "^16.6.1",
19 | "react-native": "^0.57.5"
20 | },
21 | "keywords": [
22 | "react",
23 | "react-native",
24 | "rich-text",
25 | "editor"
26 | ],
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/imnapo/react-native-cn-richtext-editor.git"
30 | },
31 | "contributors": [
32 | "Christ Kho (http://learncode.net)",
33 | "Narbe HS (http://learncode.net)"
34 | ],
35 | "author": "Narbe HS (http://learncode.net)",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/imnapo/react-native-cn-richtext-editor/issues"
39 | },
40 | "homepage": "https://github.com/imnapo/react-native-cn-richtext-editor#readme",
41 | "devDependencies": {
42 | "@babel/cli": "^7.2.3",
43 | "@babel/core": "^7.4.0",
44 | "@babel/preset-env": "^7.4.2",
45 | "babel-eslint": "^10.0.1",
46 | "eslint": "^5.15.3",
47 | "eslint-config-airbnb": "^17.1.0",
48 | "eslint-plugin-import": "^2.16.0",
49 | "eslint-plugin-jsx-a11y": "^6.2.1",
50 | "eslint-plugin-react": "^7.12.4"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/CNRichTextEditor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {
3 | TextInput, View, Image,
4 | ScrollView, Platform,
5 | TouchableWithoutFeedback,
6 | } from 'react-native';
7 | import _ from 'lodash';
8 | import update from 'immutability-helper';
9 | import { getInitialObject, getDefaultStyles } from './Convertors';
10 | import CNTextInput from './CNTextInput';
11 |
12 | const shortid = require('shortid');
13 |
14 | const IS_IOS = Platform.OS === 'ios';
15 |
16 | class CNRichTextEditor extends Component {
17 | state = {
18 | imageHighLightedInex: -1,
19 | layoutWidth: 400,
20 | styles: [],
21 | selection: { start: 0, end: 0 },
22 | justToolAdded: false,
23 | avoidUpdateText: false,
24 | focusInputIndex: 0,
25 | measureContent: [],
26 | };
27 |
28 | constructor(props) {
29 | super(props);
30 | this.textInputs = [];
31 | this.scrollview = null;
32 | this.prevSelection = { start: 0, end: 0 };
33 | this.beforePrevSelection = { start: 0, end: 0 };
34 | this.avoidSelectionChangeOnFocus = false;
35 | this.turnOnJustToolOnFocus = false;
36 | this.contentHeights = [];
37 | this.upComingStype = null;
38 | this.focusOnNextUpdate = -1;
39 | this.selectionOnFocus = null;
40 | this.scrollOffset = 0;
41 | this.defaultStyles = getDefaultStyles();
42 | }
43 |
44 | componentDidUpdate(prevProps, prevState) {
45 | if (this.focusOnNextUpdate != -1 && this.textInputs.length > this.focusOnNextUpdate) {
46 | const ref = this.textInputs[this.focusOnNextUpdate];
47 | if(ref) ref.focus(this.selectionOnFocus);
48 | this.setState({focusInputIndex: this.focusOnNextUpdate});
49 | this.focusOnNextUpdate = -1;
50 | this.selectionOnFocus = null;
51 | }
52 | }
53 |
54 | findContentIndex(content, cursorPosition) {
55 | let indx = 0;
56 | let findIndx = -1;
57 | let checknext = true;
58 | let itemNo = 0;
59 |
60 | for (let index = 0; index < content.length; index++) {
61 | const element = content[index];
62 |
63 | const ending = indx + element.len;
64 |
65 | if (checknext === false) {
66 | if (element.len === 0) {
67 | findIndx = index;
68 | itemNo = 0;
69 | break;
70 | } else {
71 | break;
72 | }
73 | }
74 | if (cursorPosition <= ending && cursorPosition >= indx) {
75 | // element.len += 1;
76 | findIndx = index;
77 | itemNo = cursorPosition - indx;
78 |
79 |
80 | checknext = false;
81 | }
82 |
83 | indx += element.len;
84 | }
85 |
86 | if (findIndx == -1) {
87 | findIndx = content.length - 1;
88 | }
89 |
90 | return { findIndx, itemNo };
91 | }
92 |
93 | updateContent(content, item, index, itemNo = 0) {
94 | let newContent = content;
95 | if (itemNo > 0 && itemNo != 0 && content[index - 1].len != itemNo) {
96 | const foundElement = content[index - 1];
97 | beforeContent = {
98 | id: foundElement.id,
99 | len: itemNo,
100 | stype: foundElement.stype,
101 | styleList: foundElement.styleList,
102 | tag: foundElement.tag,
103 | text: foundElement.text.substring(0, itemNo),
104 | };
105 |
106 | afterContent = {
107 | id: shortid.generate(),
108 | len: foundElement.len - itemNo,
109 | stype: foundElement.stype,
110 | styleList: foundElement.styleList,
111 | tag: foundElement.tag,
112 | text: foundElement.text.substring(itemNo),
113 | };
114 |
115 | newContent = update(newContent, { [index - 1]: { $set: beforeContent } });
116 | newContent = update(newContent, { $splice: [[index, 0, afterContent]] });
117 | }
118 | if (item !== null) {
119 | newContent = update(newContent, { $splice: [[index, 0, item]] });
120 | }
121 |
122 |
123 | return newContent;
124 | }
125 |
126 | onConnectToPrevClicked = (index) => {
127 | const { value } = this.props;
128 |
129 | if (index > 0 && value[index - 1].component == 'image'
130 | ) {
131 | const ref = this.textInputs[index - 1];
132 | ref.focus();
133 | }
134 | }
135 |
136 | handleKeyDown = (e, index) => {
137 | this.avoidUpdateStyle = true;
138 |
139 | const { value } = this.props;
140 |
141 | const item = value[index];
142 | if (item.component === 'image' && e.nativeEvent.key === 'Backspace') {
143 | if (this.state.imageHighLightedInex === index) {
144 | this.removeImage(index);
145 | } else {
146 | this.setState({
147 | imageHighLightedInex: index,
148 | });
149 | }
150 | }
151 | }
152 |
153 | onImageClicked = (index) => {
154 | const ref = this.textInputs[index];
155 | ref.focus();
156 | // this.setState({
157 | // imageHighLightedInex: index
158 | // })
159 | }
160 |
161 | handleOnBlur = (e, index) => {
162 | if(this.props.onBlur)
163 | this.props.onBlur(e,index);
164 | }
165 |
166 | handleOnFocus = (e, index) => {
167 | if (this.state.focusInputIndex === index) {
168 | try {
169 | this.textInputs[index].avoidSelectionChangeOnFocus();
170 | } catch (error) {
171 | // console.log(error);
172 | }
173 |
174 | this.setState({
175 | imageHighLightedInex: -1,
176 | });
177 | } else {
178 | this.setState({
179 | imageHighLightedInex: -1,
180 | focusInputIndex: index,
181 |
182 | }, () => {
183 | this.textInputs[index].forceSelectedStyles();
184 | });
185 | this.avoidSelectionChangeOnFocus = false;
186 | }
187 |
188 | if(this.props.onFocus)
189 | this.props.onFocus(e, index);
190 | }
191 |
192 | focus() {
193 | try {
194 | if (this.textInputs.length > 0) {
195 | const ref = this.textInputs[this.textInputs.length - 1];
196 | ref.focus({
197 | start: 0, // ref.textLength,
198 | end: 0, // ref.textLength
199 | });
200 | }
201 | } catch (error) {
202 | // console.log(error);
203 | }
204 | }
205 |
206 | addImageContent = (url, id, height, width) => {
207 | const { focusInputIndex } = this.state;
208 | const { value } = this.props;
209 | let index = focusInputIndex + 1;
210 |
211 | const myHeight = (this.state.layoutWidth - 4 < width) ? height * ((this.state.layoutWidth - 4) / width) : height;
212 | this.contentHeights[index] = myHeight + 4;
213 |
214 | const item = {
215 | id: shortid.generate(),
216 | imageId: id,
217 | component: 'image',
218 | url,
219 | size: {
220 | height,
221 | width,
222 | },
223 | };
224 |
225 | let newConents = value;
226 | if (newConents[index - 1] && newConents[index - 1].component === 'text') {
227 | const { before, after } = this.textInputs[index - 1].splitItems();
228 |
229 | if (Array.isArray(before) && before.length > 0) {
230 | const beforeContent = {
231 | component: 'text',
232 | id: newConents[index - 1].id,
233 | content: [],
234 | };
235 |
236 | if (before[before.length - 1].text === '\n' && before[before.length - 1].readOnly !== true) {
237 | beforeContent.content = update(before, { $splice: [[before.length - 1, 1]] });
238 | } else {
239 | beforeContent.content = before;
240 | }
241 |
242 | newConents = update(newConents, { [index - 1]: { $set: beforeContent } });
243 |
244 | if (Array.isArray(after) && after.length > 0) {
245 | const afterContent = {
246 | component: 'text',
247 | id: shortid.generate(),
248 | content: [],
249 | };
250 |
251 | if (after[0].text.startsWith('\n')) {
252 | after[0].text = after[0].text.substring(1);
253 | after[0].len = after[0].text.length;
254 | }
255 |
256 | afterContent.content = after;
257 |
258 | newConents = update(newConents, { $splice: [[index, 0, afterContent]] });
259 | this.textInputs[index - 1].reCalculateTextOnUpate = true;
260 | }
261 | } else {
262 | index -= 1;
263 | }
264 | }
265 |
266 | newConents = update(newConents, { $splice: [[index, 0, item]] });
267 |
268 | if (newConents.length === index + 1) {
269 | newConents = update(newConents, { $splice: [[index + 1, 0, getInitialObject()]] });
270 | }
271 |
272 | this.focusOnNextUpdate = index + 1;
273 |
274 | this.props.onValueChanged(
275 | newConents,
276 | );
277 | }
278 |
279 | insertImage(url, id = null, height = null, width = null) {
280 | if (height != null && width != null) {
281 | this.addImageContent(url, id, height, width);
282 | } else {
283 | Image.getSize(url, (width, height) => {
284 | this.addImageContent(url, id, height, width);
285 | });
286 | }
287 | }
288 |
289 | removeImage =(index) => {
290 | const { value } = this.props;
291 | const content = value[index];
292 |
293 |
294 | if (content.component === 'image') {
295 | let newConents = value;
296 | const removedUrl = content.url;
297 | const removedId = content.imageId;
298 |
299 | let selectionStart = 0;
300 | let removeCout = 1;
301 |
302 | if (index > 0
303 | && value[index - 1].component === 'text'
304 | ) {
305 | selectionStart = this.textInputs[index - 1].textLength;
306 | }
307 |
308 | if (value.length > index + 1
309 | && index > 0
310 | && value[index - 1].component === 'text'
311 | && value[index + 1].component === 'text'
312 | ) {
313 | removeCout = 2;
314 |
315 | const prevContent = value[index - 1];
316 | const nextContent = value[index + 1];
317 |
318 | if (this.textInputs[index + 1].textLength > 0
319 | && nextContent.content.length > 0) {
320 | const firstItem = { ...nextContent.content[0] };
321 | firstItem.text = `\n${firstItem.text}`;
322 | firstItem.len = firstItem.text.length;
323 |
324 | nextContent.content = update(nextContent.content, {
325 | $splice: [[0, 1,
326 | firstItem,
327 | ]],
328 | });
329 |
330 |
331 | nextContent.content = update(nextContent.content, {
332 | $splice: [[0, 0,
333 | {
334 | id: shortid.generate(),
335 | len: 1,
336 | text: '\n',
337 | tag: 'body',
338 | stype: [],
339 | styleList: [{
340 | fontSize: 20,
341 | }],
342 | NewLine: true,
343 | },
344 | ]],
345 | });
346 |
347 | prevContent.content = update(prevContent.content, { $push: nextContent.content });
348 |
349 | newConents = update(newConents, { [index - 1]: { $set: prevContent } });
350 | const ref = this.textInputs[index - 1];
351 | ref.reCalculateTextOnUpate = true;
352 | selectionStart += 1;
353 | // ref.textLength = ref.textLength + 2 + this.textInputs[index + 1].textLength;
354 | }
355 | }
356 |
357 | newConents = update(newConents, { $splice: [[index, removeCout]] });
358 |
359 | this.contentHeights = update(this.contentHeights, { $splice: [[index, removeCout]] });
360 |
361 | this.focusOnNextUpdate = Math.max(0, index - 1);
362 | this.selectionOnFocus = { start: selectionStart, end: selectionStart };
363 |
364 | if (this.props.onValueChanged) this.props.onValueChanged(newConents);
365 |
366 | if (this.props.onRemoveImage) {
367 | this.props.onRemoveImage(
368 | { id: removedId, url: removedUrl },
369 | );
370 | }
371 | }
372 | }
373 |
374 | onContentChanged = (items, index) => {
375 | const input = this.props.value[index];
376 | input.content = items;
377 |
378 | this.props.onValueChanged(
379 | update(this.props.value, { [index]: { $set: input } }),
380 | );
381 | }
382 |
383 | onSelectedStyleChanged = (styles) => {
384 | if (this.props.onSelectedStyleChanged) {
385 | this.props.onSelectedStyleChanged(styles);
386 | }
387 | }
388 |
389 | onSelectedTagChanged = (tag) => {
390 | if (this.props.onSelectedTagChanged) {
391 | this.props.onSelectedTagChanged(tag);
392 | }
393 | }
394 |
395 | handleMeasureContentChanged = (content) => {
396 | this.setState({
397 | measureContent: content,
398 | });
399 | }
400 |
401 | onInputLayout = (event, index, isLast) => {
402 | const { height } = event.nativeEvent.layout;
403 | this.contentHeights[index] = height;
404 | }
405 |
406 | renderInput(input, index, isLast, measureScroll = true) {
407 | const styles = this.props.styleList ? this.props.styleList : this.defaultStyles;
408 | return (
409 | this.onInputLayout(e, index, isLast)}
412 | style={{
413 | flexGrow: isLast === true ? 1 : 0,
414 | }}
415 | >
416 | { this.textInputs[index] = input; }}
418 | items={input.content}
419 | onSelectedStyleChanged={this.onSelectedStyleChanged}
420 | onSelectedTagChanged={this.onSelectedTagChanged}
421 | onContentChanged={items => this.onContentChanged(items, index)}
422 | onConnectToPrevClicked={() => this.onConnectToPrevClicked(index)}
423 | onMeasureContentChanged={measureScroll ? this.handleMeasureContentChanged : undefined}
424 | onFocus={(e) => this.handleOnFocus(e, index)}
425 | onBlur={(e)=> this.handleOnBlur(e, index)}
426 | returnKeyType={this.props.returnKeyType}
427 | foreColor={this.props.foreColor}
428 | styleList={styles}
429 | placeholder={index === 0 ? this.props.placeholder : undefined}
430 | textInputProps={this.props.textInputProps}
431 | style={[{
432 | flexGrow: 1,
433 | }, this.props.textInputStyle]
434 | }
435 | />
436 |
437 | );
438 | }
439 |
440 | renderImage(image, index) {
441 | let { width, height } = image.size;
442 | let myHeight, myWidth;
443 |
444 | if(typeof width === 'undefined' && typeof height === 'undefined'){
445 | width = 500;
446 | height = 200;
447 | Image.getSize(image.url, (width, height) => {
448 | width = width;
449 | height = height;
450 | myHeight = (this.state.layoutWidth - 4 < width) ? height * ((this.state.layoutWidth - 4) / width) : height;
451 | myWidth = (this.state.layoutWidth - 4 < width) ? this.state.layoutWidth - 4 : width;
452 | });
453 | }
454 |
455 | myHeight = (this.state.layoutWidth - 4 < width) ? height * ((this.state.layoutWidth - 4) / width) : height;
456 | myWidth = (this.state.layoutWidth - 4 < width) ? this.state.layoutWidth - 4 : width;
457 |
458 | const { ImageComponent = Image } = this.props;
459 | return (
460 |
473 | this.onImageClicked(index)}
475 | >
476 |
484 |
485 | this.handleKeyDown(event, index)}
487 | // onSelectionChange={(event) =>this.onSelectionChange(event, index)}
488 | multiline={false}
489 | style={{
490 | fontSize: myHeight * 0.65,
491 | borderWidth: 0,
492 | paddingBottom: 1,
493 | width: 1,
494 | }}
495 | ref={component => this.textInputs[index] = component}
496 | />
497 |
498 |
499 | );
500 | }
501 |
502 | applyToolbar(toolType) {
503 | const { focusInputIndex } = this.state;
504 |
505 | if (toolType === 'body' || toolType === 'title' || toolType === 'heading' || toolType === 'ul' || toolType === 'ol') {
506 | this.textInputs[focusInputIndex].applyTag(toolType);
507 | } else if (toolType == 'image') {
508 | // convertToHtmlStringconvertToHtmlString(this.state.contents);
509 |
510 | this.setState({ showAddImageModal: true });
511 | } else
512 | // if(toolType === 'bold' || toolType === 'italic' || toolType === 'underline' || toolType === 'lineThrough')
513 | { this.textInputs[focusInputIndex].applyStyle(toolType); }
514 | }
515 |
516 | onLayout = (event) => {
517 | const { width } = event.nativeEvent.layout;
518 |
519 | this.setState({
520 | layoutWidth: width,
521 | });
522 | }
523 |
524 | onRootLayout = (event) => {
525 | const { height } = event.nativeEvent.layout;
526 | const { style } = this.props;
527 | const paddingTop = (style && style.padding) ? style.padding
528 | : (style && style.paddingTop) ? style.paddingTop : 10;
529 | const paddingBottom = (style && style.padding) ? style.padding
530 | : (style && style.paddingBottom) ? style.paddingBottom : 10;
531 |
532 | this._rootHei = height - paddingTop - paddingBottom;
533 | }
534 |
535 | onMeasureLayout = (event) => {
536 | let measureRequiredHei = 0;
537 | for (let i = 0; i < this.state.focusInputIndex; i++) {
538 | measureRequiredHei += this.contentHeights[i];
539 | }
540 | measureRequiredHei += (event.nativeEvent.layout.height);
541 |
542 | const measureOffset = Math.ceil(Math.max(0, measureRequiredHei - this._rootHei));
543 |
544 | if (this._rootHei < measureRequiredHei
545 | && this.scrollOffset < measureOffset
546 | ) {
547 | this.scrollview.scrollTo({ y: measureOffset, animated: false });
548 | }
549 | }
550 |
551 | onScroll = (event) => {
552 | this.scrollOffset = Math.ceil(event.nativeEvent.contentOffset.y);
553 | }
554 |
555 | render() {
556 | const {
557 | value, style, contentContainerStyle, measureInputScroll = true,
558 | } = this.props;
559 | const styleList = this.props.styleList ? this.props.styleList : this.defaultStyles;
560 |
561 | return (
562 |
569 | this.scrollview = view}
571 | onScroll={measureInputScroll && IS_IOS ? this.onScroll : undefined}
572 | scrollEventThrottle={16}
573 | contentContainerStyle={[{
574 | flexGrow: 1,
575 | alignContent: 'flex-start',
576 | justifyContent: 'flex-start',
577 | }, contentContainerStyle]}
578 | >
579 |
587 | {
588 | _.map(value, (item, index) => {
589 | if (item.component === 'text') {
590 | return (
591 | this.renderInput(item, index, index === value.length - 1, measureInputScroll && IS_IOS)
592 | );
593 | }
594 | if (item.component === 'image') {
595 | return (
596 | this.renderImage(item, index)
597 | );
598 | }
599 | })
600 | }
601 | {
602 | // Invisible Input to measure scroll in Ios
603 | measureInputScroll
604 | && (
605 |
615 | { this.measureInput = input; }}
617 | items={this.state.measureContent}
618 | styleList={styleList}
619 | style={{
620 | width: this.state.layoutWidth,
621 | }}
622 | />
623 |
624 | )
625 | }
626 |
627 |
628 |
629 | );
630 | }
631 | }
632 |
633 | export default CNRichTextEditor;
634 |
--------------------------------------------------------------------------------
/src/CNRichTextView.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {
3 | View, Text, Image, TouchableWithoutFeedback,
4 | } from 'react-native';
5 | import _ from 'lodash';
6 | import { convertToObject } from './Convertors';
7 | import CNStyledText from './CNStyledText';
8 |
9 | class CNRichTextView extends Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | contents: [],
14 | layoutWidth: 400,
15 | };
16 | this.touchX = -1;
17 | this.touchY = -1;
18 | this.flip = this.flip.bind(this);
19 | }
20 |
21 | componentDidMount() {
22 | const { text, styleList} = this.props;
23 | const styles = styleList ? styleList : null;
24 |
25 | const items = convertToObject(text, styles);
26 |
27 | this.setState({
28 | contents: items,
29 | });
30 | }
31 |
32 | componentDidUpdate(prevProps, prevState) {
33 | const { text, styleList} = this.props;
34 |
35 | if (prevProps.text != text) {
36 | const styles = styleList ? styleList : null;
37 | const items = convertToObject(text, styles);
38 |
39 | this.setState({
40 | contents: items,
41 | });
42 | }
43 | }
44 |
45 | flip() {
46 | if (this.props.onTap) {
47 | this.props.onTap();
48 | }
49 | }
50 |
51 | renderText(input, index) {
52 | const color = this.props.color ? this.props.color : '#000';
53 |
54 | return (
55 |
62 | {
63 | _.map(input.content, item => (
64 |
65 | ))
66 |
67 | }
68 |
69 | );
70 | }
71 |
72 | renderImage(image, index) {
73 | const { width, height } = image.size;
74 | const { layoutWidth } = this.state;
75 | const { ImageComponent = Image } = this.props;
76 | const myHeight = (layoutWidth - 4 < width) ? height * ((layoutWidth - 4) / width) : height;
77 | const myWidth = (layoutWidth - 4 < width) ? layoutWidth - 4 : width;
78 |
79 | return (
80 |
87 |
88 |
96 |
97 |
98 |
99 | );
100 | }
101 |
102 | onLayout = (event) => {
103 | const {
104 | x,
105 | y,
106 | width,
107 | height,
108 | } = event.nativeEvent.layout;
109 |
110 | this.setState({
111 | layoutWidth: width - 2,
112 | });
113 | }
114 |
115 | render() {
116 | const { contents } = this.state;
117 | const { style } = this.props;
118 |
119 | const styles = style || {};
120 | return (
121 | {
125 | this.touchX = evt.nativeEvent.pageX;
126 | this.touchY = evt.nativeEvent.pageY;
127 | return true;
128 | }}
129 | onResponderRelease={(evt) => {
130 | if(Math.abs(evt.nativeEvent.pageX - this.touchX) < 25
131 | && Math.abs(evt.nativeEvent.pageY - this.touchY) < 25
132 | ) {
133 | setTimeout(this.flip, 50);
134 | }
135 |
136 | this.touchX = -1;
137 | this.touchY = -1;
138 |
139 | }}
140 | >
141 | {
142 | _.map(contents, (item, index) => {
143 | if (item.component === 'text') {
144 | return (
145 | this.renderText(item, index)
146 | );
147 | }
148 | if (item.component === 'image') {
149 | return (
150 | this.renderImage(item, index)
151 | );
152 | }
153 | })
154 | }
155 |
156 | );
157 | }
158 | }
159 |
160 | export default CNRichTextView;
161 |
--------------------------------------------------------------------------------
/src/CNSeperator.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {
3 | View,
4 | TouchableWithoutFeedback,
5 | TouchableHighlight,
6 | Text,
7 | StyleSheet
8 | } from 'react-native'
9 |
10 | const defaultColor = '#737373'
11 |
12 | export const CNSeperator = (props) => {
13 | return (
14 |
15 | )
16 | }
17 |
18 | const styles = StyleSheet.create({
19 | separator: {
20 | width: 2,
21 | marginTop: 1,
22 | marginBottom: 1,
23 | },
24 | });
25 |
--------------------------------------------------------------------------------
/src/CNStyledText.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Text, StyleSheet } from 'react-native';
3 | import _ from 'lodash';
4 |
5 | class CNStyledText extends Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 |
11 | shouldComponentUpdate(nextProps) {
12 | if (_.isEqual(this.props.text, nextProps.text)
13 | && _.isEqual(this.props.style, nextProps.style)
14 |
15 | ) {
16 | return false;
17 | }
18 |
19 |
20 | return true;
21 | }
22 |
23 | render() {
24 | return (
25 |
26 | {this.props.text}
27 |
28 | );
29 | }
30 | }
31 |
32 | export default CNStyledText;
33 |
--------------------------------------------------------------------------------
/src/CNTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { TextInput, StyleSheet, Platform } from 'react-native';
3 | import _ from 'lodash';
4 | import update from 'immutability-helper';
5 | import DiffMatchPatch from 'diff-match-patch';
6 | import CNStyledText from './CNStyledText';
7 |
8 | const shortid = require('shortid');
9 |
10 | const IS_IOS = Platform.OS == 'ios';
11 |
12 |
13 | class CNTextInput extends Component {
14 | constructor(props) {
15 | super(props);
16 | this.textInput = React.createRef();
17 | this.prevSelection = { start: 0, end: 0 };
18 | this.beforePrevSelection = { start: 0, end: 0 };
19 | this.avoidSelectionChangeOnFocus = false;
20 | this.turnOnJustToolOnFocus = false;
21 | this.textLength = 0;
22 | this.upComingStype = null;
23 | this.androidSelectionJump = 0;
24 |
25 | this.AvoidAndroidIssueWhenPressSpace = 0;
26 | this.checkKeyPressAndroid = 0;
27 |
28 | this.avoidAndroidIssueWhenPressSpaceText = '';
29 | this.justToolAdded = false;
30 | this.state = {
31 | selectedTag: 'body',
32 | selection: { start: 0, end: 0 },
33 | avoidUpdateText: false,
34 | };
35 |
36 | this.dmp = new DiffMatchPatch();
37 | this.oldText = '';
38 | this.reCalculateTextOnUpate = false;
39 | // You can also use the following properties:
40 | DiffMatchPatch.DIFF_DELETE = -1;
41 | DiffMatchPatch.DIFF_INSERT = 1;
42 | DiffMatchPatch.DIFF_EQUAL = 0;
43 | }
44 |
45 | UNSAFE_componentWillMount() {
46 | const { items } = this.props;
47 | if(items && Array.isArray(items) === true) {
48 | let content = items;
49 | for (let i = 0; i < content.length; i++) {
50 | content[i].styleList = StyleSheet.flatten(this.convertStyleList(content[i].stype));
51 | }
52 | if (this.props.onContentChanged) {
53 | this.props.onContentChanged(content);
54 | }
55 | }
56 | }
57 |
58 | componentDidMount() {
59 | if (this.props.items) {
60 | this.textLength = 0;
61 | // for (let index = 0; index < this.props.items.length; index++) {
62 | // const element = this.props.items[index];
63 | // this.textLength += element.text.length;
64 | // }
65 | this.oldText = this.reCalculateText(this.props.items);
66 | this.textLength = this.oldText.length;
67 | }
68 | }
69 |
70 | componentDidUpdate(prevProps, prevState) {
71 | if (this.reCalculateTextOnUpate === true) {
72 | this.oldText = this.reCalculateText(this.props.items);
73 | this.textLength = this.oldText.length;
74 | this.reCalculateTextOnUpate = false;
75 | }
76 | }
77 |
78 | findContentIndex(content, cursorPosition) {
79 | let indx = 0;
80 | let findIndx = -1;
81 | let checknext = true;
82 | let itemNo = 0;
83 |
84 | for (let index = 0; index < content.length; index++) {
85 | const element = content[index];
86 |
87 | const ending = indx + element.len;
88 |
89 | if (checknext === false) {
90 | if (element.len === 0) {
91 | findIndx = index;
92 | itemNo = 0;
93 | break;
94 | } else {
95 | break;
96 | }
97 | }
98 | if (cursorPosition <= ending && cursorPosition >= indx) {
99 | findIndx = index;
100 | itemNo = cursorPosition - indx;
101 | checknext = false;
102 | }
103 |
104 | indx += element.len;
105 | }
106 |
107 | if (findIndx == -1) {
108 | findIndx = content.length - 1;
109 | }
110 | // console.log('itemno', itemNo);
111 |
112 | return { findIndx, itemNo };
113 | }
114 |
115 | updateContent(content, item, index, itemNo = 0) {
116 | let newContent = content;
117 | if (index >= 0 && newContent[index].len === 0) {
118 | if (item !== null) {
119 | newContent = update(newContent, { [index]: { $set: item } });
120 | }
121 | } else if (itemNo === 0) {
122 | if (item !== null && index >= 0) {
123 | newContent = update(newContent, { $splice: [[index, 0, item]] });
124 | }
125 | } else if (itemNo === content[index].len) {
126 | if (item !== null && index >= 0) {
127 | newContent = update(newContent, { $splice: [[index + 1, 0, item]] });
128 | }
129 | } else if (itemNo > 0) {
130 | const foundElement = content[index];
131 | let beforeContent = {
132 | id: foundElement.id,
133 | len: itemNo,
134 | stype: foundElement.stype,
135 | styleList: foundElement.styleList,
136 | tag: foundElement.tag,
137 | text: foundElement.text.substring(0, itemNo),
138 | };
139 |
140 | let afterContent = {
141 | id: shortid.generate(),
142 | len: foundElement.len - itemNo,
143 | stype: foundElement.stype,
144 | styleList: foundElement.styleList,
145 | tag: foundElement.tag,
146 | text: foundElement.text.substring(itemNo),
147 | };
148 |
149 | newContent = update(newContent, { [index]: { $set: beforeContent } });
150 | newContent = update(newContent, { $splice: [[index + 1, 0, afterContent]] });
151 | newContent = update(newContent, { $splice: [[index + 1, 0, item]] });
152 | }
153 |
154 | return newContent;
155 | }
156 |
157 | onSelectionChange = (event) => {
158 | const { selection } = event.nativeEvent;
159 |
160 | if ((this.justToolAdded == true
161 | && selection.start == selection.end
162 | && selection.end >= this.textLength
163 | )
164 | || (
165 | selection.end == this.state.selection.end
166 | && selection.start == this.state.selection.start
167 | )
168 | || (
169 | this.justToolAdded == true
170 | && this.checkKeyPressAndroid > 0
171 | )
172 | ) {
173 | this.justToolAdded = false;
174 | } else {
175 | if (this.justToolAdded == true) {
176 | this.justToolAdded = false;
177 | }
178 |
179 | if (this.androidSelectionJump !== 0) {
180 | selection.start += this.androidSelectionJump;
181 | selection.end += this.androidSelectionJump;
182 | this.androidSelectionJump = 0;
183 | }
184 | const { upComingStype } = this;
185 | this.beforePrevSelection = this.prevSelection;
186 | this.prevSelection = this.state.selection;
187 |
188 | let styles = [];
189 | let selectedTag = '';
190 |
191 | if (upComingStype !== null) {
192 | if (upComingStype.sel.start === this.prevSelection.start
193 | && upComingStype.sel.end === this.prevSelection.end) {
194 | styles = upComingStype.stype;
195 | selectedTag = upComingStype.tag;
196 | } else {
197 | this.upComingStype = null;
198 | }
199 | } else {
200 | const content = this.props.items;
201 |
202 | const res = this.findContentIndex(content, selection.end);
203 |
204 | styles = content[res.findIndx].stype;
205 | selectedTag = content[res.findIndx].tag;
206 | }
207 |
208 | if (this.avoidSelectionChangeOnFocus) {
209 | this.justToolAdded = true;
210 | }
211 | this.avoidSelectionChangeOnFocus = false;
212 | // if(this.avoidAndroidJump == true) {
213 |
214 | // this.avoidSelectionChangeOnFocus = true;
215 | // }
216 | this.avoidAndroidJump = false;
217 |
218 | if (selection.end >= selection.start) {
219 | this.textInput.current.setNativeProps({ selection });
220 | this.setState({
221 | selection,
222 | });
223 | } else {
224 | this.textInput.current.setNativeProps({
225 | selection: { start: selection.end, end: selection.start },
226 | });
227 | this.setState({
228 | selection: { start: selection.end, end: selection.start },
229 | });
230 | }
231 |
232 | if (this.avoidUpdateStyle != true) {
233 | if (this.props.onSelectedStyleChanged) {
234 | this.props.onSelectedStyleChanged(styles);
235 | }
236 | if (this.props.onSelectedTagChanged) {
237 | this.props.onSelectedTagChanged(selectedTag);
238 | }
239 | }
240 |
241 | this.notifyMeasureContentChanged(this.props.items);
242 | }
243 | this.avoidUpdateStyle = false;
244 | }
245 |
246 | handleChangeText = (text) => {
247 | let recalcText = false;
248 | const { selection } = this.state;
249 | const { items } = this.props;
250 |
251 | // index of items that newLine should be applied or remove
252 |
253 | const myText = text;
254 |
255 | // get length of current text
256 | const txtLen = myText.length;
257 | // get lenght of text last called by handletextchange
258 | const prevLen = this.textLength;
259 |
260 | const textDiff = txtLen - prevLen;
261 | let cursorPosition = 0;
262 | let shouldAddText = textDiff >= 0;
263 | let shouldRemText = textDiff < 0;
264 | let remDiff = Math.abs(textDiff);
265 | let addDiff = Math.abs(textDiff);
266 | let addCursorPosition = -1;
267 |
268 | if (IS_IOS) {
269 | if (this.prevSelection.end !== this.prevSelection.start) {
270 | remDiff = Math.abs(this.prevSelection.end - this.prevSelection.start);
271 | addDiff = myText.length - (this.textLength - remDiff);
272 | if (addDiff < 0) {
273 | remDiff += Math.abs(addDiff);
274 | addDiff = 0;
275 | }
276 | shouldRemText = true;
277 | shouldAddText = addDiff > 0;
278 | cursorPosition = this.prevSelection.end;
279 | addCursorPosition = this.prevSelection.start;
280 | } else if (textDiff === 0 && this.prevSelection.end === selection.end) {
281 | remDiff = 1;
282 | addDiff = 1;
283 | shouldRemText = true;
284 | shouldAddText = addDiff > 0;
285 | cursorPosition = this.prevSelection.end;
286 | addCursorPosition = this.prevSelection.end - 1;
287 | } else if (Math.abs(this.prevSelection.end - selection.end) == Math.abs(textDiff)) {
288 | cursorPosition = this.prevSelection.end;
289 | } else if (Math.abs(this.prevSelection.end - selection.end) + Math.abs(this.beforePrevSelection.end - this.prevSelection.end) == Math.abs(textDiff)) {
290 | cursorPosition = this.beforePrevSelection.end;
291 | } else {
292 | const diff = Math.abs(textDiff) - Math.abs(this.prevSelection.end - selection.end) - Math.abs(this.beforePrevSelection.end - this.prevSelection.end);
293 |
294 | if (this.beforePrevSelection.end + diff <= prevLen) {
295 | cursorPosition = this.beforePrevSelection.end + diff;
296 | } else if (this.textLength < myText.length) {
297 | cursorPosition = this.prevSelection.end - Math.abs(textDiff);
298 | } else {
299 | console.log('error may occure');
300 | cursorPosition = this.beforePrevSelection.end;
301 | }
302 | }
303 | } else if (selection.end !== selection.start) {
304 | remDiff = Math.abs(selection.end - selection.start);
305 | addDiff = Math.abs(this.textLength - remDiff - myText.length);
306 | shouldRemText = true;
307 | shouldAddText = addDiff > 0;
308 | cursorPosition = selection.end;
309 | addCursorPosition = selection.start;
310 | } else {
311 | cursorPosition = selection.end;
312 | }
313 |
314 | let content = items;
315 |
316 | let upComing = null;
317 |
318 | if (IS_IOS === false
319 | && shouldAddText === true
320 | && text.length > cursorPosition + addDiff
321 | ) {
322 | const txt = text.substr(cursorPosition + addDiff, 1);
323 | if (txt !== ' ') {
324 | const bef = text.substring(0, cursorPosition + addDiff);
325 | const aft = text.substring(cursorPosition + addDiff);
326 |
327 | const lstIndx = bef.lastIndexOf(' ');
328 | if (lstIndx > 0) {
329 | this.AvoidAndroidIssueWhenPressSpace = 3;
330 | } else {
331 | this.AvoidAndroidIssueWhenPressSpace = 3;
332 | }
333 | }
334 | }
335 |
336 | let preparedText = this.oldText;
337 | if (shouldRemText === true) {
338 | preparedText = preparedText.substring(0, cursorPosition - remDiff) + preparedText.substring(cursorPosition);
339 | }
340 |
341 | if (shouldAddText === true) {
342 | let cursor = cursorPosition;
343 | if (shouldRemText === true) {
344 | if (addCursorPosition >= 0) {
345 | cursor = addCursorPosition;
346 | }
347 | }
348 | const addedText = text.substring(cursor, cursor + addDiff);
349 | preparedText = preparedText.substring(0, cursor) + addedText + preparedText.substring(cursor);
350 | }
351 |
352 | if (preparedText === myText) {
353 | if (shouldRemText === true) {
354 | const result = this.removeTextFromContent(content, cursorPosition, remDiff);
355 |
356 | upComing = result.upComing;
357 | content = result.content;
358 | if (!recalcText) recalcText = result.recalcText;
359 | }
360 |
361 |
362 | if (shouldAddText === true) {
363 | if (shouldRemText === true) {
364 | if (addCursorPosition >= 0) {
365 | cursorPosition = addCursorPosition;
366 | }
367 | }
368 | const addedText = text.substring(cursorPosition, cursorPosition + addDiff);
369 |
370 | const res = this.addTextToContent(content, cursorPosition, addedText);
371 | content = res.content;
372 | if (!recalcText) recalcText = res.recalcText;
373 | }
374 | } else {
375 | // shoud compare with
376 |
377 | const mydiff = this.dmp.diff_main(this.oldText, text);
378 |
379 | let myIndex = 0;
380 | for (let index = 0; index < mydiff.length; index++) {
381 | const element = mydiff[index];
382 | let result = null;
383 | switch (element[0]) {
384 | case 1:
385 | result = this.addTextToContent(content, myIndex, element[1]);
386 | content = result.content;
387 | myIndex += element[1].length;
388 | if (!recalcText) recalcText = result.recalcText;
389 | break;
390 | case -1:
391 | myIndex += element[1].length;
392 |
393 | result = this.removeTextFromContent(content, myIndex, element[1].length);
394 | content = result.content;
395 | upComing = result.upComing;
396 | myIndex -= element[1].length;
397 |
398 | if (!recalcText) recalcText = result.recalcText;
399 |
400 | break;
401 | default:
402 | myIndex += element[1].length;
403 | break;
404 | }
405 | }
406 | }
407 |
408 | if (recalcText === true) {
409 | this.oldText = this.reCalculateText(content);
410 | } else {
411 | this.oldText = text;
412 | }
413 |
414 | let styles = [];
415 | let tagg = 'body';
416 | if (upComing === null) {
417 | const res = this.findContentIndex(content, this.state.selection.end);
418 | styles = content[res.findIndx].stype;
419 | tagg = content[res.findIndx].tag;
420 | } else {
421 | styles = upComing.stype;
422 | tagg = upComing.tag;
423 | }
424 |
425 | this.upComingStype = upComing;
426 |
427 | this.props.onContentChanged(content);
428 | if (this.props.onSelectedStyleChanged) {
429 | this.props.onSelectedStyleChanged(styles);
430 | }
431 |
432 | if (this.props.onSelectedTagChanged) {
433 | this.props.onSelectedTagChanged(tagg);
434 | }
435 |
436 | this.notifyMeasureContentChanged(content);
437 | }
438 |
439 |
440 | addTextToContent(content, cursorPosition, textToAdd) {
441 | let avoidStopSelectionForIOS = false;
442 | let recalcText = false;
443 | const result = this.findContentIndex(content, cursorPosition);
444 |
445 | let foundIndex = result.findIndx;
446 | let foundItemNo = result.itemNo;
447 |
448 | let startWithReadonly = false;
449 |
450 | if (content[foundIndex].readOnly === true) {
451 | if (content[foundIndex].text.length === foundItemNo) {
452 | if (content.length > foundIndex + 1
453 | && !(content[foundIndex + 1].readOnly === true)
454 | && !(content[foundIndex + 1].NewLine === true)
455 | && !(this.upComingStype && this.upComingStype.sel.end === cursorPosition)
456 | ) {
457 | foundIndex += 1;
458 | foundItemNo = 0;
459 | } else if (this.upComingStype
460 | && this.upComingStype.sel.end === cursorPosition) {
461 |
462 | } else {
463 | avoidStopSelectionForIOS = true;
464 | this.upComingStype = {
465 | text: '',
466 | len: 0,
467 | sel: { start: cursorPosition, end: cursorPosition },
468 | stype: content[foundIndex].stype,
469 | tag: content[foundIndex].tag,
470 | styleList: content[foundIndex].styleList,
471 | };
472 | }
473 | } else {
474 | startWithReadonly = true;
475 | }
476 | }
477 |
478 | if (this.upComingStype !== null && startWithReadonly === false
479 | && this.upComingStype.sel.end === cursorPosition) {
480 | content = this.updateContent(content, {
481 | id: shortid.generate(),
482 | text: '',
483 | len: 0,
484 | stype: this.upComingStype.stype,
485 | tag: this.upComingStype.tag,
486 | styleList: this.upComingStype.styleList,
487 | }, foundIndex, foundItemNo);
488 |
489 | const { findIndx, itemNo } = this.findContentIndex(content, cursorPosition);
490 | foundIndex = findIndx;
491 | foundItemNo = itemNo;
492 |
493 | if (IS_IOS === true
494 | && avoidStopSelectionForIOS === false
495 | && !(foundIndex === content.length - 1
496 | && foundItemNo === content[foundIndex].len)
497 | ) {
498 | this.justToolAdded = true;
499 | }
500 | }
501 |
502 | this.checkKeyPressAndroid = 0;
503 | this.textLength += textToAdd.length;
504 |
505 | content[foundIndex].len += textToAdd.length;
506 | content[foundIndex].text = content[foundIndex].text.substring(0, foundItemNo) + textToAdd + content[foundIndex].text.substring(foundItemNo);
507 |
508 | const newLineIndex = content[foundIndex].text.substring(1).indexOf('\n');
509 | if (newLineIndex >= 0) {
510 | const res = this.updateNewLine(content, foundIndex, newLineIndex + 1);
511 | content = res.content;
512 | if (!recalcText) {
513 | recalcText = res.recalcText;
514 | }
515 | } else if (content[foundIndex].text.substring(0, 1) == '\n' && content[foundIndex].NewLine != true) {
516 | const res = this.updateNewLine(content, foundIndex, 0);
517 | content = res.content;
518 | if (!recalcText) {
519 | recalcText = res.recalcText;
520 | }
521 | }
522 |
523 | return { content, recalcText };
524 | }
525 |
526 | removeTextFromContent(content, cursorPosition, removeLength) {
527 | let recalcText = false;
528 | let newLineIndexs = [];
529 | const removeIndexes = [];
530 | let upComing = null;
531 | const result = this.findContentIndex(content, cursorPosition);
532 |
533 | const foundIndex = result.findIndx;
534 | const foundItemNo = result.itemNo;
535 |
536 | const remDiff = removeLength;
537 |
538 | this.textLength -= remDiff;
539 |
540 | if (foundItemNo >= remDiff) {
541 | const txt = content[foundIndex].text;
542 |
543 | content[foundIndex].len -= remDiff;
544 | content[foundIndex].text = txt.substring(0, foundItemNo - remDiff) + txt.substring(foundItemNo, txt.length);
545 |
546 | if (content[foundIndex].NewLine === true) {
547 | newLineIndexs.push(foundIndex);
548 | }
549 | if (content[foundIndex].readOnly === true) {
550 | removeIndexes.push(content[foundIndex].id);
551 | }
552 |
553 | if (content[foundIndex].len === 0 && content.length > 1) {
554 | upComing = {
555 | len: 0,
556 | text: '',
557 | stype: content[foundIndex].stype,
558 | styleList: content[foundIndex].styleList,
559 | tag: content[foundIndex].tag,
560 | sel: {
561 | start: cursorPosition - 1,
562 | end: cursorPosition - 1,
563 | },
564 | };
565 |
566 | removeIndexes.push(content[foundIndex].id);
567 | } else if (foundItemNo === 1) {
568 | upComing = {
569 | len: 0,
570 | text: '',
571 | stype: content[foundIndex].stype,
572 | styleList: content[foundIndex].styleList,
573 | tag: content[foundIndex].tag,
574 | sel: {
575 | start: cursorPosition - 1,
576 | end: cursorPosition - 1,
577 | },
578 | };
579 | }
580 | } else {
581 | let rem = remDiff - (foundItemNo);
582 |
583 | content[foundIndex].len = content[foundIndex].len - foundItemNo;
584 | content[foundIndex].text = content[foundIndex].text.substring(foundItemNo);
585 |
586 | if (rem > 0) {
587 | for (var i = foundIndex - 1; i >= 0; i--) {
588 | if (content[i].NewLine === true) {
589 | newLineIndexs.push(i);
590 | }
591 |
592 | if (content[i].len >= rem) {
593 | content[i].text = content[i].text.substring(0, content[i].len - rem);
594 | content[i].len -= rem;
595 | break;
596 | } else {
597 | rem -= content[i].len;
598 | content[i].len = 0;
599 | content[i].text = '';
600 | }
601 | }
602 | }
603 |
604 | for (var i = content.length - 1; i >= foundIndex; i--) {
605 | if (content[i].len === 0) {
606 | removeIndexes.push(content[i].id);
607 | }
608 | }
609 | }
610 |
611 | // ///// fix //////
612 |
613 | newLineIndexs = newLineIndexs.sort((a, b) => b - a);
614 |
615 | for (let i = 0; i < newLineIndexs.length; i++) {
616 | const index = newLineIndexs[i];
617 | const newLineIndex = content[index].text.indexOf('\n');
618 |
619 | if (newLineIndex < 0) {
620 | if (index > 0) {
621 | content[index].NewLine = false;
622 | beforeTag = content[index - 1].tag;
623 | const res = this.changeToTagIn(content, content[index - 1].tag, index, false);
624 | content = res.content;
625 | if (!recalcText) recalcText = res.recalcText;
626 | } else if (removeIndexes.indexOf(content[index].id) >= 0) {
627 | const tagg = content[index].tag;
628 | if (tagg == 'ul' || tagg === 'ol') {
629 | const res = this.changeToTagIn(content, 'body', 0, false);
630 | content = res.content;
631 | if (!recalcText) recalcText = res.recalcText;
632 | }
633 | if (content.length > 1) {
634 | content[index + 1].NewLine = true;
635 | } else {
636 |
637 | }
638 | }
639 | } else if (removeIndexes.indexOf(content[index].id) >= 0) {
640 | content[index].NewLine = false;
641 | content[index].readOnly = false;
642 | if (index > 0) {
643 | beforeTag = content[index - 1].tag;
644 |
645 | const res = this.changeToTagIn(content, content[index - 1].tag, index, false);
646 | content = res.content;
647 | if (!recalcText) recalcText = res.recalcText;
648 | }
649 | }
650 | }
651 |
652 |
653 | for (let i = 0; i < removeIndexes.length; i++) {
654 | const remIndex = content.findIndex(x => x.id == removeIndexes[i]);
655 | if (remIndex < 0) continue;
656 |
657 | if (content[remIndex].len > 0) {
658 | if (IS_IOS !== true) {
659 | this.androidSelectionJump -= (content[remIndex].len);
660 | }
661 | this.textLength -= content[remIndex].len;
662 | }
663 |
664 | if (remIndex == 0 && (content.length == 1 || (content.length > 1 && content[1].NewLine == true && content[0].len == 0))) {
665 | content[0].text = '';
666 | content[0].len = 0;
667 | content[0].readOnly = false;
668 | } else {
669 | content = content.filter(item => item.id != removeIndexes[i]);
670 | }
671 | }
672 |
673 | return {
674 | content,
675 | upComing,
676 | recalcText,
677 | };
678 | }
679 |
680 | updateNewLine(content, index, itemNo) {
681 | let newContent = content;
682 | let recalcText = false;
683 | const prevTag = newContent[index].tag;
684 | let isPrevList = false;
685 |
686 | if (prevTag === 'ol' || prevTag == 'ul') {
687 | isPrevList = true;
688 | if (IS_IOS === false) {
689 | // this.avoidAndroidJump = true;
690 | }
691 | }
692 |
693 | const isPrevHeading = prevTag === 'heading' || prevTag === 'title';
694 |
695 | const foundElement = newContent[index];
696 |
697 | if (itemNo === 0) {
698 | newContent[index].NewLine = true;
699 | const res = this.changeToTagIn(newContent, isPrevList ? prevTag : 'body', index, true);
700 | newContent = res.content;
701 | if (!recalcText) recalcText = res.recalcText;
702 | } else if (itemNo === foundElement.len - 1) {
703 | newContent[index].len = foundElement.len - 1;
704 | newContent[index].text = foundElement.text.substring(0, foundElement.text.length - 1);
705 |
706 | newContent[index].NewLine = newContent[index].text.indexOf('\n') === 0 || index === 0;
707 | if (newContent.length > index + 1 && newContent[index + 1].NewLine !== true) {
708 | newContent[index + 1].len += 1;
709 | newContent[index + 1].NewLine = true;
710 | newContent[index + 1].text = `\n${newContent[index + 1].text}`;
711 | const res = this.changeToTagIn(newContent, isPrevList ? prevTag : 'body', index + 1, true);
712 | newContent = res.content;
713 | if (!recalcText) recalcText = res.recalcText;
714 | } else {
715 | beforeContent = {
716 | id: shortid.generate(),
717 | len: 1,
718 | stype: isPrevHeading === true ? [] : newContent[index].stype,
719 | styleList: [],
720 | tag: 'body',
721 | text: '\n',
722 | NewLine: true,
723 | };
724 | beforeContent.styleList = StyleSheet.flatten(this.convertStyleList(update(beforeContent.stype, { $push: [beforeContent.tag] })));
725 |
726 | newContent = update(newContent, { $splice: [[index + 1, 0, beforeContent]] });
727 | if (isPrevList === true) {
728 | const res = this.changeToTagIn(newContent, prevTag, index + 1, true);
729 | newContent = res.content;
730 | if (!recalcText) recalcText = res.recalcText;
731 | }
732 | }
733 | } else {
734 | let beforeContent = {
735 | id: foundElement.id,
736 | len: itemNo,
737 | stype: foundElement.stype,
738 | styleList: foundElement.styleList,
739 | tag: foundElement.tag,
740 | text: foundElement.text.substring(0, itemNo),
741 | NewLine: foundElement.text.substring(0, itemNo).indexOf('\n') === 0 || index === 0,
742 | };
743 |
744 | let afterContent = {
745 | id: shortid.generate(),
746 | len: foundElement.len - itemNo,
747 | text: foundElement.text.substring(itemNo, foundElement.len),
748 | stype: foundElement.stype,
749 | styleList: foundElement.styleList,
750 | tag: isPrevList ? prevTag : 'body',
751 | NewLine: true,
752 | };
753 |
754 | newContent = update(newContent, { [index]: { $set: beforeContent } });
755 | newContent = update(newContent, { $splice: [[index + 1, 0, afterContent]] });
756 |
757 | const res = this.changeToTagIn(newContent, isPrevList ? prevTag : 'body', index + 1, true);
758 | newContent = res.content;
759 | if (!recalcText) recalcText = res.recalcText;
760 | }
761 |
762 | return { content: newContent, recalcText };
763 | }
764 |
765 | createUpComing(start, end, tag, stype) {
766 | this.upComingStype = {
767 | sel: { start, end },
768 | tag,
769 | text: '',
770 | stype,
771 | styleList: StyleSheet.flatten(this.convertStyleList(update(stype, { $push: [tag] }))),
772 | };
773 | }
774 |
775 | addToUpComming(toolType) {
776 | if (this.upComingStype) {
777 | const indexOfUpToolType = this.upComingStype.stype.indexOf(toolType);
778 | const newUpStype = this.upComingStype ? (indexOfUpToolType != -1 ? update(this.upComingStype.stype, { $splice: [[indexOfUpToolType, 1]] })
779 | : update(this.upComingStype.stype, { $push: [toolType] })) : [toolType];
780 | this.upComingStype.stype = newUpStype;
781 | this.upComingStype.styleList = StyleSheet.flatten(this.convertStyleList(update(newUpStype, { $push: [this.upComingStype.tag] })));
782 | }
783 | }
784 |
785 | applyStyle(toolType) {
786 | const { selection: { start, end } } = this.state;
787 | const { items } = this.props;
788 |
789 | const newCollection = [];
790 |
791 | const content = items;
792 | let indx = 0;
793 | let upComingAdded = false;
794 |
795 | for (let i = 0; i < content.length; i++) {
796 | const {
797 | id, len, stype, tag, text, styleList,
798 | } = content[i];
799 | const NewLine = content[i].NewLine ? content[i].NewLine : false;
800 | const readOnly = content[i].readOnly ? content[i].readOnly : false;
801 |
802 | const indexOfToolType = stype.indexOf(toolType);
803 | const newStype = (indexOfToolType != -1)
804 | ? update(stype, { $splice: [[indexOfToolType, 1]] })
805 | : update(stype, { $push: [toolType] });
806 |
807 | const newStyles = StyleSheet.flatten(this.convertStyleList(update(newStype, { $push: [tag] })));
808 |
809 | const from = indx;
810 | indx += len;
811 | const to = indx;
812 |
813 | if (readOnly) {
814 | newCollection.push({
815 | id,
816 | text,
817 | len: to - from,
818 | tag,
819 | stype,
820 | styleList,
821 | NewLine,
822 | readOnly,
823 | });
824 |
825 | if (i === content.length - 1 && start === end && end === to) {
826 | if (upComingAdded === false) {
827 | if (this.upComingStype === null
828 | || (this.upComingStype.sel.start === start && this.upComingStype.sel.end === end) == false) {
829 | this.createUpComing(start, end, tag, newStype);
830 | } else {
831 | this.addToUpComming(toolType);
832 | }
833 |
834 | upComingAdded = true;
835 | }
836 | }
837 | } else if ((start >= from && start < to) && (end >= from && end < to)) {
838 | if (start !== end) {
839 | if (start !== from) {
840 | newCollection.push({
841 | id,
842 | text: text.substring(0, start - from),
843 | len: start - from,
844 | stype,
845 | styleList,
846 | tag,
847 | NewLine,
848 | readOnly,
849 | });
850 | }
851 |
852 | newCollection.push({
853 | id: shortid.generate(),
854 | text: text.substring(start - from, end - from),
855 | len: end - start,
856 | tag,
857 | stype: newStype,
858 | styleList: newStyles,
859 | });
860 |
861 | if (end !== to) {
862 | newCollection.push({
863 | id: shortid.generate(),
864 | text: text.substring(end - from, to - from),
865 | len: to - end,
866 | tag,
867 | stype,
868 | styleList,
869 | });
870 | }
871 | } else {
872 | newCollection.push({
873 | id,
874 | text,
875 | len: to - from,
876 | tag,
877 | stype,
878 | styleList,
879 | NewLine,
880 | readOnly,
881 | });
882 |
883 | if (upComingAdded === false) {
884 | if (this.upComingStype === null
885 | || (this.upComingStype.sel.start === start && this.upComingStype.sel.end === end) == false) {
886 | this.createUpComing(start, end, tag, newStype);
887 | } else {
888 | this.addToUpComming(toolType);
889 | }
890 | upComingAdded = true;
891 | }
892 | }
893 | } else if (start >= from && start < to) {
894 | if (start !== from) {
895 | newCollection.push({
896 | id,
897 | len: start - from,
898 | text: text.substring(0, start - from),
899 | stype,
900 | styleList,
901 | tag,
902 | NewLine,
903 | readOnly,
904 | });
905 | }
906 |
907 | newCollection.push({
908 | id: shortid.generate(),
909 | len: to - start,
910 | text: text.substring(start - from, to - from),
911 | tag,
912 | stype: newStype,
913 | styleList: newStyles,
914 | });
915 | } else if (end > from && end <= to) {
916 | if (start === end) {
917 | newCollection.push({
918 | id,
919 | text,
920 | len: to - from,
921 | stype,
922 | styleList,
923 | tag,
924 | NewLine,
925 | readOnly,
926 |
927 | });
928 |
929 | if (upComingAdded === false) {
930 | if (this.upComingStype === null || (this.upComingStype.sel.start === start && this.upComingStype.sel.end === end) == false) {
931 | this.createUpComing(start, end, tag, newStype);
932 | } else {
933 | this.addToUpComming(toolType);
934 | }
935 | upComingAdded = true;
936 | }
937 | } else {
938 | newCollection.push({
939 | id: shortid.generate(),
940 | text: text.substring(0, end - from),
941 | len: end - from,
942 | tag,
943 | NewLine,
944 | stype: newStype,
945 | styleList: newStyles,
946 | });
947 |
948 | if (end !== to) {
949 | newCollection.push({
950 | id,
951 | text: text.substring(end - from, to - from),
952 | len: to - end,
953 | tag,
954 | stype,
955 | styleList,
956 | readOnly,
957 |
958 | });
959 | }
960 | }
961 | } else if (from === to && start === from && end === to) {
962 | newCollection.push({
963 | id,
964 | text,
965 | len: to - from,
966 | tag,
967 | stype,
968 | styleList,
969 | NewLine,
970 | readOnly,
971 | });
972 |
973 | if (upComingAdded === false) {
974 | if (this.upComingStype === null || (this.upComingStype.sel.start === start && this.upComingStype.sel.end === end) == false) {
975 | this.createUpComing(start, end, tag, newStype);
976 | } else {
977 | this.addToUpComming(toolType);
978 | }
979 |
980 | upComingAdded = true;
981 | }
982 | } else if ((from >= start && from <= end) && (to >= start && to <= end)) {
983 | newCollection.push({
984 | id,
985 | text,
986 | len: to - from,
987 | tag,
988 | stype: newStype,
989 | styleList: newStyles,
990 | NewLine,
991 | readOnly,
992 |
993 | });
994 | } else {
995 | newCollection.push({
996 | id,
997 | text,
998 | len: to - from,
999 | tag,
1000 | stype,
1001 | styleList,
1002 | NewLine,
1003 | readOnly,
1004 |
1005 | });
1006 | }
1007 | }
1008 |
1009 | const res = this.findContentIndex(newCollection, this.state.selection.end);
1010 |
1011 | let styles = [];
1012 | if (this.upComingStype != null) {
1013 | styles = this.upComingStype.stype;
1014 | } else {
1015 | styles = newCollection[res.findIndx].stype;
1016 | }
1017 |
1018 | this.justToolAdded = start !== end;
1019 | this.props.onContentChanged(newCollection);
1020 | if (this.props.onSelectedStyleChanged) this.props.onSelectedStyleChanged(styles);
1021 | }
1022 |
1023 | reCalculateText = (content) => {
1024 | let text = '';
1025 | for (let i = 0; i < content.length; i++) {
1026 | text += content[i].text;
1027 | }
1028 | return text;
1029 | }
1030 |
1031 | applyTag(tagType) {
1032 | const { items } = this.props;
1033 | const { selection } = this.state;
1034 |
1035 | const res = this.findContentIndex(items, selection.end);
1036 | const { content, recalcText } = this.changeToTagIn(items, tagType, res.findIndx);
1037 |
1038 | if (recalcText == true) {
1039 | this.oldText = this.reCalculateText(content);
1040 | }
1041 |
1042 | if (this.props.onContentChanged) {
1043 | this.props.onContentChanged(content);
1044 | }
1045 |
1046 | if (this.props.onSelectedTagChanged) {
1047 | this.props.onSelectedTagChanged(tagType);
1048 | }
1049 |
1050 | this.notifyMeasureContentChanged(content);
1051 | }
1052 |
1053 | notifyMeasureContentChanged(content) {
1054 | if (this.props.onMeasureContentChanged) {
1055 | try {
1056 | setTimeout(() => {
1057 | const res = this.findContentIndex(content, this.state.selection.end);
1058 |
1059 | const measureArray = content.slice(0, res.findIndx);
1060 | measureArray.push({
1061 | id: shortid.generate(),
1062 | len: res.itemNo,
1063 | stype: content[res.findIndx].stype,
1064 | styleList: content[res.findIndx].styleList,
1065 | text: content[res.findIndx].text.substring(0, res.itemNo + 1),
1066 | tag: content[res.findIndx].tag,
1067 | NewLine: content[res.findIndx].NewLine,
1068 | readOnly: content[res.findIndx].readOnly,
1069 | });
1070 | this.props.onMeasureContentChanged(measureArray);
1071 | }, 100);
1072 | } catch (error) {
1073 |
1074 | }
1075 | }
1076 | }
1077 |
1078 | changeToTagIn(items, tag, index, fromTextChange = false) {
1079 | let recalcText = false;
1080 | const needBold = tag === 'heading' || tag === 'title';
1081 | let content = items;
1082 |
1083 | for (let i = index + 1; i < content.length; i++) {
1084 | if (content[i].NewLine === true) {
1085 | break;
1086 | } else {
1087 | if (needBold === true && content[i].stype.indexOf('bold') == -1) {
1088 | content[i].stype = update(content[i].stype, { $push: ['bold'] });
1089 | } else if (needBold === false
1090 | && (content[i].tag === 'heading' || content[i].tag === 'title')
1091 | && content[i].stype.indexOf('bold') != -1) {
1092 | content[i].stype = content[i].stype.filter(typ => typ != 'bold');
1093 | }
1094 | content[i].tag = tag;
1095 | content[i].styleList = StyleSheet.flatten(this.convertStyleList(update(content[i].stype, { $push: [content[i].tag] })));
1096 | }
1097 | }
1098 | let shouldReorderList = false;
1099 |
1100 | for (let i = index; i >= 0; i--) {
1101 | if (content[i].NewLine === true && content[i].tag === 'ol') {
1102 | shouldReorderList = true;
1103 | }
1104 |
1105 | if (needBold === true
1106 | // (content[i].tag === 'heading' || content[i].tag === 'title') &&
1107 | && content[i].stype.indexOf('bold') == -1) {
1108 | content[i].stype = update(content[i].stype, { $push: ['bold'] });
1109 | } else if (needBold === false
1110 | && (content[i].tag === 'heading' || content[i].tag === 'title')
1111 | && content[i].stype.indexOf('bold') != -1) {
1112 | content[i].stype = content[i].stype.filter(typ => typ != 'bold');
1113 | }
1114 |
1115 | content[i].tag = tag;
1116 | content[i].styleList = StyleSheet.flatten(this.convertStyleList(update(content[i].stype, { $push: [content[i].tag] })));
1117 |
1118 | if (content[i].NewLine === true) {
1119 | recalcText = true;
1120 | if (tag === 'ul') {
1121 | if (content[i].readOnly === true) {
1122 | this.textLength -= content[i].len;
1123 | if (i === 0) {
1124 | content[i].text = '\u2022 ';
1125 | content[i].len = 2;
1126 | } else {
1127 | content[i].text = '\n\u2022 ';
1128 | content[i].len = 3;
1129 | }
1130 | this.textLength += content[i].len;
1131 |
1132 | if (fromTextChange === true && IS_IOS !== true) {
1133 | this.androidSelectionJump += content[i].len;
1134 | }
1135 | } else {
1136 | if (content[i].len > (i === 0 ? 0 : 1)) {
1137 | content[i].text = content[i].text.substring((i === 0 ? 0 : 1));
1138 | content[i].len = content[i].len - (i === 0 ? 0 : 1);
1139 | content[i].NewLine = false;
1140 | listContent = {
1141 | id: shortid.generate(),
1142 | len: i === 0 ? 2 : 3,
1143 | stype: [],
1144 | text: i === 0 ? '\u2022 ' : '\n\u2022 ',
1145 | tag: 'ul',
1146 | NewLine: true,
1147 | readOnly: true,
1148 | };
1149 | content = update(content, { $splice: [[i, 0, listContent]] });
1150 | } else {
1151 | content[i].text = i === 0 ? '\u2022 ' : '\n\u2022 ';
1152 | content[i].len = i === 0 ? 2 : 3;
1153 | content[i].readOnly = true;
1154 | content[i].stype = [];
1155 | content[i].styleList = [];
1156 | }
1157 | this.textLength += 2;
1158 | if (fromTextChange === true && IS_IOS !== true) {
1159 | this.androidSelectionJump += 2;
1160 | }
1161 |
1162 | // }
1163 | }
1164 | } else if (tag === 'ol') {
1165 | shouldReorderList = true;
1166 | if (content[i].readOnly === true) {
1167 | this.textLength -= content[i].len;
1168 | if (i === 0) {
1169 | content[i].text = '1- ';
1170 | content[i].len = 3;
1171 | } else {
1172 | content[i].text = '\n1- ';
1173 | content[i].len = 4;
1174 | }
1175 | this.textLength += content[i].len;
1176 | if (fromTextChange === true && IS_IOS !== true) {
1177 | this.androidSelectionJump += content[i].len;
1178 | }
1179 | } else {
1180 | if (content[i].len > (i === 0 ? 0 : 1)) {
1181 | content[i].text = content[i].text.substring((i === 0 ? 0 : 1));
1182 | content[i].len = content[i].len - (i === 0 ? 0 : 1);
1183 | content[i].NewLine = false;
1184 | listContent = {
1185 | id: shortid.generate(),
1186 | len: i === 0 ? 3 : 4,
1187 | stype: [],
1188 | text: i === 0 ? '1- ' : '\n1- ',
1189 | tag: 'ol',
1190 | NewLine: true,
1191 | readOnly: true,
1192 | };
1193 | content = update(content, { $splice: [[i, 0, listContent]] });
1194 | } else {
1195 | content[i].text = i === 0 ? '1- ' : '\n1- ';
1196 | content[i].len = i === 0 ? 3 : 4;
1197 | content[i].readOnly = true;
1198 | content[i].stype = [];
1199 | }
1200 |
1201 | this.textLength += 3;
1202 | if (fromTextChange === true && IS_IOS !== true) {
1203 | this.androidSelectionJump += 3;
1204 | }
1205 | }
1206 | } else if (content[i].readOnly === true) {
1207 | if (i !== 0) {
1208 | this.textLength -= (content[i].len - 1);
1209 | content[i].text = '\n';
1210 | content[i].len = 1;
1211 | content[i].readOnly = false;
1212 | } else {
1213 | this.textLength -= content[i].len;
1214 | if (content.length > 1 && !(content[1].NewLine === true)) {
1215 | content = update(content, { $splice: [[i, 1]] });
1216 | content[0].NewLine = true;
1217 | } else {
1218 | content[0].NewLine = true;
1219 | content[0].readOnly = false;
1220 | content[0].len = 0;
1221 | content[0].text = '';
1222 | }
1223 | }
1224 | }
1225 |
1226 |
1227 | break;
1228 | }
1229 | }
1230 |
1231 |
1232 | if (shouldReorderList === true) {
1233 | recalcText = true;
1234 | content = this.reorderList(content);
1235 | }
1236 |
1237 | return { content, recalcText };
1238 | }
1239 |
1240 | reorderList(items) {
1241 | let listNo = 1;
1242 | for (let i = 0; i < items.length; i++) {
1243 | const element = items[i];
1244 | if (element.NewLine === true && element.tag === 'ol') {
1245 | this.textLength -= element.len;
1246 | items[i].text = i === 0 ? (`${listNo}- `) : (`\n${listNo}- `);
1247 | items[i].len = items[i].text.length;
1248 | this.textLength += items[i].len;
1249 | listNo += 1;
1250 | } else if (element.tag !== 'ol') {
1251 | listNo = 1;
1252 | }
1253 | }
1254 | return items;
1255 | }
1256 |
1257 | convertStyleList(stylesArr) {
1258 | const styls = [];
1259 | (stylesArr).forEach((element) => {
1260 | const styleObj = this.txtToStyle(element);
1261 | if (styleObj !== null) styls.push(styleObj);
1262 | });
1263 | return styls;
1264 | }
1265 |
1266 | txtToStyle = (styleName) => {
1267 | const styles = this.props.styleList;
1268 | return styles[styleName];
1269 | }
1270 |
1271 | forceSelectedStyles() {
1272 | const content = this.props.items;
1273 | const { selection } = this.state;
1274 |
1275 | const { findIndx } = this.findContentIndex(content, selection.end);
1276 | const styles = content[findIndx].stype;
1277 | const selectedTag = content[findIndx].tag;
1278 |
1279 | if (this.props.onSelectedStyleChanged) {
1280 | this.props.onSelectedStyleChanged(styles);
1281 | }
1282 | if (this.props.onSelectedTagChanged) {
1283 | this.props.onSelectedTagChanged(selectedTag);
1284 | }
1285 | }
1286 |
1287 | onFocus = (e) => {
1288 | if (this.props.onFocus) this.props.onFocus(e);
1289 | }
1290 |
1291 | onBlur = (e) => {
1292 | if (this.props.onBlur) this.props.onBlur(e);
1293 | }
1294 |
1295 | avoidSelectionChangeOnFocus() {
1296 | this.avoidSelectionChangeOnFocus = true;
1297 | }
1298 |
1299 | handleKeyDown = (e) => {
1300 | this.checkKeyPressAndroid += 1;
1301 | if (e.nativeEvent.key === 'Backspace' && this.state.selection.start === 0
1302 | && this.state.selection.end === 0) {
1303 | if (this.props.onConnectToPrevClicked) this.props.onConnectToPrevClicked();
1304 | }
1305 | }
1306 |
1307 | handleContentSizeChange = (event) => {
1308 | if (this.props.onContentSizeChange) this.props.onContentSizeChange(event);
1309 | }
1310 |
1311 | render() {
1312 | const {
1313 | items, foreColor, style, returnKeyType, styleList, textInputProps
1314 | } = this.props;
1315 | const { selection } = this.state;
1316 | const color = foreColor || '#000';
1317 | const fontSize =styleList && styleList.body && styleList.body.fontSize ? styleList.body.fontSize : 20;
1318 |
1319 | return (
1320 |
1345 | {
1346 | _.map(items, item => (
1347 |
1348 | ))
1349 | }
1350 |
1351 | );
1352 | }
1353 |
1354 | splitItems() {
1355 | const { selection } = this.state;
1356 | const { items } = this.props;
1357 | const content = items;
1358 | const result = this.findContentIndex(content, selection.end);
1359 | let beforeContent = [];
1360 | let afterContent = [];
1361 |
1362 | for (let i = 0; i < result.findIndx; i++) {
1363 | const element = content[i];
1364 | beforeContent.push(element);
1365 | }
1366 |
1367 | const foundElement = content[result.findIndx];
1368 | if (result.itemNo != 0) {
1369 | beforeContent.push({
1370 | id: foundElement.id,
1371 | text: foundElement.text.substring(0, result.itemNo),
1372 | len: result.itemNo,
1373 | stype: foundElement.stype,
1374 | styleList: foundElement.styleList,
1375 | tag: foundElement.tag,
1376 | NewLine: foundElement.NewLine,
1377 | readOnly: foundElement.readOnly,
1378 | });
1379 | }
1380 |
1381 | if (result.itemNo !== foundElement.len) {
1382 | afterContent.push({
1383 | id: (result.itemNo === 0) ? foundElement.id : shortid.generate(),
1384 | text: foundElement.text.substring(result.itemNo, foundElement.len),
1385 | len: foundElement.len - result.itemNo,
1386 | stype: foundElement.stype,
1387 | styleList: foundElement.styleList,
1388 | tag: foundElement.tag,
1389 | NewLine: true,
1390 | readOnly: foundElement.readOnly,
1391 | });
1392 | }
1393 |
1394 | for (let i = result.findIndx + 1; i < content.length; i++) {
1395 | const element = content[i];
1396 | afterContent.push(element);
1397 | }
1398 | beforeContent = this.reorderList(beforeContent);
1399 | afterContent = this.reorderList(afterContent);
1400 |
1401 | return {
1402 | before: beforeContent,
1403 | after: afterContent,
1404 | };
1405 | }
1406 |
1407 | focus(selection = null) {
1408 | this.textInput.focus();
1409 |
1410 | if (selection != null && selection.start && selection.end) {
1411 | this.textInput.current.setNativeProps({ selection });
1412 | setTimeout(() => {
1413 | this.setState({
1414 | selection,
1415 | });
1416 | }, 300);
1417 | }
1418 | }
1419 | }
1420 |
1421 | export default CNTextInput;
1422 |
--------------------------------------------------------------------------------
/src/CNToolbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import {
3 | View,
4 | TouchableWithoutFeedback,
5 | TouchableHighlight,
6 | Text,
7 | StyleSheet
8 | } from 'react-native'
9 |
10 | import { CNSeperator } from './CNSeperator'
11 | import { CNToolbarIcon } from './CNToolbarIcon'
12 | import { CNToolbarSetIcon } from './CNToolbarSetIcon'
13 | const defaultColor = '#737373'
14 | const defaultBgColor = '#fff'
15 | const defaultSelectedColor = '#2a2a2a'
16 | const defaultSelectedBgColor = '#e4e4e4'
17 | const defaultSize = 16;
18 |
19 | class CNToolbar extends Component {
20 | constructor(props) {
21 | super(props);
22 | }
23 |
24 | componentDidMount() {
25 | if(!this.props.iconSet)
26 | console.warn('CNToolbar requires `iconSet` prop to display icons (>= 1.0.41). Please check documentation on github.')
27 | if(this.props.bold
28 | || this.props.italic
29 | || this.props.underline
30 | || this.props.lineThrough
31 | || this.props.body
32 | || this.props.title
33 | || this.props.heading
34 | || this.props.ul
35 | || this.props.ol
36 | || this.props.image
37 | || this.props.highlight
38 | || this.props.foreColor
39 | ) {
40 | console.warn('CNToolbar: using `bold`, `italic`, `underline`, `lineThrough`, `body`, `title`, `heading`, `ul`, `ol`, `image`, `highlight` or `foreColor` is deprecated. You may use `iconSet` prop instead (>= 1.0.41)')
41 | }
42 | }
43 |
44 | onStyleKeyPress = (toolItem) => {
45 | if (this.props.onStyleKeyPress) this.props.onStyleKeyPress(toolItem);
46 | }
47 |
48 | render() {
49 | const {
50 | selectedStyles,
51 | selectedTag,
52 | size,
53 | style,
54 | color,
55 | backgroundColor,
56 | selectedColor,
57 | selectedBackgroundColor,
58 | iconSet = [],
59 | iconContainerStyle,
60 | iconSetContainerStyle,
61 | } = this.props;
62 |
63 | return (
64 |
65 | {iconSet.map((object, index) => {
66 | return (
67 | object.type !== 'seperator' &&
68 | object.iconArray &&
69 | object.iconArray.length > 0 ?
70 | :
84 |
88 | )
89 | })}
90 |
91 | );
92 | }
93 | }
94 |
95 |
96 | const styles = StyleSheet.create({
97 | icon: {
98 | top: 2,
99 | },
100 | iconContainer: {
101 | borderRadius: 3,
102 | alignItems: 'center',
103 | justifyContent: 'center',
104 | },
105 | iconSetContainer: {
106 | flexDirection: 'row',
107 | justifyContent: 'space-between',
108 | alignItems: 'center',
109 | paddingTop: 2,
110 | paddingBottom: 2,
111 | paddingLeft: 3,
112 | paddingRight: 3,
113 | marginRight: 1,
114 | },
115 | toolbarContainer: {
116 | flexDirection: 'row',
117 | justifyContent: 'space-around',
118 | borderWidth: 1,
119 | borderColor: defaultSelectedBgColor,
120 | borderRadius: 4,
121 | padding: 2,
122 | backgroundColor: '#fff',
123 | },
124 | separator: {
125 | width: 2,
126 | marginTop: 1,
127 | marginBottom: 1,
128 | backgroundColor: defaultSelectedBgColor,
129 | },
130 | });
131 |
132 | export default CNToolbar;
133 |
--------------------------------------------------------------------------------
/src/CNToolbarIcon.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import {
3 | View,
4 | TouchableWithoutFeedback,
5 | TouchableHighlight,
6 | Text,
7 | StyleSheet
8 | } from 'react-native'
9 |
10 | export const CNToolbarIcon = (props) => {
11 | const {
12 | size,
13 | backgroundColor,
14 | color,
15 | iconStyles,
16 | toolTypeText,
17 | iconComponent,
18 | onStyleKeyPress,
19 | selectedColor,
20 | selectedStyles,
21 | selectedTag,
22 | buttonTypes,
23 | selectedBackgroundColor,
24 | } = props
25 | let colorCondition = '';
26 | let backgroundColorCondition = '';
27 | if (buttonTypes === 'style') {
28 | backgroundColorCondition = selectedStyles.indexOf(toolTypeText) >= 0 ? selectedBackgroundColor : backgroundColor;
29 | colorCondition = selectedStyles.indexOf(toolTypeText) >= 0 ? selectedColor : color;
30 | }
31 | else if (buttonTypes === 'tag') {
32 | backgroundColorCondition = selectedTag === toolTypeText ? selectedBackgroundColor : backgroundColor;
33 | colorCondition = selectedTag === toolTypeText ? selectedColor : color
34 | }
35 | return (
36 | {
38 | onStyleKeyPress(toolTypeText)
39 | }}
40 | >
41 |
46 | {
47 | React.cloneElement(iconComponent, { size , color: colorCondition , style: [{
48 | fontSize: size,
49 | color: colorCondition
50 | }, iconComponent.props.style || {}] })
51 | }
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/src/CNToolbarSetIcon.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import {
3 | View,
4 | TouchableWithoutFeedback,
5 | TouchableHighlight,
6 | Text,
7 | StyleSheet
8 | } from 'react-native'
9 | import { CNToolbarIcon } from './CNToolbarIcon'
10 |
11 | export const CNToolbarSetIcon = (props) => {
12 | const {
13 | size,
14 | color,
15 | backgroundColor,
16 | selectedColor,
17 | selectedBackgroundColor,
18 | selectedStyles,
19 | selectedTag,
20 | iconArray,
21 | iconSetContainerStyle,
22 | iconStyles,
23 | onStyleKeyPress
24 | } = props
25 | return (
26 |
27 | {iconArray.map((object, index) => {
28 | const {
29 | toolTypeText,
30 | iconComponent,
31 | buttonTypes
32 | } = object
33 | return (
34 |
49 | )
50 | })}
51 |
52 | )
53 | }
--------------------------------------------------------------------------------
/src/Convertors.js:
--------------------------------------------------------------------------------
1 | import update from 'immutability-helper';
2 | import { StyleSheet } from 'react-native';
3 |
4 | const { DOMParser } = require('xmldom');
5 | const { XMLSerializer } = require('xmldom');
6 | const shortid = require('shortid');
7 |
8 | export function convertToHtmlString(contents, styleList = null) {
9 | const availableStyles = styleList == null ? defaultStyles : styleList;
10 |
11 | // let keys = Object.keys(availableStyles);
12 | const myDoc = new DOMParser().parseFromString(
13 | '', 'text/xml',
14 | );
15 |
16 | for (let i = 0; i < contents.length; i++) {
17 | const input = contents[i];
18 |
19 | if (input.component === 'text') {
20 | var element = null;
21 | let parent = null;
22 | let olStarted = false;
23 | let ulStarted = false;
24 | for (let j = 0; j < input.content.length; j++) {
25 | const item = input.content[j];
26 | const isBold = item.stype.indexOf('bold') > -1;
27 | const isItalic = item.stype.indexOf('italic') > -1;
28 | const isUnderLine = item.stype.indexOf('underline') > -1;
29 | const isOverline = item.stype.indexOf('lineThrough') > -1;
30 | const isBlue = item.stype.indexOf('blue') > -1;
31 | const isRed = item.stype.indexOf('red') > -1;
32 | const isGreen = item.stype.indexOf('green') > -1;
33 | const isBlueMarker = item.stype.indexOf('blue_hl') > -1;
34 | const isGreenMarker = item.stype.indexOf('green_hl') > -1;
35 | const isPinkMarker = item.stype.indexOf('pink_hl') > -1;
36 | const isPurpleMarker = item.stype.indexOf('purple_hl') > -1;
37 | const isYellowMarker = item.stype.indexOf('yellow_hl') > -1;
38 | const isOrangeMarker = item.stype.indexOf('orange_hl') > -1;
39 | let tag = '';
40 |
41 |
42 | switch (item.tag) {
43 | case 'heading':
44 | tag = 'h3';
45 | break;
46 | case 'title':
47 | tag = 'h1';
48 | break;
49 | case 'body':
50 | tag = 'p';
51 | break;
52 | case 'ol':
53 | tag = 'ol';
54 | break;
55 | case 'ul':
56 | tag = 'ul';
57 | break;
58 |
59 | default:
60 | tag = 'p';
61 | break;
62 | }
63 | let styles = '';
64 | styles += isBold ? 'font-weight: bold;' : '';
65 | styles += isItalic ? 'font-style: italic;' : '';
66 | styles += isOverline ? 'text-decoration: line-through;' : '';
67 | styles += isUnderLine ? 'text-decoration: underline;' : '';
68 | styles += isBlue ? `color: ${availableStyles.blue.color};` : '';
69 | styles += isRed ? `color: ${availableStyles.red.color};` : '';
70 | styles += isGreen ? `color: ${availableStyles.green.color};` : '';
71 | styles += isBlueMarker ? `background-color: ${availableStyles.blue_hl.backgroundColor};` : '';
72 | styles += isGreenMarker ? `background-color: ${availableStyles.green_hl.backgroundColor};` : '';
73 | styles += isPinkMarker ? `background-color: ${availableStyles.pink_hl.backgroundColor};` : '';
74 | styles += isPurpleMarker ? `background-color: ${availableStyles.purple_hl.backgroundColor};` : '';
75 | styles += isYellowMarker ? `background-color: ${availableStyles.yellow_hl.backgroundColor};` : '';
76 | styles += isOrangeMarker ? `background-color: ${availableStyles.orange_hl.backgroundColor};` : '';
77 |
78 | if (item.NewLine == true || j == 0) {
79 | element = myDoc.createElement(tag);
80 |
81 | if (tag === 'ol') {
82 | if (olStarted !== true) {
83 | olStarted = true;
84 | parent = myDoc.createElement(tag);
85 | myDoc.documentElement.appendChild(parent);
86 | }
87 | ulStarted = false;
88 | element = myDoc.createElement('li');
89 | parent.appendChild(element);
90 | } else if (tag === 'ul') {
91 | if (ulStarted !== true) {
92 | ulStarted = true;
93 | parent = myDoc.createElement(tag);
94 | myDoc.documentElement.appendChild(parent);
95 | }
96 | olStarted = false;
97 | element = myDoc.createElement('li');
98 | parent.appendChild(element);
99 | } else {
100 | olStarted = false;
101 | ulStarted = false;
102 |
103 | element = myDoc.createElement(tag);
104 | myDoc.documentElement.appendChild(element);
105 | }
106 | }
107 | if (item.readOnly === true) {
108 |
109 | } else {
110 | const child = myDoc.createElement('span');
111 | if (item.NewLine === true && j != 0) {
112 | child.appendChild(myDoc.createTextNode(item.text.substring(1)));
113 | } else {
114 | child.appendChild(myDoc.createTextNode(item.text));
115 | }
116 |
117 | if (styles.length > 0) {
118 | child.setAttribute('style', styles);
119 | }
120 |
121 | element.appendChild(child);
122 | }
123 | }
124 | } else if (input.component === 'image') {
125 | element = myDoc.createElement('img');
126 | element.setAttribute('src', input.url);
127 | element.setAttribute('width', input.size.width);
128 | element.setAttribute('height', input.size.height);
129 | if (input.imageId) {
130 | element.setAttribute('data-id', input.imageId);
131 | }
132 | myDoc.documentElement.appendChild(element);
133 | }
134 | }
135 |
136 | return new XMLSerializer().serializeToString(myDoc);
137 | }
138 |
139 | export function convertToObject(htmlString, styleList = null) {
140 |
141 | const availableStyles = styleList == null ? defaultStyles : styleList;
142 |
143 | const doc = new DOMParser().parseFromString(htmlString, 'text/xml');
144 | let contents = [];
145 | let item = null;
146 |
147 | for (let i = 0; i < doc.documentElement.childNodes.length; i++) {
148 | const element = doc.documentElement.childNodes[i];
149 | let tag = '';
150 | switch (element.nodeName) {
151 | case 'h1':
152 | tag = 'title';
153 | break;
154 | case 'h3':
155 | tag = 'heading';
156 | break;
157 | case 'p':
158 | tag = 'body';
159 | break;
160 | case 'img':
161 | tag = 'image';
162 | break;
163 | case 'ul':
164 | tag = 'ul';
165 | break;
166 | case 'ol':
167 | tag = 'ol';
168 | break;
169 |
170 | default:
171 | break;
172 | }
173 |
174 |
175 | if (tag === 'image') {
176 | if (item != null) {
177 | // contents.push(item);
178 |
179 | contents = update(contents, { $push: [item] });
180 | item = null;
181 | }
182 |
183 | let url = '';
184 | let imageId = null;
185 | const size = {};
186 | if (element.hasAttribute('src') === true) {
187 | url = element.getAttribute('src');
188 | }
189 | if (element.hasAttribute('data-id') === true) {
190 | imageId = element.getAttribute('data-id');
191 | }
192 |
193 | if (element.hasAttribute('width') === true) {
194 | try {
195 | size.width = parseInt(element.getAttribute('width'));
196 | } catch (error) {
197 |
198 | }
199 | }
200 | if (element.hasAttribute('height') === true) {
201 | try {
202 | size.height = parseInt(element.getAttribute('height'));
203 | } catch (error) {
204 |
205 | }
206 | }
207 |
208 | contents.push({
209 | component: 'image',
210 | imageId,
211 | url,
212 | size,
213 | });
214 | } else {
215 | if (item == null) {
216 | item = {
217 | component: 'text',
218 | id: shortid.generate(),
219 | content: [],
220 | };
221 | }
222 |
223 | const firstLine = (i == 0) || (i > 0 && contents.length > 0 && contents[contents.length - 1].component == 'image');
224 |
225 |
226 | if (tag == 'ul' || tag == 'ol') {
227 | for (let k = 0; k < element.childNodes.length; k++) {
228 |
229 | const ro = {
230 | id: shortid.generate(),
231 | text: tag == 'ol' ? (firstLine == true & k == 0 ? `${k + 1}- ` : `\n${k + 1}- `) : ((firstLine === true && k == 0) ? '\u2022 ' : '\n\u2022 '),
232 | len: 2,
233 | stype: [],
234 | styleList: StyleSheet.flatten(convertStyleList(update([], { $push: [tag] }), availableStyles)),
235 | tag,
236 | NewLine: true,
237 | readOnly: true,
238 | };
239 |
240 |
241 | ro.len = ro.text.length;
242 | item.content.push(ro);
243 |
244 | const node = element.childNodes[k];
245 | for (let j = 0; j < node.childNodes.length; j++) {
246 | const child = node.childNodes[j];
247 |
248 | item.content.push(
249 | xmlNodeToItem(child, tag, false, availableStyles),
250 | );
251 | }
252 | }
253 | } else {
254 | if(element.childNodes){
255 | for (let j = 0; j < element.childNodes.length; j++) {
256 | const child = element.childNodes[j];
257 | const childItem = xmlNodeToItem(child, tag, firstLine == false && j == 0, availableStyles);
258 | if (firstLine) {
259 | childItem.NewLine = j == 0;
260 | }
261 | item.content.push(
262 | childItem,
263 | );
264 | }
265 | }
266 | }
267 | }
268 | }
269 | if (item != null) {
270 | contents = update(contents, { $push: [item] });
271 | item = null;
272 | }
273 |
274 | return contents;
275 | }
276 |
277 | function xmlNodeToItem(child, tag, newLine, styleList = null) {
278 | const availableStyles = styleList === null ? defaultStyles : styleList;
279 | let isBold = false;
280 | let isItalic = false;
281 | let isUnderLine = false;
282 | let isOverline = false;
283 | let isGreen = false;
284 | let isBlue = false;
285 | let isRed = false;
286 |
287 | let isBlueMarker = false;
288 | let isOrangeMarker = false;
289 | let isPinkMarker = false;
290 | let isPurpleMarker = false;
291 | let isGreenMarker = false;
292 | let isYellowMarker = false;
293 |
294 | let text = '';
295 | if (child.nodeName === 'span') {
296 | if (child.hasAttribute('style') === true) {
297 | const styles = child.getAttribute('style');
298 | isBold = styles.indexOf('font-weight: bold;') > -1;
299 | isItalic = styles.indexOf('font-style: italic;') > -1;
300 | isOverline = styles.indexOf('text-decoration: line-through;') > -1;
301 | isUnderLine = styles.indexOf('text-decoration: underline;') > -1;
302 | isBlue = styles.indexOf(`color: ${availableStyles.blue.color};`) > -1;
303 | isRed = styles.indexOf(`color: ${availableStyles.red.color};`) > -1;
304 | isGreen = styles.indexOf(`color: ${availableStyles.green.color};`) > -1;
305 | isBlueMarker = styles.indexOf(`background-color: ${availableStyles.blue_hl.backgroundColor};`) > -1;
306 | isGreenMarker = styles.indexOf(`background-color: ${availableStyles.green_hl.backgroundColor};`) > -1;
307 | isPinkMarker = styles.indexOf(`background-color: ${availableStyles.pink_hl.backgroundColor};`) > -1;
308 | isPurpleMarker = styles.indexOf(`background-color: ${availableStyles.purple_hl.backgroundColor};`) > -1;
309 | isYellowMarker = styles.indexOf(`background-color: ${availableStyles.yellow_hl.backgroundColor};`) > -1;
310 | isOrangeMarker = styles.indexOf(`background-color: ${availableStyles.orange_hl.backgroundColor};`) > -1;
311 | }
312 | try {
313 | text = child.childNodes[0].nodeValue;
314 | } catch (error) {
315 |
316 | }
317 | } else {
318 | if(child.nodeValue){
319 | text = child.nodeValue;
320 | } else {
321 | text = '';
322 | }
323 |
324 | }
325 |
326 | const stype = [];
327 | if (isBold) {
328 | stype.push('bold');
329 | }
330 | if (isItalic) {
331 | stype.push('italic');
332 | }
333 | if (isUnderLine) {
334 | stype.push('underline');
335 | }
336 | if (isOverline) {
337 | stype.push('lineThrough');
338 | }
339 | if (isBlue) {
340 | stype.push('blue');
341 | }
342 | if (isGreen) {
343 | stype.push('green');
344 | }
345 | if (isRed) {
346 | stype.push('red');
347 | }
348 |
349 | if (isBlueMarker) {
350 | stype.push('blue_hl');
351 | }
352 |
353 | if (isOrangeMarker) {
354 | stype.push('orange_hl');
355 | }
356 |
357 | if (isYellowMarker) {
358 | stype.push('yellow_hl');
359 | }
360 |
361 | if (isGreenMarker) {
362 | stype.push('green_hl');
363 | }
364 |
365 | if (isPinkMarker) {
366 | stype.push('pink_hl');
367 | }
368 |
369 | if (isPurpleMarker) {
370 | stype.push('purple_hl');
371 | }
372 |
373 | return {
374 | id: shortid.generate(),
375 | text: newLine === true ? `\n${text}` : text,
376 | len: newLine === true ? text.length + 1 : text.length,
377 | stype,
378 | styleList: StyleSheet.flatten(convertStyleList(update(stype, { $push: [tag] }), styleList)),
379 | tag,
380 | NewLine: newLine,
381 | };
382 | }
383 |
384 | export function getInitialObject() {
385 | return {
386 | id: shortid.generate(),
387 | component: 'text',
388 | content: [{
389 | id: shortid.generate(),
390 | text: '',
391 | len: 0,
392 | stype: [],
393 | styleList: [{
394 | fontSize: 20,
395 | }],
396 | tag: 'body',
397 | NewLine: true,
398 | },
399 | ],
400 | };
401 | }
402 |
403 |
404 | function convertStyleList(stylesArr, styleList = null) {
405 | const styls = [];
406 | (stylesArr).forEach((element) => {
407 | const styleObj = txtToStyle(element, styleList);
408 | if (styleObj !== null) styls.push(styleObj);
409 | });
410 |
411 |
412 | return styls;
413 | }
414 |
415 | function txtToStyle(styleName, styleList = null) {
416 | const styles = styleList == null ? defaultStyles : styleList;
417 |
418 | return styles[styleName];
419 | }
420 |
421 | export function getDefaultStyles() {
422 | return defaultStyles;
423 | }
424 |
425 |
426 | const defaultStyles = StyleSheet.create(
427 | {
428 | bold: {
429 | fontWeight: 'bold',
430 | },
431 | italic: {
432 | fontStyle: 'italic',
433 | },
434 | underline: { textDecorationLine: 'underline' },
435 | lineThrough: { textDecorationLine: 'line-through' },
436 | heading: {
437 | fontSize: 25,
438 | },
439 | body: {
440 | fontSize: 20,
441 | },
442 | title: {
443 | fontSize: 30,
444 | },
445 | ul: {
446 | fontSize: 20,
447 | },
448 | ol: {
449 | fontSize: 20,
450 | },
451 | red: {
452 | color: '#d23431',
453 | },
454 | green: {
455 | color: '#4a924d',
456 | },
457 | blue: {
458 | color: '#0560ab',
459 | },
460 | black: {
461 | color: '#33363d',
462 | },
463 | blue_hl: {
464 | backgroundColor: '#34f3f4',
465 | },
466 | green_hl: {
467 | backgroundColor: '#2df149',
468 | },
469 | pink_hl: {
470 | backgroundColor: '#f53ba7',
471 | },
472 | yellow_hl: {
473 | backgroundColor: '#f6e408',
474 | },
475 | orange_hl: {
476 | backgroundColor: '#f07725',
477 | },
478 | purple_hl: {
479 | backgroundColor: '#c925f2',
480 | },
481 | },
482 | );
483 |
--------------------------------------------------------------------------------