├── .DS_Store
├── .expo-shared
└── assets.json
├── .gitignore
├── .idea
├── markdown-editor-master.iml
├── misc.xml
├── modules.xml
└── workspace.xml
├── .watchmanconfig
├── App.js
├── MainScreen.js
├── README.md
├── app.json
├── assets
├── icon.png
└── splash.png
├── babel.config.js
├── demo.gif
├── editor
├── index.js
└── src
│ ├── CheckBox.js
│ ├── Constants.js
│ ├── Convertors.js
│ ├── EventEmitter.js
│ ├── Events.js
│ ├── Helpers.js
│ ├── Sketch.js
│ ├── StyledText.js
│ ├── StyledTextInput.js
│ ├── Styles.js
│ ├── TextEditor.js
│ └── TextToolbar.js
├── package-lock.json
├── package.json
└── yarn.lock
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asp3/react-native-rich-editor/a79de689d965a481f0f018242121f29af9dce30f/.DS_Store
--------------------------------------------------------------------------------
/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "7adc6cdde1172c646f8dda7fcb1186d148e59e6d2a40774bd7e03281a653f19c": true,
3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | *.jks
5 | *.p12
6 | *.key
7 | *.mobileprovision
8 |
--------------------------------------------------------------------------------
/.idea/markdown-editor-master.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 | FileSystem
122 | System
123 | FileSystem.write
124 | console
125 |
126 |
127 |
128 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | 1575327062270
196 |
197 |
198 | 1575327062270
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 |
4 | import { Root } from "native-base";
5 |
6 | import Reactotron from 'reactotron-react-native'
7 |
8 | import MainScreen from "./MainScreen";
9 | // import CNScreen from "./CNScreen";
10 |
11 | Reactotron
12 | .configure() // controls connection & communication settings
13 | .useReactNative() // add all built-in react native plugins
14 | .connect() // let's connect!
15 |
16 | console.tron = Reactotron
17 |
18 | export default class App extends React.Component {
19 | render() {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | const styles = StyleSheet.create({
34 | container: {
35 | flex: 1,
36 | // backgroundColor: 'red',
37 | // alignItems: 'center',
38 | // justifyContent: 'center',
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/MainScreen.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View, SafeAreaView, TouchableOpacity } from 'react-native';
3 | import { KeyboardAwareView } from 'react-native-keyboard-aware-view'
4 | import { Container, Header, Body, Title, Right, Left, Button, Icon, Text } from "native-base";
5 |
6 |
7 | import getEmitter from "./editor/src/EventEmitter";
8 | import EVENTS from "./editor/src/Events";
9 | import { contentState } from "./editor/src/Helpers";
10 |
11 | import { TextEditor, TextToolbar } from "./editor/index";
12 |
13 | const eventEmitter = getEmitter()
14 |
15 | let editor = null
16 |
17 | export default class App extends React.Component {
18 |
19 | state = {
20 | extraData: Date.now()
21 | }
22 |
23 | logState () {
24 | eventEmitter.emit(EVENTS.LOG_STATE)
25 | }
26 |
27 | reload () {
28 | if(editor) {
29 | editor.reload()
30 | } else {
31 | console.log("reload");
32 | }
33 | }
34 |
35 | refresh () {
36 | if(editor) {
37 | editor.refresh()
38 | } else {
39 | console.log("refresh");
40 | }
41 | }
42 |
43 | clear () {
44 | if(editor) {
45 | editor.clear()
46 | } else {
47 | console.log("clear");
48 | }
49 | }
50 |
51 | convert () {
52 | eventEmitter.emit(EVENTS.CONVERT_TO_RAW)
53 | }
54 |
55 | onChange = (data) => {
56 | // console.log(data)
57 | this.setState({ extraData: Date.now() })
58 | }
59 |
60 | render() {
61 | return (
62 |
63 |
64 |
65 |
68 |
69 |
70 | Text Editor
71 |
72 |
73 |
76 |
79 |
82 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | { editor = e }}
93 | data={contentState}
94 | onChange={this.onChange}
95 | extraData={this.state.extraData}
96 | />
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | }
105 | }
106 |
107 | const styles = StyleSheet.create({
108 | container: {
109 | flex: 1,
110 | // backgroundColor: 'red',
111 | // alignItems: 'stretch',
112 | // justifyContent: 'center',
113 | // flexDirection: 'column',
114 | },
115 | editor: {
116 | // minHeight: 100,
117 | // width: 300,
118 | paddingLeft: 20,
119 | paddingBottom: 20,
120 | flex: 1,
121 | // backgroundColor: 'red'
122 | }
123 | });
124 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native WYSIWYG
2 |
3 | Rich text editor for React Native
4 |
5 | Supports Draft.js and Markdown
6 |
7 |
8 |
9 | ### To Do
10 |
11 | - [x] Convert from Draft.js contentState
12 | - [x] Convert to Draft.js contentState
13 | - [ ] Convert from Markdown
14 | - [x] Convert to Markdown
15 | - [x] Bold
16 | - [x] Italic
17 | - [x] Underline
18 | - [x] Strikethrough
19 | - [x] Move line up & down
20 | - [x] Bullets (Unordered List)
21 | - [x] Numbered List (Ordered List)
22 | - [x] Blockquote
23 | - [x] Heading 1
24 | - [x] Heading 2
25 | - [x] Heading 3
26 | - [ ] Font colors
27 | - [ ] Tables
28 | - [ ] Insert images
29 | - [ ] Intends
30 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "react-native-markdown-editor",
4 | "slug": "react-native-markdown-editor",
5 | "privacy": "public",
6 | "sdkVersion": "35.0.0",
7 | "platforms": [
8 | "ios",
9 | "android"
10 | ],
11 | "version": "1.0.1",
12 | "orientation": "default",
13 | "icon": "./assets/icon.png",
14 | "splash": {
15 | "image": "./assets/splash.png",
16 | "resizeMode": "contain",
17 | "backgroundColor": "#ffffff"
18 | },
19 | "updates": {
20 | "fallbackToCacheTimeout": 0
21 | },
22 | "assetBundlePatterns": [
23 | "**/*"
24 | ],
25 | "ios": {
26 | "supportsTablet": true
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asp3/react-native-rich-editor/a79de689d965a481f0f018242121f29af9dce30f/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asp3/react-native-rich-editor/a79de689d965a481f0f018242121f29af9dce30f/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asp3/react-native-rich-editor/a79de689d965a481f0f018242121f29af9dce30f/demo.gif
--------------------------------------------------------------------------------
/editor/index.js:
--------------------------------------------------------------------------------
1 | import TextEditor from './src/TextEditor';
2 | import TextToolbar from './src/TextToolbar';
3 |
4 | export {
5 | TextEditor,
6 | TextToolbar,
7 | };
8 |
--------------------------------------------------------------------------------
/editor/src/CheckBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TouchableOpacity, StyleSheet } from 'react-native';
3 | import { MaterialIcons } from '@expo/vector-icons';
4 |
5 | const CheckBox = ({ style = {}, isChecked = false, toggle = () => {} }) => {
6 | const icon = isChecked ? 'check-box' : 'check-box-outline-blank'
7 | return (
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | const styles = StyleSheet.create({
15 | default: {
16 | // backgroundColor: 'red',
17 | },
18 | })
19 |
20 | export default CheckBox
--------------------------------------------------------------------------------
/editor/src/Constants.js:
--------------------------------------------------------------------------------
1 | export const ROW_TYPES = {
2 | TEXT: 'text',
3 | HEADING1: 'heading1',
4 | HEADING2: 'heading2',
5 | HEADING3: 'heading3',
6 | TODOS: 'todos',
7 | HR: 'hr',
8 | BLOCKQUOTE: 'blockquote',
9 | BULLETS: 'bullets',
10 | NUMBERS: 'numbers',
11 | IMAGE: 'image',
12 | }
13 |
14 | export const STYLE_TYPES = {
15 | BOLD: 'bold',
16 | ITALIC: 'italic',
17 | UNDERLINE: 'underline',
18 | STRIKETHROUGH: 'strikethrough',
19 | CODE: 'code',
20 | LINK: 'link',
21 | }
22 |
23 | export const COLORS = [
24 | '#F44336',
25 | '#E91E63',
26 | '#673AB7',
27 | '#3F51B5',
28 | '#2196F3',
29 | '#00BCD4',
30 | '#009688',
31 | '#4CAF50',
32 | '#8BC34A',
33 | '#FFEB3B',
34 | '#FF9800',
35 | '#795548',
36 | '#9E9E9E',
37 | '#607D8B',
38 | '#263238',
39 | ]
--------------------------------------------------------------------------------
/editor/src/Convertors.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import { STYLE_TYPES, ROW_TYPES } from './Constants'
3 |
4 | import { generateId, parseRawBlock } from "./Helpers";
5 |
6 | export const convertToMarkdown = ({ rows = [] }) => {
7 | let markdown = ''
8 |
9 | rows.forEach(row => {
10 | const { blocks = [], type } = row
11 |
12 | if(!row.value) {
13 | markdown += `\n\n`
14 | }
15 |
16 | for (let i = 0; i < blocks.length; i++) {
17 | const block = blocks[i];
18 |
19 | let { text: blockText = '', styles = [] } = block
20 |
21 | if(!blockText || !blockText.length || blockText === '') {
22 | continue
23 | }
24 |
25 | styles = _.uniq(styles);
26 |
27 | if( styles.includes(STYLE_TYPES.BOLD) && styles.includes(STYLE_TYPES.ITALIC) && styles.includes(STYLE_TYPES.UNDERLINE)) {
28 | blockText = `__***${blockText}***__`
29 | } else if (styles.includes(STYLE_TYPES.BOLD) && styles.includes(STYLE_TYPES.ITALIC)) {
30 | blockText = `***${blockText}***`
31 | } else if (styles.includes(STYLE_TYPES.BOLD) && styles.includes(STYLE_TYPES.UNDERLINE)) {
32 | blockText = `__**${blockText}**__`
33 | } else if (styles.includes(STYLE_TYPES.ITALIC) && styles.includes(STYLE_TYPES.UNDERLINE)) {
34 | blockText = `__*${blockText}*__`
35 | } else if (styles.includes(STYLE_TYPES.BOLD) && styles.length === 1) {
36 | blockText = `**${blockText}**`
37 | } else if (styles.includes(STYLE_TYPES.ITALIC) && styles.length === 1) {
38 | blockText = `*${blockText}*`
39 | } else if (styles.includes(STYLE_TYPES.UNDERLINE) && styles.length === 1) {
40 | blockText = `__${blockText}__`
41 | }
42 |
43 | if(type === ROW_TYPES.HEADING1) {
44 | blockText = `# ${blockText}`
45 | } else if (type === ROW_TYPES.HEADING2) {
46 | blockText = `## ${blockText}`
47 | } else if (type === ROW_TYPES.HEADING3) {
48 | blockText = `### ${blockText}`
49 | } else if ( type === ROW_TYPES.BULLETS ) {
50 | blockText = `* ${blockText}`
51 | } else if ( type === ROW_TYPES.BLOCKQUOTE ) {
52 | blockText = `> ${blockText}`
53 | }
54 |
55 | markdown += blockText
56 |
57 | if(blocks.length-1 === i) {
58 | markdown += `\n\n`
59 | }
60 | }
61 |
62 | })
63 |
64 | console.log(markdown)
65 |
66 | return markdown
67 | }
68 |
69 |
70 | export const convertFromRaw = ({ contentState }) => {
71 | const { blocks = [] } = contentState
72 |
73 | const result = []
74 |
75 | blocks.forEach(block => {
76 | const { row, text, type } = parseRawBlock(block)
77 | result.push({ id: generateId(), type, value: text, blocks: row })
78 | })
79 |
80 |
81 | // console.log(result)
82 |
83 | return result
84 | }
85 |
86 | export const convertToRaw = ({ rows = [] }) => {
87 | const result = { blocks: [], entityMap: {} }
88 |
89 | const sample = {
90 | key: '1la1e',
91 | text: '',
92 | type: 'unstyled',
93 | depth: 0,
94 | inlineStyleRanges: [],
95 | entityRanges: [],
96 | data: {},
97 | }
98 |
99 | rows.forEach(row => {
100 | // console.log(item)
101 | // const { row, text, type } = parseRow(item)
102 | // result.push({ id: generateId(), type, value: text, blocks: row })
103 |
104 | const item = {
105 | ...sample,
106 | key: generateId(),
107 | text: row.value,
108 | inlineStyleRanges: []
109 | }
110 |
111 | let type = 'unstyled'
112 |
113 | if (row.type === ROW_TYPES.BULLETS) {
114 | type = 'unordered-list-item'
115 | }
116 |
117 | if (row.type === ROW_TYPES.NUMBERS) {
118 | type = 'ordered-list-item'
119 | }
120 |
121 | item.type = type
122 |
123 | let rangeIndex = 0;
124 |
125 | (row.blocks || []).forEach(block => {
126 |
127 | const offset = rangeIndex;
128 | rangeIndex += block.text.length
129 |
130 | const blockStyles = block.styles || []
131 |
132 | for (let i = 0; i < blockStyles.length; i++) {
133 | const style = blockStyles[i];
134 |
135 | const inlineStyleRange = {
136 | offset: offset,
137 | length: block.text.length,
138 | style: style.toUpperCase()
139 | }
140 | item.inlineStyleRanges.push(inlineStyleRange)
141 | }
142 | //
143 | })
144 |
145 | result.blocks.push(item)
146 | })
147 |
148 | // console.log(JSON.stringify(result))
149 |
150 | return result
151 | }
152 |
--------------------------------------------------------------------------------
/editor/src/EventEmitter.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'fbemitter'
2 |
3 | let emitter = null
4 |
5 | export default () => {
6 | if (!emitter) {
7 | emitter = new EventEmitter()
8 | }
9 | return emitter
10 | }
11 |
--------------------------------------------------------------------------------
/editor/src/Events.js:
--------------------------------------------------------------------------------
1 | export default {
2 | TOGGLE_BOLD: 'RN_WYSIWYG_EDITOR/TOGGLE_BOLD',
3 | HIDE_KEYBOARD: 'RN_WYSIWYG_EDITOR/HIDE_KEYBOARD',
4 | SHOW_INSERT_BLOCK: 'RN_WYSIWYG_EDITOR/SHOW_INSERT_BLOCK',
5 | SHOW_UPLOAD_FILE: 'RN_WYSIWYG_EDITOR/SHOW_UPLOAD_FILE',
6 | DELETE_BLOCK: 'RN_WYSIWYG_EDITOR/DELETE_BLOCK',
7 | CHANGE_BLOCK_TYPE: 'RN_WYSIWYG_EDITOR/CHANGE_BLOCK_TYPE',
8 | TOGGLE_STYLE: 'RN_WYSIWYG_EDITOR/TOGGLE_STYLE',
9 | CHANGE_COLOR_STYLE: 'RN_WYSIWYG_EDITOR/CHANGE_COLOR_STYLE',
10 | CLEAR_STYLES: 'RN_WYSIWYG_EDITOR/CLEAR_STYLES',
11 | ALIGN_ROW: 'RN_WYSIWYG_EDITOR/ALIGN_ROW',
12 | TOGGLE_FULL_SCREEN: 'RN_WYSIWYG_EDITOR/TOGGLE_FULL_SCREEN',
13 | ACTIVE_STYLE_CHANGED: 'RN_WYSIWYG_EDITOR/ACTIVE_STYLE_CHANGED',
14 | CHANGE_BLOCK_INDEX: 'RN_WYSIWYG_EDITOR/CHANGE_BLOCK_INDEX',
15 | BROWSE_HISTORY: 'RN_WYSIWYG_EDITOR/BROWSE_HISTORY',
16 | CHANGE_BLOCK_INDENT: 'RN_WYSIWYG_EDITOR/CHANGE_BLOCK_INDENT',
17 | DUPLICATE_ROW: 'RN_WYSIWYG_EDITOR/DUPLICATE_ROW',
18 | LOG_STATE: 'RN_WYSIWYG_EDITOR/LOG_STATE',
19 | CONVERT_TO_RAW: 'RN_WYSIWYG_EDITOR/CONVERT_TO_RAW',
20 | ROW_TYPE_CHANGED: 'RN_WYSIWYG_EDITOR/ROW_TYPE_CHANGED',
21 | REFRESH: 'RN_WYSIWYG_EDITOR/REFRESH',
22 | RELOAD: 'RN_WYSIWYG_EDITOR/RELOAD',
23 | }
--------------------------------------------------------------------------------
/editor/src/Helpers.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | import shortid from "shortid";
3 |
4 | import { STYLE_TYPES, ROW_TYPES } from './Constants'
5 |
6 | export const generateId = () => shortid()
7 |
8 | export const contentState = {"blocks":[{"key":"1la1e","text":"thi wil contain bold","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":16,"length":4,"style":"BOLD"}],"entityRanges":[],"data":{}},{"key":"5bl2j","text":"thi wil contain italic","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":16,"length":6,"style":"ITALIC"}],"entityRanges":[],"data":{}},{"key":"9sen6","text":"thi wil contain underline","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":16,"length":9,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"a11h3","text":"thi wil contain bold italic","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":16,"length":4,"style":"BOLD"},{"offset":21,"length":6,"style":"BOLD"},{"offset":21,"length":6,"style":"ITALIC"}],"entityRanges":[],"data":{}},{"key":"3g7tj","text":"thi wil contain bold underline","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":16,"length":4,"style":"BOLD"},{"offset":21,"length":9,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"o4sp","text":"thi wil contain italic underline","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":16,"length":6,"style":"BOLD"},{"offset":23,"length":9,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"dtmbt","text":"thi wil contain bold italic underline","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":16,"length":4,"style":"BOLD"},{"offset":21,"length":6,"style":"ITALIC"},{"offset":28,"length":9,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"596kp","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"8jtu1","text":"combine bolditalic","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":8,"length":10,"style":"BOLD"},{"offset":12,"length":6,"style":"ITALIC"}],"entityRanges":[],"data":{}},{"key":"86phc","text":"combine boldunderline","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":8,"length":13,"style":"BOLD"},{"offset":12,"length":9,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"69jv4","text":"combine italicunderline","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":8,"length":6,"style":"ITALIC"},{"offset":14,"length":9,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"kpe8","text":"combine bolditalicunderline","type":"unstyled","depth":0,"inlineStyleRanges":[{"offset":8,"length":4,"style":"BOLD"},{"offset":12,"length":6,"style":"ITALIC"},{"offset":18,"length":9,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"292du","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"6g2mu","text":"test ","type":"unordered-list-item","depth":0,"inlineStyleRanges":[{"offset":0,"length":5,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"a2fp3","text":"asdasdasda s","type":"unordered-list-item","depth":0,"inlineStyleRanges":[{"offset":0,"length":12,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"4u38f","text":"asd asd","type":"unordered-list-item","depth":0,"inlineStyleRanges":[{"offset":0,"length":7,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"28gr5","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}},{"key":"2tjri","text":"test ","type":"ordered-list-item","depth":0,"inlineStyleRanges":[{"offset":0,"length":5,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"8oduf","text":"asda sd","type":"ordered-list-item","depth":0,"inlineStyleRanges":[{"offset":0,"length":7,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"dtld2","text":"asdasdd ","type":"ordered-list-item","depth":0,"inlineStyleRanges":[{"offset":0,"length":8,"style":"UNDERLINE"}],"entityRanges":[],"data":{}},{"key":"dhfar","text":"asd dasd asd","type":"ordered-list-item","depth":0,"inlineStyleRanges":[{"offset":0,"length":12,"style":"UNDERLINE"},{"offset":4,"length":4,"style":"ITALIC"},{"offset":10,"length":2,"style":"BOLD"}],"entityRanges":[],"data":{}}],"entityMap":{}}
9 |
10 |
11 | const getBlockType = type => {
12 | if (type === 'unordered-list-item') {
13 | return ROW_TYPES.BULLETS
14 | }
15 |
16 | if (type === 'ordered-list-item') {
17 | return ROW_TYPES.NUMBERS
18 | }
19 |
20 | return ROW_TYPES.TEXT
21 | }
22 |
23 | export const parseRawBlock = block => {
24 | const {text, inlineStyleRanges, type} = block
25 |
26 | if(!text || !text.length) {
27 | return { row: [ { text: '' } ], type: getBlockType(type), text: '' }
28 | }
29 | const result = []
30 |
31 | let styleOffsets = []
32 |
33 | inlineStyleRanges.forEach(inlineStyleRange => {
34 | styleOffsets.push(inlineStyleRange['offset'])
35 | styleOffsets.push(inlineStyleRange['length'] + inlineStyleRange['offset'])
36 | })
37 |
38 | styleOffsets = styleOffsets.filter((item, pos) => {
39 | return styleOffsets.indexOf(item) == pos;
40 | });
41 |
42 |
43 | styleOffsets.sort((a, b) => a - b)
44 |
45 | const firstIndex = styleOffsets[0]
46 | if(firstIndex > 0) {
47 | const item = {
48 | text: text.substring(0, firstIndex),
49 | }
50 | result.push(item)
51 | }
52 |
53 | styleOffsets.forEach((a, index) => {
54 | const b = styleOffsets[index+1]
55 | if(b) {
56 | const item = {
57 | styles: []
58 | }
59 |
60 | inlineStyleRanges.forEach(inlineStyleRange => {
61 | const start = inlineStyleRange['offset']
62 | const end = inlineStyleRange['length'] + inlineStyleRange['offset']
63 |
64 | if(start >= a && b <= end ) {
65 | item.text = text.substring(a, b)
66 | if(inlineStyleRange.style) {
67 | item.styles.push(inlineStyleRange.style.toLowerCase())
68 | }
69 | }
70 | })
71 |
72 | result.push(item)
73 | }
74 | })
75 |
76 | const lastIndex = styleOffsets[styleOffsets.length-1]
77 | if(lastIndex < text.length) {
78 | const item = {
79 | text: text.substring(lastIndex, text.length),
80 | }
81 | result.push(item)
82 | }
83 |
84 | if(!inlineStyleRanges.length) {
85 | const item = { row: [ { text: text } ], type: getBlockType(type), text: text }
86 | result.push(item)
87 | }
88 |
89 | return { row: result, text, type: getBlockType(type) }
90 | }
91 |
92 | export const getRowValueFromBlocks = ({ row = {} }) => {
93 | const { blocks = [] } = row
94 | return blocks.map(item => item.text).join('')
95 | }
96 |
97 | export const getCurrentBlockInRow = ({ selection, row, cursorAt = null }) => {
98 | if(cursorAt === null && selection.id !== row.id) { return null }
99 |
100 | let rowCursorAt = cursorAt !== null ? cursorAt : selection.start
101 |
102 | let currentBlock = {}
103 | let blockIndex = null
104 | let pointerAt = null
105 | let position = null
106 |
107 | const { blocks = [] } = row
108 |
109 | const value = getRowValueFromBlocks({ row })
110 |
111 | // Cursor is At the Beginning
112 | if(rowCursorAt === 0) {
113 | currentBlock = blocks[0] || { text: '' }
114 | blockIndex = 0
115 | pointerAt = 0
116 | position = "start"
117 | } else if(rowCursorAt === value.length) { // Cursor is At the End
118 | const lastBlockIndex = blocks.length-1
119 | currentBlock = blocks[lastBlockIndex] || { text: '' }
120 | blockIndex = lastBlockIndex
121 | pointerAt = (currentBlock.text || '').length
122 | position = "end"
123 | } else {
124 | position = "middle"
125 | // Cursor is at middle text
126 | const textBeforePointer = value.substring(0, rowCursorAt)
127 |
128 | let blockTexts = ""
129 |
130 | for (let i = 0; i < blocks.length; i++) {
131 | const block = blocks[i] || {};
132 |
133 | const { text: blockText = '', styles: blockStyles = [] } = block
134 | blockTexts += blockText
135 |
136 | if(blockTexts.length >= textBeforePointer.length) {
137 |
138 | currentBlock = block
139 | blockIndex = i
140 |
141 | // const textLengthAfterPointerInBlock = blockTexts.length - textBeforePointer.length
142 | const textLengthAfterPointerInBlock = blockTexts.substring(textBeforePointer.length, blockTexts.length).length
143 | pointerAt = blockText.length - textLengthAfterPointerInBlock
144 | break;
145 | }
146 | }
147 |
148 | // console.log("Cursor is At middle text::", pointerAt)
149 | }
150 |
151 | return { block: currentBlock, blockIndex, pointerAt, position }
152 | }
153 |
154 | export const getSelectedBlocks = ({ selection, row }) => {
155 | const { start = 0, end = 0 } = selection
156 | const startBlock = getCurrentBlockInRow({ row, cursorAt: start })
157 | const endBlock = getCurrentBlockInRow({ row, cursorAt: end })
158 | return { startBlock, endBlock }
159 | }
160 |
161 | export const splitString = (value = '', index) => [value.substring(0, index) , value.substring(index)]
162 |
163 | // FIXME: re-write
164 | export const mergeNewStyles = (currentStyles = [], newStyles = [], oldStyles = []) => {
165 |
166 | let styles = currentStyles // _.uniq(currentStyles)
167 |
168 | console.tron.display({
169 | name: 'mergeNewStyles',
170 | value: { currentStyles, newStyles, oldStyles },
171 | })
172 |
173 | if(oldStyles.includes(STYLE_TYPES.BOLD) && !newStyles.includes(STYLE_TYPES.BOLD)) {
174 | styles = styles.filter(i => i !== STYLE_TYPES.BOLD)
175 | }
176 |
177 | if(oldStyles.includes(STYLE_TYPES.ITALIC) && !newStyles.includes(STYLE_TYPES.ITALIC)) {
178 | styles = styles.filter(i => i !== STYLE_TYPES.ITALIC)
179 | }
180 |
181 | if(oldStyles.includes(STYLE_TYPES.UNDERLINE) && !newStyles.includes(STYLE_TYPES.UNDERLINE)) {
182 | styles = styles.filter(i => i !== STYLE_TYPES.UNDERLINE)
183 | }
184 |
185 | if(oldStyles.includes(STYLE_TYPES.STRIKETHROUGH) && !newStyles.includes(STYLE_TYPES.STRIKETHROUGH)) {
186 | styles = styles.filter(i => i !== STYLE_TYPES.STRIKETHROUGH)
187 | }
188 |
189 | if(oldStyles.includes(STYLE_TYPES.CODE) && !newStyles.includes(STYLE_TYPES.CODE)) {
190 | styles = styles.filter(i => i !== STYLE_TYPES.CODE)
191 | }
192 |
193 | if(oldStyles.includes(STYLE_TYPES.LINK) && !newStyles.includes(STYLE_TYPES.LINK)) {
194 | styles = styles.filter(i => i !== STYLE_TYPES.LINK)
195 | }
196 |
197 | styles = [...styles, ...newStyles]
198 |
199 | styles = _.uniq(styles)
200 |
201 | return styles
202 | }
203 |
204 | export const attachStylesToSelectedText = ({ selection, row, newStyles, oldStyles }) => {
205 | const { startBlock, endBlock } = getSelectedBlocks({ selection, row })
206 |
207 | const { blocks } = row
208 |
209 | let selectedText = ''
210 |
211 | const newBlocks = []
212 |
213 | for (let i = 0; i < blocks.length; i++) {
214 | if(i >= startBlock.blockIndex && i <= endBlock.blockIndex) {
215 | const block = blocks[i];
216 | const { text } = block
217 |
218 | let blockText = text
219 |
220 | if (startBlock.blockIndex === endBlock.blockIndex) {
221 | const blockPrevText = text.substring(0, startBlock.pointerAt)
222 | const blockSelectedText = text.substring(startBlock.pointerAt, endBlock.pointerAt)
223 | const blockNextText = text.substring(endBlock.pointerAt, text.length)
224 |
225 | console.log("Same blog", blockSelectedText, startBlock.pointerAt, endBlock.pointerAt)
226 |
227 | const { styles: currentStyles = [] } = block
228 |
229 | const prevBlock = { text: blockPrevText, styles: currentStyles }
230 | const newBlock = { text: blockSelectedText, styles: mergeNewStyles(currentStyles, newStyles, oldStyles) }
231 | const nextBlock = { text: blockNextText, styles: currentStyles }
232 |
233 | newBlocks.push(prevBlock)
234 | newBlocks.push(newBlock)
235 | newBlocks.push(nextBlock)
236 |
237 | } else if(i === startBlock.blockIndex) {
238 | textParts = splitString(text, startBlock.pointerAt)
239 | const { styles: currentStyles = [] } = block
240 | const prevBlock = { text: textParts[0], styles: currentStyles }
241 | const newBlock = { text: textParts[1], styles: mergeNewStyles(currentStyles, newStyles, oldStyles) }
242 | newBlocks.push(prevBlock)
243 | newBlocks.push(newBlock)
244 | } else if (i === endBlock.blockIndex) {
245 | blockText = text.substring(0, endBlock.pointerAt)
246 | textParts = splitString(text, endBlock.pointerAt)
247 | const { styles: currentStyles = [] } = block
248 | const newBlock = { text: textParts[0], styles: mergeNewStyles(currentStyles, newStyles, oldStyles) }
249 | const nextBlock = { text: textParts[1], styles: currentStyles }
250 | newBlocks.push(newBlock)
251 | newBlocks.push(nextBlock)
252 | } else {
253 | blockText = text
254 | const { styles: currentStyles = [] } = block
255 | const newBlock = { text: text, styles: mergeNewStyles(currentStyles, newStyles, oldStyles) }
256 | newBlocks.push(newBlock)
257 | }
258 |
259 | selectedText += blockText
260 | }
261 | }
262 |
263 | let p1 = blocks.slice(0, startBlock.blockIndex)
264 | let p2 = blocks.slice(endBlock.blockIndex+1)
265 |
266 | // if (startBlock.blockIndex === endBlock.blockIndex) {
267 |
268 | // }
269 |
270 | const data = [...p1, ...newBlocks, ...p2].filter(item => !!item.text && item.text.length > 0)
271 |
272 | // blocks.splice(startBlock.blockIndex, endBlock.blockIndex-startBlock.blockIndex+1);
273 |
274 | console.tron.display({
275 | name: 'newBlocks',
276 | value: { newBlocks, selectedText, blocks, data, startBlock, endBlock },
277 | })
278 |
279 |
280 | return { blocks: data }
281 | }
282 |
283 | export const removeSelectedText = ({ selection, row }) => {
284 | const { blocks = [], value = '' } = row
285 |
286 | if(selection.end - selection.start === value.length) {
287 | return []
288 | }
289 |
290 | const { startBlock, endBlock } = getSelectedBlocks({ selection, row })
291 |
292 | console.tron.display({
293 | name: 'removeSelectedText',
294 | value: { startBlock, endBlock },
295 | })
296 |
297 | const newBlocks = []
298 |
299 | let p1 = blocks.slice(0, startBlock.blockIndex)
300 | let p2 = blocks.slice(endBlock.blockIndex+1)
301 |
302 | for (let i = 0; i < blocks.length; i++) {
303 | if(i >= startBlock.blockIndex && i <= endBlock.blockIndex) {
304 | const block = blocks[i];
305 |
306 | const { text, styles: currentStyles = [] } = block
307 | // let blockText = text
308 |
309 | if (startBlock.blockIndex === endBlock.blockIndex) {
310 | let blockPrevText = text.substring(0, startBlock.pointerAt)
311 | if(startBlock.pointerAt === text.length) {
312 | blockPrevText = text.slice(0, -1);
313 | }
314 | const prevBlock = { text: blockPrevText, styles: currentStyles }
315 | newBlocks.push(prevBlock)
316 | } else if(i === startBlock.blockIndex) {
317 | textParts = splitString(text, startBlock.pointerAt)
318 | const { styles: currentStyles = [] } = block
319 | const prevBlock = { text: textParts[0], styles: currentStyles }
320 | newBlocks.push(prevBlock)
321 | } else if (i === endBlock.blockIndex) {
322 | // blockText = text.substring(0, endBlock.pointerAt)
323 | textParts = splitString(text, endBlock.pointerAt)
324 | const { styles: currentStyles = [] } = block
325 | const nextBlock = { text: textParts[1], styles: currentStyles }
326 | newBlocks.push(nextBlock)
327 | } else {
328 | // blockText = text
329 | // const { styles: currentStyles = [] } = block
330 | // const newBlock = { text: text, styles: [ ...currentStyles, ...newStyles ] }
331 | // newBlocks.push(newBlock)
332 | }
333 |
334 | // selectedText += blockText
335 | }
336 | }
337 |
338 | if (startBlock.blockIndex === endBlock.blockIndex) {
339 | console.log("Deleted one character")
340 | }
341 |
342 | const data = [...p1, ...newBlocks, ...p2].filter(i => !!i.text)
343 |
344 | return data
345 | }
346 |
347 | export const splitRow = ({ row, selection }) => {
348 |
349 | if(selection.id !== row.id) { return }
350 |
351 | let rows = []
352 |
353 | const { type, align = 'auto', blocks = [] } = row
354 |
355 | const value = getRowValueFromBlocks({ row })
356 |
357 | const rowBlocks = blocks.filter(item => item.text)
358 |
359 | const textParts = splitString(value, selection.start)
360 |
361 | const row1 = { id: generateId(), value: textParts[0], type, align }
362 | const row2 = { id: generateId(), value: textParts[1], type, align }
363 |
364 | const currentBlock = getCurrentBlockInRow({ row, selection })
365 |
366 | let blockTexts = ''
367 | for (let i = 0; i < rowBlocks.length; i++) {
368 | const block = rowBlocks[i];
369 | blockTexts += block.text
370 | if(blockTexts.length >= row1.value.length) {
371 |
372 | let row1Blocks = rowBlocks.filter((item, index) => index < i) || []
373 |
374 | const blockTextParts = splitString(block.text, currentBlock.pointerAt)
375 |
376 |
377 | if(blockTextParts[0]) {
378 | row1Blocks.push({ text: blockTextParts[0], styles: block.styles })
379 | }
380 |
381 | let row2Blocks = rowBlocks.filter((item, index) => index > i) || []
382 | if(blockTextParts[1]) {
383 | row2Blocks.unshift({ text: blockTextParts[1], styles: block.styles })
384 | }
385 |
386 | row1.blocks = row1Blocks.filter(item => item.text)
387 | row2.blocks = row2Blocks.filter(item => item.text)
388 |
389 | break;
390 | }
391 | }
392 |
393 | rows = [row1, row2]
394 |
395 | console.tron.display({
396 | name: 'rows',
397 | value: { props: rows },
398 | })
399 |
400 | return rows
401 | }
402 |
403 | export const insertAt = (main_string, ins_string, pos) => {
404 | if(typeof(pos) == "undefined") {
405 | pos = 0;
406 | }
407 | if(typeof(ins_string) == "undefined") {
408 | ins_string = '';
409 | }
410 | return main_string.slice(0, pos) + ins_string + main_string.slice(pos);
411 | }
412 |
--------------------------------------------------------------------------------
/editor/src/Sketch.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import { View, TouchableOpacity, ActivityIndicator, TextInput, FlatList, Slider, Modal, SafeAreaView, ScrollView } from 'react-native';
4 | import * as ExpoPixi from 'expo-pixi';
5 |
6 | import { ActionSheet, Container, Header, Body, Left, Right, Title, Button, Footer, Icon, Text } from "native-base";
7 | import { KeyboardAwareView } from 'react-native-keyboard-aware-view'
8 | import TextToolbar from "./TextToolbar";
9 |
10 | import {
11 | generateId,
12 | getCurrentBlockInRow,
13 | removeSelectedText,
14 | insertAt,
15 | attachStylesToSelectedText,
16 | mergeNewStyles,
17 | contentState,
18 | getSelectedBlocks,
19 | splitRow
20 | } from "./Helpers";
21 |
22 | import { convertToMarkdown, convertToRaw, convertFromRaw, } from "./Convertors";
23 |
24 | import getEmitter from "./EventEmitter";
25 | import EVENTS from "./Events";
26 |
27 | import styles from "./Styles";
28 | import StyledText from "./StyledText";
29 |
30 | import { ROW_TYPES } from "./Constants";
31 |
32 |
33 | const eventEmitter = getEmitter()
34 |
35 | const listeners = {}
36 |
37 | const COLORS = [ '#2fbbf8', '#3efb43', '#fc39f5', '#e72535', '#fb7d34', '#fcfcfc' ]
38 |
39 | const ColorDot = ({ color: backgroundColor, selected: selectedColor, setColor }) => {
40 | return (
41 | setColor(backgroundColor)} style={[styles.colorDot, {backgroundColor}]}>
42 | {selectedColor === backgroundColor && }
43 |
44 | )
45 | }
46 |
47 | class SketchModal extends React.Component {
48 |
49 | sketch = null
50 |
51 | lastBlock = {}
52 |
53 | state = {
54 | image: null,
55 | initializing: true,
56 | strokeColor: COLORS[0],
57 | strokeWidth: Math.random() * 30 + 10,
58 | strokeAlpha: 1,
59 | lines: [
60 | {
61 | points: [{ x: 300, y: 300 }, { x: 600, y: 300 }, { x: 450, y: 600 }, { x: 300, y: 300 }],
62 | color: 0xff00ff,
63 | alpha: 1,
64 | width: 10,
65 | },
66 | ],
67 | }
68 |
69 | setColor = strokeColor => this.setState({ strokeColor })
70 | setWidth = strokeWidth => this.setState({ strokeWidth })
71 |
72 | onReady = () => { console.log("onReady") }
73 |
74 | onChangeAsync = async () => {
75 | const { uri } = await this.sketch.takeSnapshotAsync();
76 | this.setState({ image: uri });
77 | };
78 |
79 | undo = () => {
80 | this.sketch.undo();
81 | }
82 |
83 | onSave = () => {
84 | const { image } = this.state
85 | console.log(image)
86 | const { onSave = () => {} } = this.props
87 | onSave(image)
88 | }
89 |
90 | render() {
91 | const { isSketchVisible, onCancel } = this.props
92 | const { strokeColor = '', strokeWidth, lines, strokeAlpha, } = this.state
93 |
94 | return (
95 |
101 |
102 |
103 |
104 |
107 |
108 |
109 | Sketch
110 |
111 |
112 |
113 |
114 |
115 |
116 | {COLORS.map(item => )}
117 |
118 |
119 |
120 | this.setState({ strokeAlpha: e })}
124 | step={0.1}
125 | value={strokeAlpha}
126 | style={{ flex: 1 }}
127 | />
128 |
129 |
130 |
131 | this.setState({ strokeWidth: e })}
135 | step={1}
136 | value={strokeWidth}
137 | style={{ flex: 1 }}
138 | />
139 |
140 |
141 |
142 | (this.sketch = ref)}
144 | style={styles.sketch}
145 | strokeColor={`0x${strokeColor.replace('#', '')}`}
146 | strokeWidth={strokeWidth}
147 | strokeAlpha={strokeAlpha}
148 | onChange={this.onChangeAsync}
149 | onReady={this.onReady}
150 | initialLines={lines}
151 | />
152 |
153 |
154 |
166 |
167 |
168 | );
169 | }
170 | }
171 |
172 | export default SketchModal
--------------------------------------------------------------------------------
/editor/src/StyledText.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, StyleSheet } from 'react-native';
3 |
4 | import { ROW_TYPES } from "./Constants";
5 |
6 | const StyledText = ({ text, textStyles = [], type = ROW_TYPES.TEXT, isCompleted = false }) => {
7 |
8 | let inputStyles = [styles.default]
9 |
10 | // Block Styles
11 | textStyles.forEach(item => {
12 | const key = item.toLowerCase()
13 | if(styles[key]) {
14 | inputStyles.push(styles[key])
15 | }
16 |
17 | if(key.includes('fill')) {
18 | inputStyles.push({ backgroundColor: key.split('-')[1] })
19 | }
20 |
21 | if(key.includes('color')) {
22 | inputStyles.push({ color: key.split('-')[1] })
23 | }
24 | });
25 |
26 | // Row Styles
27 | if(styles[type]) {
28 | inputStyles.push(styles[type])
29 | }
30 |
31 | if(type === ROW_TYPES.TODOS && isCompleted === true) {
32 | inputStyles = [styles.default, styles.strikethrough]
33 | }
34 |
35 | return (
36 | {text}
37 | )
38 | }
39 |
40 | const styles = StyleSheet.create({
41 | default: {
42 | // backgroundColor: 'red',
43 | },
44 | bold: {
45 | fontWeight: 'bold'
46 | },
47 | italic: {
48 | fontStyle: 'italic'
49 | },
50 | underline: {
51 | textDecorationLine: 'underline'
52 | },
53 | link: {
54 | textDecorationLine: 'underline',
55 | color: '#2196f3',
56 | },
57 | strikethrough: {
58 | textDecorationLine: 'line-through',
59 | color: 'gray',
60 | },
61 | code: {
62 | backgroundColor: '#e3e3e3',
63 | color: 'red',
64 | paddingHorizontal: 5,
65 | paddingVertical: 1,
66 | borderRadius: 2
67 | },
68 | heading1: {
69 | fontSize: 25,
70 | },
71 | heading2: {
72 | fontSize: 21,
73 | },
74 | heading3: {
75 | fontSize: 18,
76 | },
77 | })
78 |
79 | export default StyledText
--------------------------------------------------------------------------------
/editor/src/StyledTextInput.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TextInput, View } from 'react-native';
3 |
4 | import StyledText from "./StyledText";
5 |
6 | class StyledInput extends React.Component {
7 |
8 | state = {
9 | now: null,
10 | hide: false,
11 | value: ''
12 | }
13 |
14 | input = null
15 |
16 | componentDidMound() {
17 | const { value = '' } = this.props
18 | this.setState({ value })
19 | }
20 |
21 | componentDidUpdate(prevProps) {
22 | // console.log(prevProps.row)
23 | if(prevProps.row.text !== this.props.row.text) {
24 | console.log(prevProps.row.text, this.props.row.text)
25 | this.forceUpdate()
26 | }
27 | }
28 |
29 | handleKeyPress = (e) => {
30 | const { row, index } = this.props
31 | this.props.handleKeyPress({ row, index })(e)
32 | }
33 |
34 | onChangeText = (nv = '') => {
35 | const { row, index, onChangeText } = this.props
36 | const newValue = nv.replace(/\n/, '')
37 | this.setState({ value: newValue })
38 | onChangeText({ row, index })(nv)
39 | }
40 |
41 | getValue = () => {
42 | return this.state.value
43 | }
44 |
45 | focus = () => {
46 | if(this.input) {
47 | this.input.focus()
48 | }
49 | }
50 |
51 | setSelection = ({ start, end }) => {
52 | const { row, index, onSelectionChange } = this.props
53 |
54 | this.input.setNativeProps({ selection: { start, end } })
55 | setTimeout(() => {
56 | this.input.setNativeProps({ selection: { start, end } })
57 | console.log({ start, end })
58 | // setTimeout(() => {
59 | // onSelectionChange({ row, index })({ nativeEvent: { selection: { start, end } } })
60 | // setTimeout(() => {
61 | // onSelectionChange({ row, index })({ nativeEvent: { selection: { start, end } } })
62 | // });
63 | // });
64 | })
65 | }
66 |
67 | refresh = ({ focus = false } = {}, callback = () => {}) => {
68 | this.setState({ hide: true }, () => {
69 | setTimeout(() => {
70 | this.setState({ hide: false }, () => {
71 | if(focus) {
72 | this.input.focus()
73 | }
74 | callback()
75 | })
76 | }, 0);
77 | })
78 | }
79 |
80 | onEndEditing = () => {
81 | this.refresh()
82 | }
83 |
84 | stylesChanged = ({ pointerAt, row, keyValue }) => {
85 | const { row: { value = '' }, index } = this.props
86 | this.refresh({ focus: true }, () => {
87 | if (value.length > pointerAt) {
88 | this.setSelection({ start: pointerAt + 1, end: pointerAt + 1 })
89 | } else {
90 | // const newValue = row.blocks.map(i => i.text).join('')
91 | // this.props.onSelectionChange({ row: this.props.row, index, value: newValue })({ nativeEvent: { selection: { start: pointerAt + 1, end: pointerAt + 1 } } })
92 | }
93 | })
94 | }
95 |
96 | render () {
97 |
98 | const {
99 | row,
100 | index,
101 | placeholder,
102 |
103 | onSubmitEditing,
104 | onFocus,
105 | onChangeText,
106 | handleKeyPress,
107 | onSelectionChange,
108 |
109 | textInput,
110 | inputStyles,
111 | alignStyles,
112 | } = this.props
113 |
114 | const { blocks = [] } = row
115 |
116 | if(this.state.hide) {
117 | return (
118 |
119 | {blocks.map((block, i) => (
120 |
127 | ))}
128 |
129 | )
130 | }
131 |
132 | return (
133 | { this.input = c; }}
135 | underlineColorAndroid="transparent"
136 | placeholder={placeholder}
137 | onSubmitEditing={onSubmitEditing({ row, index })}
138 | onFocus={onFocus({ row, index })}
139 | onChangeText={this.onChangeText}
140 | onKeyPress={this.handleKeyPress}
141 | onSelectionChange={onSelectionChange({ row, index })}
142 | onEndEditing={this.onEndEditing}
143 | style={[textInput, inputStyles, alignStyles]}
144 | clearButtonMode="never"
145 | blurOnSubmit={false}
146 | autoCorrect={false}
147 | autoCapitalize="none"
148 | returnKeyType="default"
149 | multiline={!false}
150 | scrollEnabled={false}
151 | >
152 | {blocks.map((block, i) => (
153 |
160 | ))}
161 |
162 | )
163 | }
164 | }
165 |
166 | export default StyledInput
--------------------------------------------------------------------------------
/editor/src/Styles.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, TextInput, FlatList, Keyboard, StyleSheet } from 'react-native';
3 |
4 | export default StyleSheet.create({
5 | flatList: {
6 | flex: 1,
7 | backgroundColor: 'white'
8 | },
9 | container: {
10 | flex: 1,
11 | },
12 | editor: {
13 | padding: 20,
14 | flex: 1,
15 | },
16 | contentContainerStyle: {
17 | // flex: 1
18 | },
19 | text: {
20 | fontSize: 16,
21 | },
22 | heading1: {
23 | fontSize: 25,
24 | },
25 | heading2: {
26 | fontSize: 21,
27 | },
28 | heading3: {
29 | fontSize: 18,
30 | },
31 | blockquote: {
32 | fontStyle: 'italic',
33 | borderLeftWidth: 2,
34 | paddingLeft: 10,
35 | paddingVertical: 2,
36 | marginVertical: 3,
37 | },
38 | textInput: {
39 | flex: 1,
40 | paddingTop: 0,
41 | },
42 | row: {
43 | flexDirection: 'row',
44 | flex: 1,
45 | marginBottom: 2,
46 | },
47 | hr: {
48 | height: 1,
49 | flex: 1,
50 | backgroundColor: '#e3e3e3',
51 | marginVertical: 10,
52 | },
53 | imageRow: {
54 | flex: 1,
55 | marginVertical: 15,
56 | borderRadius: 5,
57 | borderWidth: 1,
58 | borderRadius: 15,
59 | borderColor: '#e3e3e3',
60 | overflow: 'hidden',
61 | backgroundColor: 'white'
62 | },
63 | image: {
64 | width: '100%',
65 | height: 200,
66 | },
67 | bullet: {
68 | fontWeight: 'bold',
69 | marginRight: 5
70 | },
71 | numberOrder: {
72 | width: 20,
73 | marginRight: 2
74 | },
75 | checkbox: {
76 | paddingRight: 5,
77 | },
78 | sketchContainer: {
79 | flex: 1,
80 | padding: 20
81 | },
82 | sketch: {
83 | flex: 1,
84 | borderWidth: 1,
85 | borderRadius: 15,
86 | borderColor: '#e3e3e3',
87 | backgroundColor: '#ffffff'
88 | },
89 | colorsContainer: {
90 | flexDirection: 'row',
91 | justifyContent: 'space-between',
92 | marginHorizontal: 20,
93 | marginTop: 20
94 | },
95 | colorDot: {
96 | width: 24,
97 | height: 24,
98 | borderRadius: 12,
99 | justifyContent: 'center',
100 | alignItems: 'center',
101 | },
102 | colorDotSelected: {
103 | width: 10,
104 | height: 10,
105 | borderRadius: 5,
106 | backgroundColor: 'black'
107 | },
108 | sketchFooter: {
109 | flex: 1,
110 | flexDirection: 'row',
111 | justifyContent: 'space-between',
112 | alignItems: 'center'
113 | }
114 | })
115 |
--------------------------------------------------------------------------------
/editor/src/TextEditor.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import _ from 'lodash';
3 | import * as Permissions from 'expo-permissions';
4 | import * as ImagePicker from 'expo-image-picker';
5 | import { Text, View, ActivityIndicator, TextInput, FlatList, Keyboard, Modal, SafeAreaView, Image, TouchableOpacity } from 'react-native';
6 | import { ActionSheet, Container, Header, Body, Title } from "native-base";
7 | import Lightbox from 'react-native-lightbox';
8 | import { KeyboardAwareView } from 'react-native-keyboard-aware-view'
9 | import TextToolbar from "./TextToolbar";
10 | import Sketch from "./Sketch";
11 |
12 | import {
13 | generateId,
14 | getCurrentBlockInRow,
15 | removeSelectedText,
16 | insertAt,
17 | attachStylesToSelectedText,
18 | mergeNewStyles,
19 | contentState,
20 | getSelectedBlocks,
21 | splitRow
22 | } from "./Helpers";
23 |
24 | import { convertToMarkdown, convertToRaw, convertFromRaw, } from "./Convertors";
25 |
26 | import getEmitter from "./EventEmitter";
27 | import EVENTS from "./Events";
28 |
29 | import styles from "./Styles";
30 | import StyledText from "./StyledText";
31 | import StyledTextInput from "./StyledTextInput";
32 | import CheckBox from "./CheckBox";
33 |
34 | import { ROW_TYPES, STYLE_TYPES, COLORS } from "./Constants";
35 |
36 |
37 | const eventEmitter = getEmitter()
38 |
39 | const listeners = {}
40 |
41 | const isListRow = type => type === ROW_TYPES.BULLETS || type === ROW_TYPES.NUMBERS || type === ROW_TYPES.TODOS
42 |
43 | const history = []
44 |
45 | class Editor extends React.Component {
46 |
47 | textInputRefs = []
48 | sketch = null
49 | refs = []
50 |
51 | lastBlock = {}
52 |
53 | state = {
54 | isReady: false,
55 | isFullscreen: false,
56 | isSketchVisible: false,
57 | rows: [],
58 | extraData: null,
59 | activeRowIndex: 0,
60 | selection: { start: 0, end: 0 },
61 | activeStyles: [],
62 | }
63 |
64 | componentDidMount() {
65 | listeners.hideKeyboard = eventEmitter.addListener(EVENTS.HIDE_KEYBOARD, this.hideKeyboard)
66 | listeners.toggleFullscreen = eventEmitter.addListener(EVENTS.TOGGLE_FULL_SCREEN, this.toggleFullscreen)
67 | listeners.showInsertRow = eventEmitter.addListener(EVENTS.SHOW_INSERT_BLOCK, this.showInsertRow)
68 | listeners.showUploadFile = eventEmitter.addListener(EVENTS.SHOW_UPLOAD_FILE, this.showUploadFile)
69 | listeners.toggleStyle = eventEmitter.addListener(EVENTS.TOGGLE_STYLE, this.toggleStyle)
70 | listeners.clearStyles = eventEmitter.addListener(EVENTS.CLEAR_STYLES, this.clearStyles)
71 | listeners.changeColorStyles = eventEmitter.addListener(EVENTS.CHANGE_COLOR_STYLE, this.changeColorStyles)
72 | listeners.alignRow = eventEmitter.addListener(EVENTS.ALIGN_ROW, this.alignRow)
73 | listeners.deleteActiveRow = eventEmitter.addListener(EVENTS.DELETE_BLOCK, this.deleteActiveRow)
74 | listeners.changeRowIndex = eventEmitter.addListener(EVENTS.CHANGE_BLOCK_INDEX, this.changeRowIndex)
75 | listeners.duplicateRow = eventEmitter.addListener(EVENTS.DUPLICATE_ROW, this.duplicateRow)
76 | listeners.showChangeRowType = eventEmitter.addListener(EVENTS.CHANGE_BLOCK_TYPE, this.showChangeRowType)
77 | listeners.changeRowIndent = eventEmitter.addListener(EVENTS.CHANGE_BLOCK_INDENT, this.changeRowIndent)
78 | listeners.reload = eventEmitter.addListener(EVENTS.RELOAD, this.reload)
79 | listeners.refresh = eventEmitter.addListener(EVENTS.REFRESH, this.refresh)
80 | // listeners.browseHistory = eventEmitter.addListener(EVENTS.BROWSE_HISTORY, this.browseHistory)
81 |
82 |
83 | listeners.logState = eventEmitter.addListener(EVENTS.LOG_STATE, () => {
84 | console.tron.display({
85 | name: 'STATE',
86 | value: { props: this.state },
87 | })
88 | })
89 |
90 | listeners.duplicateRow = eventEmitter.addListener(EVENTS.CONVERT_TO_RAW, () => {
91 | const data = convertToRaw({ rows: this.state.rows })
92 | console.tron.display({
93 | name: 'convertToRaw',
94 | value: { data },
95 | })
96 | })
97 |
98 |
99 | this.initialize()
100 | }
101 |
102 |
103 | // TODO: done
104 | componentWillUnmount() {
105 | if(listeners) {
106 | for (const key in listeners) {
107 | if (listeners.hasOwnProperty(key)) {
108 | const listener = listeners[key];
109 | listener.remove()
110 | }
111 | }
112 | }
113 | }
114 |
115 |
116 | // TODO: done
117 | initialize = () => {
118 | this.fillContentState()
119 | }
120 |
121 | // TODO: done
122 | fillContentState = () => {
123 | const { data = { blocks: [], entityMap: {} } } = this.props
124 | const rows = convertFromRaw({ contentState: data })
125 | this.setState({ rows, isReady: true })
126 | }
127 |
128 | // TODO: reload
129 | reload = () => {
130 | this.fillContentState()
131 | }
132 |
133 | // TODO: done
134 | refresh = () => {
135 | const { activeRowIndex } = this.state
136 | this.setState({ isReady: false }, () => {
137 | setTimeout(() => {
138 | this.setState({ isReady: true }, () => {
139 | setTimeout(() => {
140 | this.focusRow({ index: activeRowIndex })
141 | }, 100);
142 | })
143 | }, 100);
144 | })
145 | }
146 |
147 | // TODO: done
148 | clear = () => {
149 | this.setState({ rows: [] })
150 | }
151 |
152 | // TODO: Done
153 | hideKeyboard () {
154 | Keyboard.dismiss()
155 | }
156 |
157 | // TODO: Done
158 | toggleFullscreen = () => {
159 | const { isFullscreen } = this.state
160 | this.setState({ isFullscreen: !isFullscreen })
161 | }
162 |
163 | // TODO: Done
164 | toggleStyle = ({ style }) => {
165 | const { activeStyles = [], selection, activeRowIndex, rows } = this.state
166 |
167 | const activeRow = Object.assign({}, rows[activeRowIndex])
168 | style = style.toLowerCase()
169 | const isStyleActive = activeStyles.includes(style)
170 | const oldStyles = [...activeStyles]
171 |
172 | let newActiveStyles = []
173 |
174 | if (isStyleActive) {
175 | newActiveStyles = activeStyles.filter(i => i.toLowerCase() !== style)
176 | } else {
177 | newActiveStyles = [...activeStyles, style]
178 | }
179 |
180 | let newState = { activeStyles: newActiveStyles }
181 |
182 | let throwOnChange = false
183 |
184 | if(activeRowIndex !== null && selection.start < selection.end && selection.id === activeRow.id) {
185 | const data = attachStylesToSelectedText({ selection, row: activeRow, newStyles: newActiveStyles, oldStyles })
186 | activeRow.blocks = data.blocks
187 | const newRows = [...rows]
188 | newRows[activeRowIndex] = activeRow
189 |
190 | newState = {...newState, rows: newRows }
191 | throwOnChange = true
192 | } else {
193 | // const newRows = [...rows]
194 | // newRows[activeRowIndex] = activeRow
195 | // newState = {...newState, rows: newRows }
196 | // throwOnChange = true
197 | }
198 |
199 |
200 | this.setState({...newState, extraData: Date.now()}, () => {
201 |
202 | // this.textInputRefs[activeRowIndex].refresh({ focus: true })
203 |
204 | this.emitActiveStyles()
205 | if(throwOnChange) {
206 | this.emitOnChange()
207 |
208 | // this.textInputRefs[activeRowIndex].setSelection({ start: selection.start, end: selection.end })
209 | }
210 | })
211 | }
212 |
213 | // TODO: Done
214 | changeColorStyles = ({ color, type }) => {
215 | const { activeStyles = [], selection, activeRowIndex, rows } = this.state
216 |
217 | const activeRow = Object.assign({}, rows[activeRowIndex])
218 | style = `${type}-${color}`
219 | const isStyleActive = activeStyles.includes(style)
220 | const oldStyles = [...activeStyles]
221 |
222 | let newActiveStyles = []
223 |
224 | if (isStyleActive) {
225 | newActiveStyles = activeStyles.filter(i => i.toLowerCase() !== style)
226 | } else {
227 | newActiveStyles = activeStyles.filter(i => !i.toLowerCase().includes(type))
228 | newActiveStyles = [...newActiveStyles, style]
229 | }
230 |
231 | let newState = { activeStyles: newActiveStyles }
232 |
233 | let throwOnChange = true
234 |
235 | if(activeRowIndex !== null && selection.start < selection.end && selection.id === activeRow.id) {
236 | const data = attachStylesToSelectedText({ selection, row: activeRow, newStyles: newActiveStyles, oldStyles })
237 | activeRow.blocks = data.blocks
238 | const newRows = rows.concat([])
239 | newRows[activeRowIndex] = activeRow
240 |
241 | newState = {...newState, rows: newRows, extraData: Date.now() }
242 | throwOnChange = true
243 | }
244 |
245 | this.setState(newState, () => {
246 | this.emitActiveStyles()
247 | if(throwOnChange) {
248 | this.emitOnChange()
249 | }
250 | })
251 | }
252 |
253 | // TODO: review
254 | clearStyles = () => {
255 | const { activeStyles = [], selection, activeRowIndex, rows } = this.state
256 |
257 | const activeRow = Object.assign({}, rows[activeRowIndex])
258 |
259 | let newState = { activeStyles: [] }
260 | const fills = COLORS.map(color => `fill-${color}`)
261 | const colors = COLORS.map(color => `color-${color}`)
262 | let oldStyles = [...Object.values(STYLE_TYPES), ...fills, ...colors]
263 |
264 | console.tron.display({
265 | name: 'oldStyles',
266 | value: { props: oldStyles },
267 | })
268 |
269 | let throwOnChange = false
270 |
271 | if(activeRowIndex !== null && selection.start < selection.end && selection.id === activeRow.id) {
272 | const data = attachStylesToSelectedText({ selection, row: activeRow, newStyles: [], oldStyles })
273 | activeRow.blocks = data.blocks
274 | const newRows = rows.concat([])
275 | newRows[activeRowIndex] = activeRow
276 | newState = {...newState, rows: newRows, extraData: Date.now() }
277 | throwOnChange = true
278 | }
279 |
280 | this.setState(newState, () => {
281 | this.emitActiveStyles()
282 | if(throwOnChange) {
283 | this.emitOnChange()
284 | }
285 | })
286 | }
287 |
288 | // TODO: review
289 | alignRow = ({ type }) => {
290 | const { activeRowIndex, rows } = this.state
291 |
292 | const activeRow = Object.assign({}, rows[activeRowIndex])
293 |
294 | const newRows = [...rows]
295 | newRows[activeRowIndex].align = type
296 |
297 | const newState = {rows: newRows, extraData: Date.now() }
298 |
299 | this.setState(newState, () => {
300 | this.emitActiveStyles()
301 | this.emitOnChange()
302 | })
303 | }
304 |
305 | // TODO: Done
306 | deleteActiveRow = () => {
307 | const { activeRowIndex, rows } = this.state
308 | if(activeRowIndex !== null && rows.length > 0) {
309 | this.removeRow({ index: activeRowIndex, focusPrev: true })
310 | }
311 | }
312 |
313 | // TODO: done
314 | showChangeRowType = () => {
315 | const { activeRowIndex, rows =[] } = this.state
316 |
317 | if(activeRowIndex === null) {
318 | return
319 | }
320 |
321 | const activeRow = Object.assign({}, rows[activeRowIndex])
322 |
323 | var BUTTONS = {
324 | [ROW_TYPES.TEXT]:"Paragraph",
325 | [ROW_TYPES.HEADING1]:"Heading 1",
326 | [ROW_TYPES.HEADING2]:"Heading 2",
327 | [ROW_TYPES.HEADING3]:"Heading 3",
328 | [ROW_TYPES.BLOCKQUOTE]:"Blockquote",
329 | [ROW_TYPES.BULLETS]:"Bulleted List",
330 | [ROW_TYPES.NUMBERS]:"Numbered List",
331 | [ROW_TYPES.TODOS]:"TODO List",
332 | cancel: 'Cancel'
333 | };
334 | var CANCEL_INDEX = Object.values(BUTTONS).length-1;
335 | var DESTRUCTIVE_INDEX = Object.keys(BUTTONS).indexOf(activeRow.type);
336 |
337 | ActionSheet.show({
338 | options: Object.values(BUTTONS),
339 | cancelButtonIndex: CANCEL_INDEX,
340 | destructiveButtonIndex: DESTRUCTIVE_INDEX,
341 | title: "Change Block Type"
342 | }, i => {
343 | const keys = Object.keys(BUTTONS)
344 | if (keys[i] === 'text') {
345 | this.changeRowType({ index: activeRowIndex, type: ROW_TYPES.TEXT })
346 | }
347 | if (keys[i] === ROW_TYPES.HEADING1) {
348 | this.changeRowType({ index: activeRowIndex, type: ROW_TYPES.HEADING1 })
349 | }
350 | if (keys[i] === ROW_TYPES.HEADING2) {
351 | this.changeRowType({ index: activeRowIndex, type: ROW_TYPES.HEADING2 })
352 | }
353 | if (keys[i] === ROW_TYPES.HEADING3) {
354 | this.changeRowType({ index: activeRowIndex, type: ROW_TYPES.HEADING3 })
355 | }
356 | if (keys[i] === 'blockquote') {
357 | this.changeRowType({ index: activeRowIndex, type: ROW_TYPES.BLOCKQUOTE })
358 | }
359 | if (keys[i] === 'bullets') {
360 | this.changeRowType({ index: activeRowIndex, type: ROW_TYPES.BULLETS })
361 | }
362 | if (keys[i] === ROW_TYPES.NUMBERS) {
363 | this.changeRowType({ index: activeRowIndex, type: ROW_TYPES.NUMBERS })
364 | }
365 | if (keys[i] === ROW_TYPES.TODOS) {
366 | this.changeRowType({ index: activeRowIndex, type: ROW_TYPES.TODOS })
367 | }
368 | })
369 | }
370 |
371 | // TODO: done
372 | showInsertRow = () => {
373 | var BUTTONS = {
374 | [ROW_TYPES.TEXT]:"Paragraph",
375 | [ROW_TYPES.HEADING1]:"Heading 1",
376 | [ROW_TYPES.HEADING2]:"Heading 2",
377 | [ROW_TYPES.HEADING3]:"Heading 3",
378 | [ROW_TYPES.HR]:"Line Break",
379 | [ROW_TYPES.BLOCKQUOTE]: 'Blockquote',
380 | [ROW_TYPES.BULLETS]:"Bulleted List",
381 | [ROW_TYPES.NUMBERS]:"Numbered List",
382 | [ROW_TYPES.TODOS]:"TODO List",
383 | ["Sketch"]:"Sketch",
384 | cancel: 'Cancel'
385 | };
386 | var CANCEL_INDEX = Object.values(BUTTONS).length-1;
387 | var DESTRUCTIVE_INDEX = -1;
388 |
389 | ActionSheet.show({
390 | options: Object.values(BUTTONS),
391 | cancelButtonIndex: CANCEL_INDEX,
392 | destructiveButtonIndex: DESTRUCTIVE_INDEX,
393 | title: "Insert Block"
394 | }, i => {
395 | const keys = Object.keys(BUTTONS)
396 | if (keys[i] === ROW_TYPES.TEXT) {
397 | this.insertRow({ focus: true })
398 | }
399 | if (keys[i] === ROW_TYPES.HEADING1) {
400 | this.insertHeading({ heading: ROW_TYPES.HEADING1 })
401 | }
402 | if (keys[i] === ROW_TYPES.HEADING2) {
403 | this.insertHeading({ heading: ROW_TYPES.HEADING2 })
404 | }
405 | if (keys[i] === ROW_TYPES.HEADING3) {
406 | this.insertHeading({ heading: ROW_TYPES.HEADING3 })
407 | }
408 | if (keys[i] === ROW_TYPES.HR) {
409 | this.insertRow({ type: ROW_TYPES.HR, focus: false, insertAfterActive: true })
410 | }
411 | if (keys[i] === ROW_TYPES.BLOCKQUOTE) {
412 | this.insertRow({ type: ROW_TYPES.BLOCKQUOTE, focus: true, insertAfterActive: true })
413 | }
414 | if (keys[i] === ROW_TYPES.BULLETS) {
415 | this.insertList({ list: 'bullets' })
416 | }
417 | if (keys[i] === ROW_TYPES.NUMBERS) {
418 | this.insertList({ list: 'numbers' })
419 | }
420 | if (keys[i] === ROW_TYPES.TODOS) {
421 | this.insertList({ list: 'todos' })
422 | }
423 | if (keys[i] === "Sketch") {
424 | this.showSketchModal()
425 | }
426 | })
427 | }
428 |
429 | // FIXME: done
430 | showUploadFile = () => {
431 | var BUTTONS = {
432 | ["Take Photo"]:"Take Photo",
433 | ["Browse Photo"]:"Browse Photo",
434 | cancel: 'Cancel'
435 | };
436 | var CANCEL_INDEX = Object.values(BUTTONS).length-1;
437 | var DESTRUCTIVE_INDEX = -1
438 |
439 | ActionSheet.show({
440 | options: Object.values(BUTTONS),
441 | cancelButtonIndex: CANCEL_INDEX,
442 | destructiveButtonIndex: DESTRUCTIVE_INDEX,
443 | title: "Insert Image"
444 | }, i => {
445 | const keys = Object.keys(BUTTONS)
446 | if (keys[i] === "Take Photo") {
447 | this.insertImage({ type: "Take Photo" })
448 | }
449 | if (keys[i] === "Browse Photo") {
450 | this.insertImage({ type: "Browse Photo" })
451 | }
452 | })
453 | }
454 |
455 | // FIXME: Done
456 | insertImage = async ({ type }) => {
457 | const { statusCamera } = await Permissions.askAsync(Permissions.CAMERA);
458 | if(statusCamera === 'granted') {
459 | alert('CAMERA Permission Error')
460 | return
461 | }
462 |
463 | const { statusLibrary } = await Permissions.askAsync(Permissions.CAMERA_ROLL);
464 | if(statusLibrary === 'granted') {
465 | alert('CAMERA_ROLL Permission Error')
466 | return
467 | }
468 |
469 | const pickerTypes = {
470 | ['Take Photo'] : 'launchCameraAsync',
471 | ['Browse Photo'] : 'launchImageLibraryAsync',
472 | }
473 |
474 | let result = await ImagePicker[pickerTypes[type]]({
475 | allowsEditing: true,
476 | aspect: [4, 3],
477 | });
478 |
479 | console.log(result);
480 |
481 | if (!result.cancelled) {
482 | let newRowData = { id: generateId(), type: ROW_TYPES.IMAGE, image: result.uri }
483 | this.insertRow({ newRowData, insertAfterActive: true, focus: true })
484 | }
485 | }
486 |
487 | // TODO: Done
488 | duplicateRow = () => {
489 | const { activeRowIndex, rows = [] } = this.state
490 |
491 | if(activeRowIndex === null) { return }
492 |
493 | const activeRow = Object.assign({}, rows[activeRowIndex])
494 | const newRowData = {...activeRow, id: generateId()}
495 |
496 | this.insertRow({ newRowData, insertAfterActive: true, focus: true })
497 | }
498 |
499 | // TODO: Done
500 | emitActiveStyles = ({ activeStyles, updateState = false } = {}, callback = () => {}) => {
501 | let newActiveStyles = activeStyles || this.state.activeStyles
502 | newActiveStyles = _.uniq(newActiveStyles)
503 | getEmitter().emit(EVENTS.ACTIVE_STYLE_CHANGED, { activeStyles: newActiveStyles })
504 | if(updateState) {
505 | this.setState({ activeStyles })
506 | }
507 | callback()
508 | }
509 |
510 | // TODO: Done
511 | changeRowIndex = ({ direction }) => {
512 | const { activeRowIndex, rows = [] } = this.state
513 |
514 | if(activeRowIndex === null) { return }
515 |
516 | const newRows = [...rows]
517 | const currentBlock = Object.assign({}, rows[activeRowIndex])
518 | let shouldSwapIndex = false
519 | let newIndex
520 |
521 | if(direction === 'up') {
522 | newIndex = activeRowIndex-1
523 | const prevRow = Object.assign({}, rows[newIndex])
524 | if(prevRow && newIndex > -1) {
525 | newRows[newIndex] = currentBlock
526 | newRows[activeRowIndex] = prevRow
527 | shouldSwapIndex = true
528 | }
529 | }
530 |
531 | if(direction === 'down') {
532 | newIndex = activeRowIndex+1
533 | const nextRow = Object.assign({}, rows[newIndex])
534 | if(nextRow) {
535 | newRows[newIndex] = currentBlock
536 | newRows[activeRowIndex] = nextRow
537 | shouldSwapIndex = true
538 | }
539 | }
540 |
541 | if(shouldSwapIndex) {
542 | this.focusRow({ index: newIndex })
543 | this.setState({ rows: newRows, activeRowIndex: newIndex, extraData: Date.now() }, () => {
544 | this.emitOnChange()
545 | setTimeout(() => {
546 | this.focusRow({ index: newIndex })
547 | }, 50);
548 | })
549 | }
550 | }
551 |
552 | // TODO: done
553 | changeRowIndent = ({ direction }) => {
554 | const { activeRowIndex, rows = [] } = this.state
555 | if(activeRowIndex === null) {
556 | return
557 | }
558 |
559 | const indent = " "
560 |
561 | const newRows = [...rows]
562 | const currentRow = Object.assign({}, rows[activeRowIndex])
563 | const text = currentRow.value || ''
564 |
565 | if(direction === 'increase') {
566 | currentRow.value = `${indent}${text}`
567 | currentRow.blocks.unshift({ text: indent })
568 | newRows[activeRowIndex] = currentRow
569 | this.setState({ rows: newRows, extraData: Date.now() })
570 | }
571 |
572 | if(direction === 'decrease') {
573 | if(text.startsWith(indent)) {
574 | currentRow.value = text.slice(indent.length-1, text.length);
575 | currentRow.blocks.shift()
576 | newRows[activeRowIndex] = currentRow
577 | this.setState({ rows: newRows, extraData: Date.now() })
578 | }
579 | }
580 | }
581 |
582 | // TODO: done
583 | splitRow = ({ row, index }) => {
584 | const { rows = [], selection } = this.state
585 | const newSplittedRows = splitRow({ row, selection })
586 |
587 | if(newSplittedRows) {
588 | let newActiveRowIndex = index+1
589 |
590 | const newRows = [...rows]
591 | newRows[index] = newSplittedRows[0]
592 | let newSelection = {...selection }
593 | if(newSplittedRows[1]) {
594 | newRows.splice(newActiveRowIndex, 0,newSplittedRows[1])
595 | newSelection = {...selection, id: newSplittedRows[1].id }
596 | }
597 | this.setState({ rows: newRows, activeRowIndex: newActiveRowIndex, selection: newSelection, extraData: Date.now() }, () => {
598 | this.focusRow({ index: newActiveRowIndex })
599 | })
600 | } else {
601 | console.log("Split rows without selection")
602 | }
603 |
604 | }
605 |
606 | // TODO: done
607 | showSketchModal = () => {
608 | this.setState({ isSketchVisible: true })
609 | }
610 |
611 | /**
612 | * TODO: re-write
613 | * beginning press enter: insert new block before (keep type if is list)
614 | * middle press enter: split row at cursor
615 | * end press enter: insert new block after (keep type if is list)
616 | */
617 | onSubmitEditing = ({ row, index }) => () => {
618 | const { rows = [], selection } = this.state
619 |
620 | const nextRow = Object.assign({}, rows[index+1])
621 | const currentRow = Object.assign({}, rows[index])
622 |
623 | // If there is selected text, remove first
624 | // if (selection.start < selection.end) {
625 | // const newRowBlocks = removeSelectedText({ selection, row }) || []
626 | // currentRow.blocks = newRowBlocks
627 | // row.blocks = newRowBlocks
628 | // }
629 |
630 | if (selection.start === 0 && selection.end === 0 && currentRow.value) {
631 | this.insertRow({ focus: false, currentRow, insertBeforeActive: true, focusIndex: index+1 })
632 | } else if(currentRow.value
633 | && selection.start === selection.end
634 | && selection.start > 0
635 | && selection.end < currentRow.value.length
636 | ) { // Split this line
637 | this.splitRow({ row, index })
638 | } else if(!currentRow.value && isListRow(currentRow.type)) { // Switch this line to text row
639 | this.changeRowType({ index, type: ROW_TYPES.TEXT })
640 | } else if (nextRow.id && isListRow(currentRow.type)) {
641 | this.insertRow({ focus: true, currentRow, insertAfterActive: true })
642 | } else if (nextRow.id){
643 | this.insertRow({ focus: true, insertAfterActive: true })
644 | } else {
645 | this.insertRow({ focus: true, insertAfterActive: true, currentRow })
646 | }
647 | }
648 |
649 | // TODO: done
650 | onChangeText = ({ row, index }) => (nv = '') => {
651 | const { rows = [] } = this.state
652 | let newRows = [...rows]
653 | newRows[index].value = nv
654 | row.value = nv
655 | this.setState({ rows: newRows })
656 | }
657 |
658 | // FIXME: check
659 | handleBackspace = ({ row: item, index }) => {
660 | const { selection, rows = [] } = this.state
661 | let row = Object.assign({}, item)
662 | const prevRow = Object.assign({}, rows[index-1])
663 | const { value = '', blocks = [] } = row
664 |
665 | if (selection.start < selection.end) {
666 | const newBlocks = removeSelectedText({ selection, row })
667 | const newRows = [].concat(rows)
668 | newRows[index].blocks = newBlocks
669 | this.setState({ rows: newRows })
670 | } else if (selection.start === selection.end && selection.start === 0 && prevRow && prevRow.type === ROW_TYPES.HR) {
671 | this.removeRow({ index: index-1 }, () => {
672 | this.focusRow({ index: index-1 })
673 | })
674 | } else if(!value.length) {
675 | this.removeRow({ index, focusPrev: true })
676 | } else if(value.length === 1) {
677 | this.removeRow({ index, focusPrev: true })
678 | } else if(selection.start === 0) {
679 |
680 | } else {
681 | const newBlocks = removeSelectedText({ selection, row })
682 | let newRows = [].concat(rows)
683 | newRows[index].blocks = newBlocks
684 | this.setState({ rows: newRows })
685 |
686 | console.tron.display({
687 | name: 'Backspace',
688 | value: { blocks, newRows },
689 | })
690 | }
691 | }
692 |
693 | getRowValue = ({ index }) => {
694 | return this.textInputRefs[index].getValue()
695 | }
696 |
697 | // FIXME: check
698 | handleKeyPress = ({ row: item, index }) => ({ nativeEvent: { key: keyValue }, ...rest }) => {
699 | const { rows = [], selection, activeStyles } = this.state
700 | let row = item // Object.assign({}, item)
701 | const currentRow = item // Object.assign({}, rows[index])
702 | let blocks = item.blocks // [...currentRow.blocks]
703 |
704 | row.value = this.getRowValue({ index })
705 | currentRow.value = this.getRowValue({ index })
706 |
707 | // console.log("handleKeyPress fired::", keyValue)
708 |
709 | if (keyValue === 'Backspace') {
710 | this.handleBackspace({ row, index })
711 | } else if (keyValue === 'Enter') {
712 | this.setState({ selection: { start: 1, end: 1 } })
713 | } else if (keyValue === 'Tab') {
714 | // TODO: indent
715 | } else {
716 |
717 | // If there is selected text, remove first
718 | if (selection.start < selection.end) {
719 | const newRowBlocks = removeSelectedText({ selection, row }) || []
720 | row.blocks = newRowBlocks
721 | blocks = [].concat(newRowBlocks)
722 | }
723 |
724 | const currentBlock = getCurrentBlockInRow({ selection, row }) || {}
725 |
726 | const {
727 | block: {
728 | text: blockText = '',
729 | styles: blockStyles = []
730 | } = {},
731 | pointerAt = 0,
732 | blockIndex = 0
733 | } = currentBlock
734 |
735 | // console.tron.display({
736 | // name: 'currentBlock',
737 | // value: { props: currentBlock },
738 | // })
739 |
740 | let newBlocks = blocks // [].concat(blocks)
741 |
742 | const isStylesChanged = !_.isEqual(activeStyles, blockStyles)
743 |
744 | if (isStylesChanged) {
745 |
746 | let p1 = blocks.slice(0, blockIndex)
747 | let p2 = blocks.slice(blockIndex+1)
748 |
749 | // console.tron.display({
750 | // name: 'p1, p2',
751 | // value: { p1, p2 },
752 | // })
753 |
754 | const newCharBlocks = []
755 |
756 | const blockBeforeText = blockText.substring(0, pointerAt)
757 | const blockAfterText = blockText.substring(pointerAt, blockText.length)
758 |
759 | const newBlock = { text: keyValue, styles: [].concat(activeStyles) }
760 |
761 | if(blockBeforeText) {
762 | newCharBlocks.push({ text: blockBeforeText, styles: [...blockStyles] })
763 | }
764 |
765 | newCharBlocks.push(newBlock)
766 |
767 | if(blockAfterText) {
768 | newCharBlocks.push({ text: blockAfterText, styles: [...blockStyles] })
769 | }
770 |
771 | newBlocks = [...p1, ...newCharBlocks, ...p2]
772 |
773 | // console.tron.display({
774 | // name: 'newCharBlocks',
775 | // value: { newCharBlocks, p1, p2, },
776 | // })
777 |
778 | } else {
779 | const newBlockText = insertAt(blockText, keyValue, pointerAt)
780 | if (!newBlocks[blockIndex]) {
781 | newBlocks[blockIndex] = {}
782 | }
783 | newBlocks[blockIndex].text = newBlockText
784 | }
785 |
786 | // let newRows = [].concat(rows)
787 | let newRows = [...rows]
788 | newRows[index].blocks = newBlocks
789 | // newRows[index].value = newBlocks.map(i => item.text).join()
790 | rows[index].blocks = newBlocks
791 |
792 | let newState = {rows: rows, extraData: Date.now() }
793 | this.setState(newState, () => {
794 | if(isStylesChanged) {
795 | this.textInputRefs[index].stylesChanged({ pointerAt: selection.start, row: newRows[index], keyValue })
796 | }
797 | })
798 | }
799 |
800 | this.emitOnChange()
801 | }
802 |
803 | // FIXME: re-write
804 | onSelectionChange = ({ row, index, value }) => (event) => {
805 | const { selection } = event.nativeEvent
806 | const { rows = [], activeRowIndex, selection: oldSelection } = this.state
807 | const activeRow = Object.assign({}, rows[activeRowIndex])
808 |
809 | if(row.id !== activeRow.id) {
810 | return
811 | }
812 |
813 | row.value = value || this.getRowValue({ index })
814 |
815 | console.log(row.value)
816 |
817 | if(oldSelection.id === row.id
818 | && oldSelection.start === selection.start
819 | && oldSelection.end === selection.end
820 | ) {
821 | return
822 | }
823 |
824 | let newSelection = { ...selection, id: row.id }
825 |
826 | this.setState({ selection: newSelection }, () => {
827 |
828 | // console.log("onSelectionChange::", selection.start, selection.end, activeRowIndex)
829 |
830 | setTimeout(() => {
831 | if (newSelection.start === newSelection.end && activeRowIndex !== null) {
832 | const { block: { styles: blockStyles = [] } = {}, position } = getCurrentBlockInRow({ selection: newSelection, row })
833 | let activeStyles = blockStyles
834 |
835 | if(position === 'end') {
836 | const lastBlockIndex = row.blocks.length-1
837 | lastBlock = row.blocks[lastBlockIndex] || { styles: [] }
838 | console.tron.display({
839 | name: 'lastBlock',
840 | value: { props: lastBlock, row },
841 | })
842 | activeStyles = blockStyles.concat(lastBlock.styles || [])
843 | activeStyles = _.uniq(activeStyles)
844 | }
845 |
846 | this.emitActiveStyles({ activeStyles, updateState: true })
847 |
848 | } else if (newSelection.start < newSelection.end) {
849 | const { startBlock, endBlock } = getSelectedBlocks({ selection: newSelection, row })
850 | const { blocks } = row
851 |
852 | const blockStyles = []
853 |
854 | for (let i = startBlock.blockIndex; i <= endBlock.blockIndex; i++) {
855 | const block = blocks[i] || {};
856 | const { styles = [], text = '' } = block
857 | if(text && text !== ' ') {
858 | blockStyles.push(styles)
859 | }
860 | }
861 |
862 | const commonStyles = _.intersection(...blockStyles);
863 | this.emitActiveStyles({ activeStyles: commonStyles, updateState: true })
864 | }
865 | });
866 |
867 | })
868 |
869 | }
870 |
871 | // FIXME: half
872 | onFocus = ({ row, index }) => (e) => {
873 | const { rows = [], activeRowIndex } = this.state
874 | const activeRow = Object.assign({}, rows[index])
875 |
876 | if (activeRowIndex !== index) {
877 | this.setState({ activeRowIndex: index }, () => {
878 | // if(activeRow.value) {
879 | // const newSelection = { start: activeRow.value.length, end: activeRow.value.length, id: activeRow.id }
880 | // const event = { nativeEvent: { selection: newSelection } }
881 | // this.onSelectionChange({ row: activeRow, index })(event)
882 | // } else {
883 | // const newSelection = { start: 0, end: 0, id: activeRow.id }
884 | // const event = { nativeEvent: { selection: newSelection } }
885 | // this.onSelectionChange({ row: activeRow, index })(event)
886 | // }
887 | this.checkActiveRowTypeChanged()
888 | })
889 | } else {
890 | this.checkActiveRowTypeChanged()
891 | }
892 |
893 | const { onFocus } = this.props
894 | if(onFocus) {
895 | onFocus({ index, contentState: this.getContentState() })
896 | }
897 | }
898 |
899 | // TODO: half
900 | getContentState = () => {
901 | return convertToRaw({ rows: this.state.rows })
902 | }
903 |
904 | // TODO: done
905 | checkActiveRowTypeChanged = () => {
906 | const { activeRowIndex, rows = [] } = this.state
907 | const activeRow = rows[activeRowIndex] || {}
908 | const { type = '' } = activeRow
909 | getEmitter().emit(EVENTS.ROW_TYPE_CHANGED, { type })
910 | }
911 |
912 | // TODO: needs check
913 | focusRow ({ index, timeout = 0, clearStyles = true }, callback = () => {}) {
914 | const { rows = [], activeStyles = [], activeRowIndex } = this.state
915 | const input = this.textInputRefs[index]
916 | if(input) {
917 | const row = Object.assign({}, rows[index])
918 | let newStyles = [...activeStyles]
919 | if(!row.value || clearStyles) {
920 | newStyles = []
921 | }
922 |
923 | let newState = {} // { activeStyles: newStyles }
924 |
925 | if(activeRowIndex !== index) {
926 | newState.activeRowIndex = index
927 | }
928 |
929 | this.setState(newState, () => {
930 | setTimeout(() => {
931 | input.focus()
932 | callback()
933 | }, timeout);
934 | })
935 | }
936 | }
937 |
938 | // FIXME: needs attention
939 | insertRow ({
940 | type = ROW_TYPES.TEXT,
941 | focus = false,
942 | focusIndex = null,
943 | newRowData,
944 | currentRow,
945 | insertAtActive = false,
946 | insertAfterActive = false,
947 | insertBeforeActive = false,
948 | insertAtLast = false,
949 | updateActiveIndex = false,
950 | } = {}, callback = () => {}) {
951 | const { rows = [], activeRowIndex } = this.state
952 |
953 | let newRows = [...rows]
954 |
955 | let newRow = newRowData || { id: generateId(), type, blocks: [] }
956 |
957 |
958 | if(currentRow) {
959 | if(currentRow.type === ROW_TYPES.BULLETS) {
960 | newRow.type = ROW_TYPES.BULLETS
961 | }
962 | if(currentRow.type === ROW_TYPES.NUMBERS) {
963 | newRow.type = ROW_TYPES.NUMBERS
964 | }
965 | if(currentRow.type === ROW_TYPES.TODOS) {
966 | newRow.type = ROW_TYPES.TODOS
967 | }
968 | }
969 |
970 | if(activeRowIndex !== null) {
971 | const activeRow = Object.assign({}, rows[activeRowIndex])
972 | newRow.align = activeRow.align
973 | }
974 |
975 | newRow = Object.assign({}, newRow)
976 |
977 | let newRowIndex
978 |
979 | if(activeRowIndex !== null && insertAtActive) {
980 | newRows.splice(activeRowIndex, 0, newRow);
981 | newRowIndex = activeRowIndex
982 | } else if(activeRowIndex !== null && insertAfterActive) {
983 | newRows.splice(activeRowIndex+1, 0, newRow);
984 | newRowIndex = activeRowIndex+1
985 | } else if(activeRowIndex !== null && insertBeforeActive) {
986 | newRows.splice(activeRowIndex, 0, newRow);
987 | newRowIndex = activeRowIndex
988 | } else if(insertAtLast) {
989 | newRows.push(newRow)
990 | newRowIndex = rows.length - 1
991 | } else {
992 | newRows.push(newRow)
993 | newRowIndex = rows.length - 1
994 | }
995 |
996 | const newState = {
997 | rows: newRows,
998 | extraData: Date.now(),
999 | activeStyles: [],
1000 | selection: { start: 0, end: 0, id: newRow.id }
1001 | }
1002 |
1003 | if(updateActiveIndex && !focus) {
1004 | newState.activeRowIndex = newRowIndex
1005 | }
1006 |
1007 | if(focusIndex !== null) {
1008 | newState.activeRowIndex = focusIndex
1009 | }
1010 |
1011 | this.setState(newState, () => {
1012 | this.emitActiveStyles()
1013 | this.emitOnChange()
1014 | callback({ newRowIndex, newRow })
1015 | // setTimeout(() => {
1016 | if(focus) {
1017 | this.focusRow({ index: newRowIndex, isNew: true })
1018 | } if(focusIndex !== null) {
1019 | this.focusRow({ index: focusIndex })
1020 | }
1021 | // });
1022 | })
1023 | }
1024 |
1025 | // TODO: done
1026 | insertHeading = ({ heading }) => {
1027 | this.insertRow({ type: heading, focus: true, insertAfterActive: true })
1028 | }
1029 |
1030 | // TODO: done
1031 | insertList = ({ list }) => {
1032 | const key = list.toUpperCase()
1033 | this.insertRow({ type: ROW_TYPES[key], focus: true, insertAfterActive: true })
1034 | }
1035 |
1036 | // TODO: done
1037 | removeRow ({ index, focusPrev = false }, callback = () => {}) {
1038 | const { rows = [] } = this.state
1039 | if(rows.length > 0) {
1040 | let newRows = [...rows]
1041 | newRows.splice(index, 1)
1042 | this.setState({ rows: newRows, activeRowIndex: index-1, extraData: Date.now() }, () => {
1043 | this.emitOnChange()
1044 | setTimeout(() => {
1045 | if(focusPrev) {
1046 | this.focusRow({ index: index-1 })
1047 | }
1048 | callback()
1049 | });
1050 | })
1051 | }
1052 | }
1053 |
1054 | // TODO: done
1055 | emitOnChange = () => {
1056 | const { onChange } = this.props
1057 | if(onChange) {
1058 | onChange(this.getContentState())
1059 | }
1060 | }
1061 |
1062 | // TODO: done
1063 | changeRowType ({ index, type }) {
1064 | const { rows = [] } = this.state
1065 | const row = Object.assign({}, rows[index])
1066 | if(row) {
1067 | row.type = type
1068 | // If its heading row, remove styles
1069 | if(type.includes('heading')) {
1070 | row.blocks = row.blocks.map(item => ({ text: item.text }))
1071 | }
1072 | const newRows = [...rows]
1073 | newRows[index] = row
1074 | this.setState({ rows: newRows, extraData: Date.now() }, () => {
1075 | this.emitOnChange()
1076 | this.checkActiveRowTypeChanged()
1077 | this.focusRow({ index, timeout: 100 })
1078 | })
1079 | }
1080 | }
1081 |
1082 | // FIXME:
1083 | handleImage = ({ row, index }) => () => {
1084 | const { activeRowIndex, rows = [] } = this.state
1085 | ActionSheet.show({
1086 | options: ['Delete', 'Cancel'],
1087 | cancelButtonIndex: 1,
1088 | destructiveButtonIndex: 0,
1089 | title: "Delete Image"
1090 | }, i => {
1091 | if (i === 0) {
1092 | // this.changeRowType({ index: activeRowIndex, type: ROW_TYPES.TEXT })
1093 | this.removeRow({ index, focusPrev: true })
1094 | }
1095 | })
1096 | }
1097 |
1098 | // FIXME:
1099 | toggleTodo = ({ row, index }) => () => {
1100 | const { rows = [] } = this.state
1101 | const isCompleted = !row.isCompleted
1102 | const newRows = [...rows]
1103 | newRows[index].isCompleted = isCompleted
1104 | this.setState({ rows: newRows })
1105 | }
1106 |
1107 |
1108 | // FIXME:
1109 | onSketchSave = (image) => {
1110 | if(!image) {
1111 | this.setState({ isSketchVisible: false })
1112 | return
1113 | }
1114 |
1115 | let newRowData = { id: generateId(), type: ROW_TYPES.IMAGE, image }
1116 | this.insertRow({ newRowData, insertAfterActive: true, focus: true }, () => {
1117 | this.setState({ isSketchVisible: false })
1118 | })
1119 | };
1120 |
1121 |
1122 | // FIXME: re-think
1123 | getPlaceholder = ({ row, index }) => {
1124 | const { rows } = this.state
1125 | if(index === 0 && rows.length === 1) {
1126 | return "Write..."
1127 | }
1128 | if(row.type === ROW_TYPES.HEADING1) {
1129 | return 'Heading 1'
1130 | }
1131 | if(row.type === ROW_TYPES.HEADING2) {
1132 | return 'Heading 2'
1133 | }
1134 | if(row.type === ROW_TYPES.HEADING3) {
1135 | return 'Heading 3'
1136 | }
1137 | if(row.type === ROW_TYPES.BLOCKQUOTE) {
1138 | return 'Blockquote'
1139 | }
1140 | if(row.type === ROW_TYPES.BULLETS) {
1141 | return 'List'
1142 | }
1143 | if(row.type === ROW_TYPES.NUMBERS) {
1144 | return 'List'
1145 | }
1146 | return ''
1147 | }
1148 |
1149 | // TODO: half
1150 | getInputStyles = ({ row, index }) => {
1151 | if(row.type === ROW_TYPES.HEADING1) {
1152 | return styles.heading1
1153 | }
1154 | if(row.type === ROW_TYPES.HEADING2) {
1155 | return styles.heading2
1156 | }
1157 | if(row.type === ROW_TYPES.HEADING3) {
1158 | return styles.heading3
1159 | }
1160 | if(row.type === ROW_TYPES.BLOCKQUOTE) {
1161 | return styles.blockquote
1162 | }
1163 | return {}
1164 | }
1165 |
1166 | // TODO: half
1167 | getNumberOrder = ({ index }) => {
1168 | const { rows } = this.state
1169 | let numberOrder = 0
1170 | for (let i = index; i >= 0; i--) {
1171 | const block = rows[i];
1172 | if(block.type !== ROW_TYPES.NUMBERS) {
1173 | break
1174 | }
1175 | numberOrder+=1
1176 | }
1177 | return numberOrder
1178 | }
1179 |
1180 | // TODO: half
1181 | getAlignStyles = ({ row }) => {
1182 | let styles = {}
1183 |
1184 | if(row.align) {
1185 | styles.textAlign =row.align
1186 | }
1187 |
1188 | return styles
1189 | }
1190 |
1191 | renderInput = ({ row, index }) => {
1192 | const placeholder = this.getPlaceholder({ row, index })
1193 | const inputStyles = this.getInputStyles({ row, index })
1194 | const alignStyles = this.getAlignStyles({ row, index })
1195 |
1196 | const isBullet = row.type === ROW_TYPES.BULLETS
1197 | const isNumbers = row.type === ROW_TYPES.NUMBERS
1198 | const isTodo = row.type === ROW_TYPES.TODOS
1199 | const numberOrder = this.getNumberOrder({row, index})
1200 |
1201 | const { blocks = [] } = row
1202 |
1203 | return (
1204 |
1205 | {isBullet && •}
1206 | {isNumbers && {numberOrder}.}
1207 | {isTodo && }
1208 | { this.textInputRefs[index] = input; }}
1211 | placeholder={placeholder}
1212 | onSubmitEditing={this.onSubmitEditing}
1213 | onFocus={this.onFocus}
1214 | onChangeText={this.onChangeText}
1215 | handleKeyPress={this.handleKeyPress}
1216 | onSelectionChange={this.onSelectionChange}
1217 | textInput={styles.textInput}
1218 | inputStyles={inputStyles}
1219 | alignStyles={alignStyles}
1220 | row={row}
1221 | index={index}
1222 | value={row.value}
1223 | />
1224 |
1225 | )
1226 | }
1227 |
1228 | // FIXME: needs attention
1229 | renderItem = ({ item: row, index }) => {
1230 | if(row.type === ROW_TYPES.HR) {
1231 | return this.renderLineBreak({ row, index })
1232 | }
1233 |
1234 | if(row.type === ROW_TYPES.IMAGE) {
1235 | return this.renderImage({ row, index })
1236 | }
1237 |
1238 | return this.renderInput({ row, index })
1239 | }
1240 |
1241 | // FIXME:
1242 | renderImage = ({ row, index }) => {
1243 | return (
1244 |
1245 |
1246 |
1247 |
1248 |
1249 |
1250 |
1251 | )
1252 | }
1253 |
1254 | // FIXME:
1255 | renderLineBreak = ({ row, index }) => {
1256 | return (
1257 |
1258 |
1259 |
1260 | )
1261 | }
1262 |
1263 | // FIXME: half - bug when last row is list
1264 | renderFooter = () => {
1265 | const { rows = []} = this.state
1266 | const lastRowIndex = rows.length-1
1267 | let lastRow = rows[lastRowIndex]
1268 | ? Object.assign({}, rows[lastRowIndex])
1269 | : null
1270 | return (
1271 |
1272 | {
1279 | this.insertRow({ focusIndex: lastRowIndex+1, currentRow: lastRow })
1280 | }}
1281 | autoCapitalize="none"
1282 | returnKeyType="default"
1283 | multiline={false}
1284 | placeholder="Footer"
1285 | >
1286 |
1287 |
1288 | )
1289 | }
1290 |
1291 | // FIXME: half - focus
1292 | renderEmpty = () => {
1293 | return (
1294 |
1295 | {
1302 | this.insertRow({ focusIndex: 0 })
1303 | }}
1304 | autoCapitalize="none"
1305 | returnKeyType="default"
1306 | multiline={false}
1307 | placeholder="Empty"
1308 | >
1309 |
1310 |
1311 | )
1312 | }
1313 |
1314 | // TODO: done
1315 | renderLoading = () => {
1316 | return (
1317 |
1318 |
1319 |
1320 | )
1321 | }
1322 |
1323 | // FIXME: empty
1324 | renderHeader = () => {
1325 | return (
1326 |
1327 | )
1328 | }
1329 |
1330 | // FIXME: half
1331 | renderList() {
1332 | const { rows, extraData, isReady } = this.state
1333 |
1334 | if(!isReady) {
1335 | return this.renderLoading()
1336 | }
1337 |
1338 | return (
1339 | i.id}
1342 | extraData={this.state}
1343 | keyboardShouldPersistTaps={"always"}
1344 | keyboardDismissMode="none"
1345 | contentContainerStyle={styles.contentContainerStyle}
1346 | renderItem={this.renderItem}
1347 | ListFooterComponent={this.renderFooter}
1348 | ListEmptyComponent={this.renderEmpty}
1349 | ListHeaderComponent={this.renderHeader}
1350 | style={styles.flatList}
1351 | />
1352 | );
1353 | }
1354 |
1355 | renderFullScreen() {
1356 | const { isFullscreen } = this.state
1357 | return (
1358 | {}}
1363 | >
1364 |
1365 |
1366 |
1367 | Edit Text
1368 |
1369 |
1370 |
1371 |
1372 |
1373 |
1374 | {this.renderList()}
1375 |
1376 |
1377 |
1378 |
1379 |
1380 |
1381 |
1382 | );
1383 | }
1384 |
1385 | renderSketchModal() {
1386 | const { isSketchVisible } = this.state
1387 | return (
1388 | {this.setState({ isSketchVisible: false })}}
1391 | onSave={this.onSketchSave}
1392 | />
1393 | );
1394 | }
1395 |
1396 | render() {
1397 | const { isFullscreen, isSketchVisible, isReady } = this.state
1398 |
1399 | if(!isReady) {
1400 | return this.renderLoading()
1401 | }
1402 |
1403 | return (
1404 |
1405 | {isFullscreen ? this.renderFullScreen() : this.renderList()}
1406 | {this.renderSketchModal()}
1407 |
1408 | )
1409 | }
1410 | }
1411 |
1412 | export default Editor
1413 |
--------------------------------------------------------------------------------
/editor/src/TextToolbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View, TouchableOpacity, ScrollView, StyleSheet, InteractionManager } from 'react-native';
3 |
4 | import { MaterialIcons } from '@expo/vector-icons';
5 |
6 | import getEmitter from "./EventEmitter";
7 | import EVENTS from "./Events";
8 | import { COLORS } from "./Constants";
9 |
10 | const eventEmitter = getEmitter()
11 |
12 | const HEIGHT = 38
13 |
14 | const Divider = () => (
15 |
16 | )
17 |
18 | const Button = ({ icon, name, isActive = false, isDisabled = false, arrow = false, onPress = () => {} }) => {
19 | const hasIcon = !!icon
20 | const hasName = !!name
21 |
22 | const activeButtonStyle = isActive ? styles.activeButton : {}
23 | const disabledButtonStyle = isDisabled ? styles.disabledButton : {}
24 |
25 | const textStyles = hasIcon && hasName ? styles.iconText : {}
26 |
27 | return (
28 |
29 | {hasIcon && }
30 | {hasName && {name}}
31 | {arrow && }
32 |
33 | )}
34 |
35 | const listeners = {}
36 |
37 | const VIEWS = {
38 | DEFAULT: 'default',
39 | ALIGN: 'align',
40 | FILL: 'fill',
41 | COLOR: 'color',
42 | }
43 |
44 | class Toolbar extends React.Component {
45 |
46 | state = {
47 | activeStyles: [],
48 | activeRowType: '',
49 | activeView: VIEWS.DEFAULT
50 | }
51 |
52 | componentDidMount() {
53 | listeners.activeStylesChanged = getEmitter().addListener(EVENTS.ACTIVE_STYLE_CHANGED, this.activeStylesChanged)
54 | listeners.rowTypeChanged = getEmitter().addListener(EVENTS.ROW_TYPE_CHANGED, this.rowTypeChanged)
55 | }
56 |
57 | componentWillUnmount() {
58 | if(listeners) {
59 | for (const key in listeners) {
60 | if (listeners.hasOwnProperty(key)) {
61 | const listener = listeners[key];
62 | listener.remove()
63 | }
64 | }
65 | }
66 | }
67 |
68 | activeStylesChanged = ({ activeStyles }) => {
69 | this.setState({ activeStyles })
70 | }
71 |
72 | rowTypeChanged = ({ type }) => {
73 | this.setState({ activeRowType: type })
74 | }
75 |
76 | emit = (event, params = {}) => () => {
77 | eventEmitter.emit(event, params)
78 | }
79 |
80 | toggleFormatAlign = () => {
81 | this.setState({ activeView: VIEWS.ALIGN })
82 | }
83 |
84 | toggleFormatFill = () => {
85 | this.setState({ activeView: VIEWS.FILL })
86 | }
87 |
88 | toggleFormatColor = () => {
89 | this.setState({ activeView: VIEWS.COLOR })
90 | }
91 |
92 | setDefaultView = () => {
93 | this.setState({ activeView: VIEWS.DEFAULT })
94 | }
95 |
96 | selectColor = ({ color = 'default' }) => () => {
97 | const { activeView } = this.state
98 | let newColor = color
99 |
100 | if(activeView === VIEWS.COLOR && color === 'default') {
101 | newColor = 'black'
102 | }
103 |
104 | if(activeView === VIEWS.FILL && color === 'default') {
105 | newColor = 'transparent'
106 | }
107 |
108 | InteractionManager.runAfterInteractions(() => {
109 | this.emit(EVENTS.CHANGE_COLOR_STYLE, { color: newColor, type: activeView })()
110 | })
111 | // this.setDefaultView()
112 | }
113 |
114 | renderColorPicker = () => {
115 | const { activeStyles = [] } = this.state
116 | return (
117 |
118 |
119 |
120 |
121 |
128 |
129 | {COLORS.map(item => {
130 | const type = VIEWS.COLOR ? 'color' : 'fill'
131 | const isActive = activeStyles.includes(`${type}-${item}`)
132 | return (
133 |
134 |
135 | {isActive && (
136 |
137 |
138 |
139 | )}
140 |
141 |
142 |
143 | )})}
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | )
152 | }
153 |
154 | renderAlign = () => {
155 | return (
156 |
157 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | )
182 | }
183 |
184 | renderDefault = () => {
185 | const { activeStyles, activeRowType, activeView } = this.state
186 |
187 | const isActiveBold = activeStyles.includes('bold')
188 | const isActiveItalic = activeStyles.includes('italic')
189 | const isActiveUnderline = activeStyles.includes('underline')
190 | const isActiveStrikeThrough = activeStyles.includes('strikethrough')
191 | const isActiveCode = activeStyles.includes('code')
192 | const isActiveLink = activeStyles.includes('link')
193 |
194 | const isDisabledBold = activeRowType.includes('heading')
195 | const isDisabledItalic = activeRowType.includes('heading')
196 | const isDisabledUnderline = activeRowType.includes('heading')
197 | const isDisabledStrikeThrough = activeRowType.includes('heading')
198 | const isDisabledCode = activeRowType.includes('code')
199 | const isDisabledLink = activeRowType.includes('link')
200 |
201 | return (
202 |
203 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 | {/*
238 |
239 | */}
240 |
241 |
242 |
243 |
244 |
245 | {/*
246 | */}
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 | {/*
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 | */}
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 | )
290 | }
291 |
292 | render() {
293 | const { activeStyles, activeRowType, activeView } = this.state
294 |
295 | return (
296 |
297 | { activeView === VIEWS.ALIGN && this.renderAlign() }
298 | { activeView === VIEWS.COLOR && this.renderColorPicker() }
299 | { activeView === VIEWS.FILL && this.renderColorPicker() }
300 | { activeView === VIEWS.DEFAULT && this.renderDefault() }
301 |
302 | )
303 | }
304 | }
305 |
306 | const styles = StyleSheet.create({
307 | toolbar: {
308 | // flex: 1,
309 | flexDirection: 'row',
310 | height: HEIGHT,
311 | borderTopWidth: 1/2,
312 | borderColor: '#e3e3e3',
313 | },
314 | contentContainerStyle: {
315 | paddingRight: 120
316 | },
317 | divider: {
318 | height: HEIGHT,
319 | width: 1/2,
320 | backgroundColor: '#e3e3e3'
321 | },
322 | button: {
323 | paddingHorizontal: 10,
324 | alignItems: 'center',
325 | justifyContent: 'center',
326 | minWidth: 40,
327 | flexDirection: 'row'
328 | },
329 | activeButton: {
330 | backgroundColor: '#e7e7e7'
331 | },
332 | disabledButton: {
333 | opacity: 0.5
334 | },
335 | iconText: {
336 | marginLeft: 5
337 | },
338 | colorDot: {
339 | minWidth: 40,
340 | },
341 | checkboxContainer: {
342 | flex: 1,
343 | justifyContent: 'center',
344 | alignItems: 'center',
345 | backgroundColor: 'rgba(55, 71, 79, 0.1)'
346 | }
347 | })
348 |
349 | export default Toolbar
--------------------------------------------------------------------------------
/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 | "@dimerapp/markdown": "^3.2.0",
11 | "@expo/browser-polyfill": "0.0.1-alpha.3",
12 | "@expo/vector-icons": "^10.0.0",
13 | "expo": "^35.0.0",
14 | "expo-asset-utils": "1.1.1",
15 | "expo-file-system": "~7.0.0",
16 | "expo-gl": "~7.0.0",
17 | "expo-image-picker": "~7.0.0",
18 | "expo-permissions": "~7.0.0",
19 | "expo-pixi": "1.2.0",
20 | "fbemitter": "^2.1.1",
21 | "lodash": "^4.17.13",
22 | "native-base": "^2.12.1",
23 | "react": "16.8.3",
24 | "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
25 | "react-native-easy-markdown": "^1.3.0",
26 | "react-native-keyboard-aware-view": "^0.0.14",
27 | "react-native-lightbox": "^0.8.0",
28 | "reactotron-react-native": "^3.4.1",
29 | "runes": "^0.4.3",
30 | "shortid": "^2.2.14"
31 | },
32 | "devDependencies": {
33 | "babel-preset-expo": "^7.0.0"
34 | },
35 | "private": true
36 | }
37 |
--------------------------------------------------------------------------------