├── .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 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | Take Photo 179 | 180 | 181 | 182 | 183 | 184 | Photo Library 185 | 186 | 187 | 188 | 189 | 190 | Cancel 191 | 192 | 193 | 194 | 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 | 269 | 270 | 274 | 275 | 276 | {this.renderColorMenuOptions()} 277 | 278 | 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 | 290 | 291 | 294 | 295 | 296 | {this.renderHighlightMenuOptions()} 297 | 298 | 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 | --------------------------------------------------------------------------------