├── .eslintignore
├── .npmignore
├── .flowconfig
├── source
├── index.js
├── view
│ ├── BlockingView.js
│ ├── utils.js
│ ├── CodeEditor.js
│ └── BaseEditorView.js
├── event
│ ├── Request.js
│ ├── ResponseListener.js
│ ├── EditorEvent.js
│ ├── EditorRequests.js
│ └── WebViewAPI.js
└── assets
│ ├── webview.js
│ └── webview_bundle.json
├── rollup.config.js
├── .prettierrc
├── .travis.yml
├── .editorconfig
├── .gitattributes
├── files
├── editor.html
└── initscript.js
├── .gitignore
├── rollup.helpers.js
├── LICENSE
├── .eslintrc
├── gulpfile.js
├── README.md
├── example
└── index.js
└── package.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .*
2 | coverage/
3 | fixtures/
4 | source/
5 | __tests__/
6 | rollup.*
7 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 | source/**/*.js
5 |
6 | [libs]
7 |
8 | [options]
9 |
--------------------------------------------------------------------------------
/source/index.js:
--------------------------------------------------------------------------------
1 | import CodeEditor from './view/CodeEditor';
2 |
3 | export default CodeEditor;
4 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { cjsConfig } from './rollup.helpers';
2 |
3 | export default [
4 | cjsConfig,
5 | ];
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "arrowParens": "always",
5 | "parser": "flow",
6 | "printWidth": 100
7 | }
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | before_script:
5 | - npm install rollup jest-cli eslint coveralls -g
6 | - npm run build
7 | - npm run lint
8 | script: npm run test -- --coverage --coverageReporters=text-lcov | coveralls
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 |
11 | # Matches multiple files with brace expansion notation
12 | # Set default charset
13 | [*.js]
14 | charset = utf-8
15 | indent_style = space
16 | indent_size = 2
17 | trim_trailing_whitespace = false
18 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto eol=lf
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/files/editor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Code Editor
7 |
16 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/source/view/BlockingView.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Text } from 'react-native';
3 |
4 | const BlockingView = () => {
5 | return (
6 |
18 |
24 | Initializing, please wait...
25 |
26 |
27 | );
28 | };
29 |
30 | export default BlockingView;
31 |
--------------------------------------------------------------------------------
/source/view/utils.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const DEFAULT_SETTINGS = {
3 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
4 | inputStyle: 'contenteditable',
5 | styleActiveLine: true,
6 | autofocus: true,
7 | };
8 |
9 | export const generateInitScript = (
10 | editorSettings = {},
11 | theme,
12 | content,
13 | viewportScaling,
14 | autoUpdateInterval = 1000,
15 | ) => {
16 | const settings = {
17 | ...DEFAULT_SETTINGS,
18 | ...editorSettings,
19 | theme,
20 | value: content,
21 | };
22 |
23 | return `
24 | (function () {
25 | setViewport(${JSON.stringify(viewportScaling, null, 2)});
26 | runEditor(${JSON.stringify(settings, null, 2)}, ${
27 | autoUpdateInterval > 0 ? autoUpdateInterval : 0
28 | });
29 | })();
30 | `;
31 | };
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Windows image file caches
2 | Thumbs.db
3 | ehthumbs.db
4 |
5 | .idea/
6 | .history/
7 | .vscode/
8 |
9 | bower_components/
10 | node_modules/
11 | coverage/
12 |
13 | # Folder config file
14 | Desktop.ini
15 |
16 | # Recycle Bin used on file shares
17 | $RECYCLE.BIN/
18 |
19 | # Windows Installer files
20 | *.cab
21 | *.msi
22 | *.msm
23 | *.msp
24 |
25 | # Windows shortcuts
26 | *.lnk
27 |
28 | # =========================
29 | # Operating System Files
30 | # =========================
31 |
32 | # OSX
33 | # =========================
34 |
35 | .DS_Store
36 | .AppleDouble
37 | .LSOverride
38 |
39 | # Thumbnails
40 | ._*
41 |
42 | # Files that might appear in the root of a volume
43 | .DocumentRevisions-V100
44 | .fseventsd
45 | .Spotlight-V100
46 | .TemporaryItems
47 | .Trashes
48 | .VolumeIcon.icns
49 |
50 | # Directories potentially created on remote AFP share
51 | .AppleDB
52 | .AppleDesktop
53 | Network Trash Folder
54 | Temporary Items
55 | .apdisk
56 |
--------------------------------------------------------------------------------
/source/event/Request.js:
--------------------------------------------------------------------------------
1 | import { getResponseEvent } from './EditorEvent';
2 | import ResponseListener from './ResponseListener';
3 |
4 | class Request {
5 | constructor(dispatcher, eventType, data = null, responseEventType = '') {
6 | this.response = new ResponseListener(
7 | dispatcher,
8 | responseEventType || getResponseEvent(eventType),
9 | );
10 | dispatcher.dispatchEvent(eventType, data);
11 | }
12 |
13 | get listening() {
14 | return this.response.listening;
15 | }
16 |
17 | then(handler) {
18 | return this.response.then(handler);
19 | }
20 |
21 | catch(handler) {
22 | return this.response.catch(handler);
23 | }
24 |
25 | cancel() {
26 | this.response.cancel();
27 | }
28 |
29 | dispose() {
30 | this.response.dispose();
31 | this.response = null;
32 | }
33 | }
34 |
35 | export const createRequestFactory = (dispatcher) => (eventType, data = null) => {
36 | const request = new Request(dispatcher, eventType, data);
37 | return request;
38 | };
39 |
40 | export default Request;
41 |
--------------------------------------------------------------------------------
/rollup.helpers.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import babel from 'rollup-plugin-babel';
4 | import json from 'rollup-plugin-json';
5 |
6 | export const LIBRARY_VAR_NAME = 'CodeEditor';
7 |
8 | export const plugins = [
9 | resolve(),
10 | babel({
11 | presets: [['module:metro-react-native-babel-preset', { disableImportExportTransform: true }]],
12 | plugins: ['@babel/plugin-external-helpers'],
13 | exclude: 'node_modules/**',
14 | runtimeHelpers: true,
15 | externalHelpers: true,
16 | babelrc: false,
17 | }),
18 | commonjs(),
19 | json(),
20 | ];
21 |
22 | export const cjsConfig = {
23 | input: 'source/index.js',
24 | output: [
25 | {
26 | file: 'index.js',
27 | sourcemap: true,
28 | exports: 'named',
29 | format: 'cjs',
30 | },
31 | ],
32 | plugins,
33 | external: [
34 | 'react',
35 | 'react-native',
36 | 'prop-types',
37 | '@actualwave/deferred',
38 | '@actualwave/event-dispatcher',
39 | '@actualwave/messageport-dispatcher',
40 | ],
41 | };
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Oleg Galaburda
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "ecmaFeatures": {
7 | "impliedStrict": true,
8 | "experimentalObjectRestSpread": true
9 | }
10 | },
11 | "env": {
12 | "browser": true,
13 | "node": true
14 | },
15 | "globals": {
16 | "jest": true,
17 | "describe": true,
18 | "it": true,
19 | "test": true,
20 | "expect": true,
21 | "should": true,
22 | "assert": true,
23 | "beforeEach": true,
24 | "afterEach": true,
25 | "beforeAll": true,
26 | "afterAll": true,
27 | "before": true,
28 | "after": true
29 | },
30 | "rules": {
31 | "arrow-parens": [
32 | "error",
33 | "always"
34 | ],
35 | "linebreak-style": 0,
36 | "no-underscore-dangle": 0,
37 | "no-restricted-syntax": [
38 | "error",
39 | "WithStatement"
40 | ],
41 | "no-param-reassign": 1,
42 | "padded-blocks": 0,
43 | "no-plusplus": 0,
44 | "function-paren-newline": 0,
45 | "object-curly-newline": 0,
46 | "no-bitwise": 0,
47 | "max-len": ["error", { "code": 120 }],
48 | "implicit-arrow-linebreak": 0,
49 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
50 | "react/sort-comp": 0
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/source/event/ResponseListener.js:
--------------------------------------------------------------------------------
1 | import deferred from '@actualwave/deferred';
2 |
3 | const stopListening = (target) => {
4 | const { dispatcher, eventType, requestHandler } = target;
5 |
6 | dispatcher.removeEventListener(eventType, requestHandler);
7 | target.listening = false;
8 | };
9 |
10 | class ResponseListener {
11 | constructor(dispatcher, eventType) {
12 | this.dispatcher = dispatcher;
13 | this.eventType = eventType;
14 | this.deferred = deferred();
15 | this.listening = true;
16 | dispatcher.addEventListener(this.eventType, this.requestHandler);
17 | }
18 |
19 | requestHandler = (event) => {
20 | this.deferred.resolve(event);
21 | stopListening(this);
22 | };
23 |
24 | cancel = () => {
25 | if (this.listening) {
26 | stopListening(this);
27 | }
28 | };
29 |
30 | reject = (data = null) => {
31 | if (this.listening) {
32 | this.deferred.reject(data);
33 | stopListening(this);
34 | }
35 | };
36 |
37 | then(handler) {
38 | return this.deferred.promise.then(handler);
39 | }
40 |
41 | catch(handler) {
42 | return this.deferred.promise.catch(handler);
43 | }
44 |
45 | dispose = () => {
46 | this.cancel();
47 | delete this.dispatcher;
48 | delete this.deferred;
49 | };
50 | }
51 |
52 | export const createResponseListenerFactory = (dispatcher) => (eventType) =>
53 | new ResponseListener(dispatcher, eventType);
54 |
55 | export default ResponseListener;
56 |
--------------------------------------------------------------------------------
/source/assets/webview.js:
--------------------------------------------------------------------------------
1 | import { Editor } from '@actualwave/codemirror-package';
2 | import webviewBundle from './webview_bundle.json';
3 |
4 | const HTML_JS_MARK = '/*#${rn-webview-editor-js}#*/'; // eslint-disable-line no-template-curly-in-string
5 |
6 | const HTML_CSS_MARK = '/*#${rn-webview-editor-css}#*/'; // eslint-disable-line no-template-curly-in-string
7 |
8 | export const getWebViewHtml = () => webviewBundle['editor.html'];
9 |
10 | const stringReplaceMark = (string, mark, content) => {
11 | const index = string.indexOf(mark);
12 |
13 | return string.substr(0, index) + content + mark + string.substr(index + mark.length);
14 | };
15 |
16 | export const appendJsToWebViewHtml = (html, js) => stringReplaceMark(html, HTML_JS_MARK, js);
17 | // html.replace(HTML_JS_MARK, `${js}\n${HTML_JS_MARK}`);
18 |
19 | export const appendCssToWebViewHtml = (html, css) => stringReplaceMark(html, HTML_CSS_MARK, css);
20 | // html.replace(HTML_CSS_MARK, `${css}\n${HTML_CSS_MARK}`);
21 |
22 | class WebViewEditor extends Editor {
23 | constructor(theme, modules) {
24 | super(true, theme, modules);
25 | }
26 |
27 | toString() {
28 | const { js, css } = this.bundle();
29 |
30 | // add editor, theme and all modules
31 | const html = appendCssToWebViewHtml(appendJsToWebViewHtml(getWebViewHtml(), js), css);
32 |
33 | // add communication layer
34 | return appendJsToWebViewHtml(
35 | html,
36 | `${webviewBundle['messageport-dispatcher.min.js']}\n${webviewBundle['initscript.js']}`,
37 | );
38 | }
39 | }
40 |
41 | export default WebViewEditor;
42 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable implicit-arrow-linebreak) */
2 | const fs = require('fs');
3 | const path = require('path');
4 | const gulp = require('gulp');
5 |
6 | const through2 = require('through2');
7 |
8 | const sourceFiles = [
9 | './files/editor.html',
10 | './node_modules/@actualwave/messageport-dispatcher/dist/messageport-dispatcher.min.js',
11 | './files/initscript.js',
12 | ];
13 |
14 | const createStream = (data) => {
15 | return through2.obj((target, enc, cb) => {
16 | const { base } = path.parse(target.path);
17 |
18 | console.log('Add module: ', base);
19 | data[base] = target.contents.toString();
20 |
21 | cb();
22 | });
23 | };
24 |
25 | const loadFileData = (filePath, data) => {
26 | return new Promise((res) => {
27 | gulp
28 | .src(filePath)
29 | .pipe(createStream(data))
30 | .pipe(gulp.dest('./tmp/'))
31 | .on('end', () => {
32 | res(data);
33 | });
34 | });
35 | };
36 |
37 | const loadData = (files, data = {}) =>
38 | new Promise((res, rej) => {
39 | Promise.all(files.map((file) => loadFileData(file, data)))
40 | .then(() => res(data))
41 | .catch(rej);
42 | });
43 |
44 | const createBundle = (files) =>
45 | loadData(files).then((data) => {
46 | if (!fs.existsSync('./source/assets')) {
47 | fs.mkdirSync('./source/assets');
48 | }
49 |
50 | fs.writeFileSync('./source/assets/webview_bundle.json', JSON.stringify(data, null, 2));
51 | });
52 |
53 | const webview = () => createBundle(sourceFiles);
54 |
55 | exports.webview = webview;
56 | exports.default = gulp.series(webview);
57 |
--------------------------------------------------------------------------------
/source/event/EditorEvent.js:
--------------------------------------------------------------------------------
1 | export const EditorEvent = {
2 | // ping from WebView to check if connection with RN component is alive
3 | INITIALIZE: 'initialize',
4 |
5 | // RN component responds to "initialize" event by sending init data, like content, history and settings
6 | HANDSHAKE: 'handshake',
7 |
8 | // WebView response to handshake, it says that all init data is applied and WebView is ready for communication
9 | INITIALIZED: 'initialized',
10 |
11 | // If autoUpdateInterval > 0, then WebView will send "autoUpdate" in intervals event after changes to Editor content made
12 | AUTO_UPDATE: 'autoUpdate',
13 |
14 | // Apply viewport settings for scaling content
15 | SET_VIEWPORT: 'setViewport',
16 |
17 | SET_OPTION: 'setOption',
18 | GET_OPTION: 'getOption',
19 |
20 | SET_VALUE: 'setValue',
21 | GET_VALUE: 'getValue',
22 | RESET_VALUE: 'resetValue',
23 | FOCUS: 'focus',
24 | GET_CURSOR: 'getCursor',
25 | SET_CURSOR: 'setCursor',
26 | GET_SELECTION: 'getSelection',
27 | SET_SELECTION: 'setSelection',
28 | REPLACE_SELECTION: 'replaceSelection',
29 | CANCEL_SELECTION: 'cancelSelection',
30 | HISTORY_READ: 'historyRead',
31 | HISTORY_WRITE: 'historyWrite',
32 | HISTORY_UNDO: 'historyUndo',
33 | HISTORY_REDO: 'historyRedo',
34 | HISTORY_CLEAR: 'historyClear',
35 | HISTORY_SIZE: 'historySize',
36 |
37 | // comes from WebView when global window.onerror handler was called
38 | WV_GLOBAL_ERROR: 'wvGlobalError',
39 |
40 | // comes from WebView when global window.log handler was called
41 | WV_LOG: 'wvLog',
42 | SCROLL_TO_CURSOR: 'scrollToCursor',
43 | UPDATE_SETTINGS: 'updateSettings',
44 | EXEC_COMMAND: 'execCommand',
45 | };
46 |
47 | export const EditorResponseEvent = {
48 | INIT_COMPLETE: 'initialized',
49 | };
50 |
51 | export const getResponseEvent = (eventType) => {
52 | if (eventType === EditorEvent.HANDSHAKE) {
53 | return EditorResponseEvent.INIT_COMPLETE;
54 | }
55 |
56 | return `${eventType}Response`;
57 | };
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Code Editor
2 |
3 | Code Editor based on [Codemirror](https://codemirror.net/).
4 |
5 | ```javascript
6 | import React, { Component } from 'react';
7 | import CodeEditor from '@actualwave/react-native-codeditor';
8 |
9 | export default () => (
10 | console.log('Initialized!')}
12 | onHistorySizeUpdate={(size) => console.log('History Size Update:', size)}
13 | onLog={(content) => console.log('Log:', content)}
14 | onError={(content) => console.log('Error:', content)}
15 | onContentUpdate={(content) => console.log('Content Update:', content)}
16 | theme="darcula"
17 | settings={{
18 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
19 | inputStyle: 'contenteditable',
20 | styleActiveLine: true,
21 | mode: 'jsx',
22 | lineNumbers: true,
23 | lineWrapping: true,
24 | foldGutter: true,
25 | matchBrackets: true,
26 | autoCloseBrackets: true,
27 | matchTags: true,
28 | autoCloseTags: true,
29 | highlightSelectionMatches: true,
30 | theme: 'darcula',
31 | }}
32 | modules={[
33 | 'addon/fold/foldgutter',
34 | 'mode/javascript/javascript',
35 | 'mode/xml/xml',
36 | 'mode/jsx/jsx',
37 | // FIXME causes unexpected new lines during editin
38 | // 'addon/selection/active-line',
39 | 'addon/edit/matchbrackets',
40 | 'addon/edit/matchtags',
41 | 'addon/search/match-highlighter',
42 | 'addon/edit/closebrackets',
43 | 'addon/edit/closetag',
44 | 'addon/fold/foldcode',
45 | 'addon/fold/foldgutter',
46 | 'addon/fold/brace-fold',
47 | 'addon/fold/comment-fold',
48 | 'addon/fold/indent-fold',
49 | 'addon/fold/xml-fold',
50 | ]}
51 | content={`import React, { Component, PropTypes } from 'react';
52 | import {
53 | View,
54 | Text,
55 | InputText,
56 | TouchableHighlight,
57 | TouchableOpacity,
58 | } from 'react-native';
59 |
60 |
61 | class ClassComponent extends Component {
62 | static propTypes = {};
63 | static defaultProps = {};
64 |
65 | componentWillMount() {
66 | }
67 |
68 | componentWillReceiveProps(nextProps, nextContext) {
69 | }
70 |
71 | render() {
72 | return (
73 |
74 | );
75 | }
76 | }
77 |
78 | export default ClassComponent;
79 | `}
80 | />
81 | );
82 | ```
83 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Sample React Native App
3 | * https://github.com/facebook/react-native
4 | * @flow
5 | */
6 |
7 | import React, { Component } from 'react';
8 | import { AppRegistry } from 'react-native';
9 | import CodeEditor from '@actualwave/react-native-codeditor';
10 |
11 | export default class RNCodEditorComponent extends Component {
12 | render() {
13 | return (
14 | (
47 |
48 |
56 |
57 | {label}
58 |
59 |
60 | );`}
61 | theme="darcula"
62 | onInitialized={() => console.log(' -- onInitialized')}
63 | onContentUpdate={() => console.log(' -- onContentUpdate')}
64 | onHistorySizeUpdate={() => console.log(' -- onHistorySizeUpdate')}
65 | />
66 | );
67 | }
68 | }
69 |
70 | AppRegistry.registerComponent('RNCodEditorComponent', () => RNCodEditorComponent);
71 |
--------------------------------------------------------------------------------
/source/event/EditorRequests.js:
--------------------------------------------------------------------------------
1 | import { EditorEvent } from './EditorEvent';
2 |
3 | class EditorRequests {
4 | constructor(requestFactory) {
5 | this.requestFactory = requestFactory;
6 | }
7 |
8 | sendRequest = (eventType, data = null, responseEventType = '') =>
9 | this.requestFactory(eventType, data, responseEventType).then((event) => event && event.data);
10 |
11 | handshake = (data) => this.sendRequest(EditorEvent.HANDSHAKE, data, EditorEvent.INITIALIZED);
12 |
13 | setViewport = ({ maximumScale, minimumScale, intialScale, userScalable, viewportWidth }) =>
14 | this.sendRequest(EditorEvent.SET_VIEWPORT, {
15 | maximumScale,
16 | minimumScale,
17 | intialScale,
18 | userScalable,
19 | viewportWidth,
20 | });
21 |
22 | setOption = (option, value) => this.sendRequest(EditorEvent.SET_OPTION, { option, value });
23 |
24 | getOption = (option) => this.sendRequest(EditorEvent.GET_OPTION, option);
25 |
26 | setValue = (data) => this.sendRequest(EditorEvent.SET_VALUE, data);
27 |
28 | getValue = (data) => this.sendRequest(EditorEvent.GET_VALUE, data);
29 |
30 | resetValue = (data) => this.sendRequest(EditorEvent.RESET_VALUE, data);
31 |
32 | focus = (data) => this.sendRequest(EditorEvent.FOCUS, data);
33 |
34 | getCursor = (data) => this.sendRequest(EditorEvent.GET_CURSOR, data);
35 |
36 | setCursor = (lineOrIndex, ch, options) =>
37 | this.sendRequest(
38 | EditorEvent.SET_CURSOR,
39 | ch === undefined ? lineOrIndex : { line: lineOrIndex, ch, options },
40 | );
41 |
42 | getSelection = (data) => this.sendRequest(EditorEvent.GET_SELECTION, data);
43 |
44 | setSelection = (anchor, head, options) =>
45 | this.sendRequest(EditorEvent.SET_SELECTION, { anchor, head, options });
46 |
47 | replaceSelection = (data) => this.sendRequest(EditorEvent.REPLACE_SELECTION, data);
48 |
49 | cancelSelection = () => this.sendRequest(EditorEvent.CANCEL_SELECTION);
50 |
51 | historyRead = () => this.sendRequest(EditorEvent.HISTORY_READ);
52 |
53 | historyWrite = (data) => this.sendRequest(EditorEvent.HISTORY_WRITE, data);
54 |
55 | historyUndo = (data) => this.sendRequest(EditorEvent.HISTORY_UNDO, data);
56 |
57 | historyRedo = (data) => this.sendRequest(EditorEvent.HISTORY_REDO, data);
58 |
59 | historyClear = (data) => this.sendRequest(EditorEvent.HISTORY_CLEAR, data);
60 |
61 | historySize = (data) => this.sendRequest(EditorEvent.HISTORY_SIZE, data);
62 |
63 | scrollToCursor = (margin) => this.sendRequest(EditorEvent.SCROLL_TO_CURSOR, margin);
64 |
65 | updateSettings = (settings) => this.sendRequest(EditorEvent.UPDATE_SETTINGS, settings);
66 |
67 | execCommand = (command, ...args) => this.sendRequest(EditorEvent.EXEC_COMMAND, { command, args });
68 | }
69 |
70 | export default EditorRequests;
71 |
--------------------------------------------------------------------------------
/source/view/CodeEditor.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import BaseEditorView from './BaseEditorView';
3 | import { EditorEvent, getResponseEvent } from '../event/EditorEvent';
4 |
5 | class CodeEditor extends BaseEditorView {
6 | static propTypes = {
7 | ...BaseEditorView.propTypes,
8 | onContentUpdate: PropTypes.func.isRequired,
9 | forceUpdates: PropTypes.bool,
10 | };
11 |
12 | static defaultProps = {
13 | ...BaseEditorView.defaultProps,
14 | forceUpdates: false,
15 | };
16 |
17 | currentContent = this.props.content;
18 |
19 | componentDidMount() {
20 | const { settings, modules, theme } = this.props;
21 |
22 | this.editor.setTheme(theme);
23 | this.editor.resetModules(modules);
24 |
25 | this.sendUpdatedSettings(settings);
26 | }
27 |
28 | componentDidUpdate(oldProps) {
29 | const {
30 | content: oldContent,
31 | modules: oldModules,
32 | theme: oldTheme,
33 | settings: oldSettings,
34 | viewport: oldViewport,
35 | } = oldProps;
36 | const { content, theme, modules, settings, viewport } = this.props;
37 |
38 | if (oldTheme !== theme || oldModules !== modules) {
39 | this.updateEditorHtml();
40 | this.updateInitScript();
41 | }
42 |
43 | if (oldContent !== content) {
44 | this.sendUpdatedContent(content);
45 | }
46 |
47 | if (oldSettings !== settings) {
48 | this.sendUpdatedSettings(settings);
49 | }
50 |
51 | if (oldViewport !== viewport) {
52 | this.sendUpdatedViewport(viewport);
53 | }
54 | }
55 |
56 | shouldComponentUpdate(
57 | { settings, modules, theme, viewport },
58 | { initialized, source, initScript },
59 | ) {
60 | return (
61 | this.state.initialized !== initialized ||
62 | this.props.settings !== settings ||
63 | this.props.viewport !== viewport ||
64 | this.props.modules !== modules ||
65 | this.state.theme !== theme
66 | );
67 | }
68 |
69 | onWebViewInitialized(api) {
70 | const { forceUpdates, viewport, settings } = this.props;
71 | super.onWebViewInitialized(api);
72 |
73 | if (forceUpdates) {
74 | this.sendUpdatedViewport(viewport);
75 | this.sendUpdatedSettings(settings);
76 | this.sendUpdatedContent(this.currentContent, true);
77 | }
78 |
79 | this.api.addEventListener(getResponseEvent(EditorEvent.GET_VALUE), this.onGetValueResponse);
80 | this.api.addEventListener(EditorEvent.AUTO_UPDATE, this.onGetValueResponse);
81 | }
82 |
83 | onGetValueResponse = (event) => {
84 | const content = event.data;
85 | this.currentContent = content;
86 | this.props.onContentUpdate(content);
87 | };
88 |
89 | sendUpdatedContent(content, force = false) {
90 | if (force || (this.state.initialized && this.currentContent !== content)) {
91 | this.api.setValue(content);
92 |
93 | if (force) {
94 | this.api.historyClear();
95 | }
96 |
97 | this.currentContent = content;
98 | }
99 | }
100 |
101 | sendUpdatedSettings(settings) {
102 | if (this.state.initialized) {
103 | this.api.updateSettings(settings);
104 | }
105 | }
106 |
107 | sendUpdatedViewport(viewport) {
108 | if (this.state.initialized) {
109 | this.api.setViewport(viewport);
110 | }
111 | }
112 | }
113 |
114 | export default CodeEditor;
115 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@actualwave/react-native-codeditor",
3 | "description": "",
4 | "version": "0.1.2",
5 | "main": "source/index.js",
6 | "private": true,
7 | "keywords": [
8 | "js",
9 | "javascript"
10 | ],
11 | "homepage": "https://github.com/burdiuz/react-native-codeditor",
12 | "bugs": {
13 | "url": "https://github.com/burdiuz/react-native-codeditor/issues",
14 | "email": "burdiuz@gmail.com"
15 | },
16 | "license": "MIT",
17 | "author": "Oleg Galaburda (http://actualwave.com/)",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/burdiuz/react-native-codeditor.git"
21 | },
22 | "dependencies": {
23 | "@actualwave/deferred": "0.0.1",
24 | "@actualwave/event-dispatcher": "^1.2.2",
25 | "@actualwave/messageport-dispatcher": "^1.1.3"
26 | },
27 | "peerDependencies": {
28 | "react": ">15.0.0",
29 | "react-native": ">0.57.3",
30 | "react-native-webview": "5.12.1",
31 | "prop-types": "15.6.2"
32 | },
33 | "devDependencies": {
34 | "@actualwave/codemirror-package": "git+https://github.com/burdiuz/js-codemirror-package.git",
35 | "@babel/core": "^7.5.5",
36 | "@babel/plugin-external-helpers": "^7.2.0",
37 | "@babel/runtime": "^7.5.5",
38 | "babel-eslint": "^10.0.1",
39 | "babel-loader": "^8.0.5",
40 | "babel-plugin-istanbul": "^5.2.0",
41 | "babel-preset-jest": "^24.6.0",
42 | "eslint": "^5.16.0",
43 | "eslint-config-airbnb": "^17.1.0",
44 | "eslint-plugin-import": "^2.17.0",
45 | "eslint-plugin-jsx-a11y": "^6.2.1",
46 | "eslint-plugin-react": "^7.12.4",
47 | "gulp": "^4.0.2",
48 | "husky": "^1.3.1",
49 | "jest": "^24.7.1",
50 | "lint-staged": "^8.1.5",
51 | "metro-react-native-babel-preset": "^0.54.0",
52 | "prettier": "^1.17.0",
53 | "rollup-plugin-babel": "^4.3.2",
54 | "rollup-plugin-commonjs": "^9.3.4",
55 | "rollup-plugin-json": "^4.0.0",
56 | "rollup-plugin-livereload": "^1.0.0",
57 | "rollup-plugin-node-resolve": "^4.2.3",
58 | "rollup-plugin-serve": "^1.0.2",
59 | "rollup-plugin-terser": "^4.0.4",
60 | "through2": "^3.0.1"
61 | },
62 | "lint-staged": {
63 | "source/**/*.js": [
64 | "prettier --write",
65 | "eslint --fix",
66 | "git add"
67 | ]
68 | },
69 | "babel": {
70 | "env": {
71 | "test": {
72 | "plugins": [
73 | "babel-plugin-transform-es2015-modules-commonjs",
74 | "babel-plugin-transform-flow-strip-types",
75 | "babel-plugin-transform-class-properties",
76 | [
77 | "babel-plugin-transform-object-rest-spread",
78 | {
79 | "useBuiltIns": true
80 | }
81 | ]
82 | ],
83 | "presets": [
84 | "jest"
85 | ]
86 | }
87 | }
88 | },
89 | "jest": {
90 | "collectCoverage": true,
91 | "coverageReporters": [
92 | "json",
93 | "lcov"
94 | ],
95 | "coverageDirectory": "coverage",
96 | "collectCoverageFrom": [
97 | "source/**/*.js",
98 | "!**/node_modules/**",
99 | "!**/vendor/**"
100 | ],
101 | "modulePathIgnorePatterns": [
102 | "\\/\\."
103 | ]
104 | },
105 | "scripts": {
106 | "start": "npm run build",
107 | "build": "rollup --config",
108 | "build:watch": "rollup --config --watch \"./source\"",
109 | "lint": "./node_modules/.bin/eslint \"source/**/*.js\"",
110 | "test": "jest --colors",
111 | "test:watch": "jest --colors --watch",
112 | "lint-staged": "lint-staged",
113 | "precommit": "npm run build && npm run test && npm run lint-staged"
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/source/event/WebViewAPI.js:
--------------------------------------------------------------------------------
1 | import MessagePortDispatcher from '@actualwave/messageport-dispatcher';
2 | import Request from './Request';
3 | import { EditorEvent } from './EditorEvent';
4 | import EditorRequests from './EditorRequests';
5 |
6 | const rawEvents = {
7 | // FIXME don't really remember why I added this
8 | };
9 |
10 | const composeErrorMessage = (event) => {
11 | const { data } = event;
12 |
13 | if (data.error) {
14 | return data.error;
15 | }
16 |
17 | const { message, source, lineno, colno } = data;
18 |
19 | return { name: 'Error', message, fileName: source, lineNumber: lineno, columnNumber: colno };
20 | };
21 |
22 | class MessagePortDummy {
23 | constructor(target) {
24 | this.listeners = new Set();
25 | this.target = target;
26 | }
27 |
28 | // original implementation using WebView.postMessage
29 | // postMessage = (event) => this.target.postMessage(JSON.stringify(event));
30 |
31 | postMessage = (event) => {
32 | const eventStr = JSON.stringify(event);
33 | this.target.injectJavaScript(`(
34 | (target, data) => {
35 | const e = new CustomEvent('message');
36 | e.data = data;
37 | target.dispatchEvent(e);
38 | }
39 | )(window, ${JSON.stringify(eventStr)})`);
40 | };
41 |
42 | addEventListener = (type, listener) => {
43 | if (type !== 'message') {
44 | throw new Error(`MessagePortDummy is intended for single event type and its not "${type}"`);
45 | }
46 |
47 | this.listeners.add(listener);
48 | };
49 |
50 | callMessageListeners = (message) => {
51 | this.listeners.forEach((listener) => listener(message));
52 | };
53 | }
54 |
55 | class WebViewAPI extends EditorRequests {
56 | webView = null;
57 |
58 | dispatcher = null;
59 |
60 | initialized = false;
61 |
62 | constructor({ onInitialized, onHistorySizeUpdate, onLog, onError }) {
63 | const requiestFactory = (eventType, data = null, responseEventType = '') =>
64 | new Request(this.dispatcher, eventType, data, responseEventType);
65 |
66 | super(requiestFactory);
67 |
68 | this.handlers = { onInitialized, onHistorySizeUpdate, onLog, onError };
69 | }
70 |
71 | initialize(webView, { content, history, settings } = {}) {
72 | this.initialized = false;
73 | this.webView = webView;
74 | this.initData = { content, history, settings };
75 | this.port = new MessagePortDummy(webView);
76 | this.dispatcher = new MessagePortDispatcher(this.port, null, this.preprocessIncomingMessages);
77 |
78 | this.dispatcher.addEventListener(EditorEvent.INITIALIZE, this.webViewInitializeHandler);
79 | this.dispatcher.addEventListener(EditorEvent.WV_GLOBAL_ERROR, this.webViewGlobalErrorHandler);
80 | this.dispatcher.addEventListener(EditorEvent.WV_LOG, this.webViewLogHandler);
81 | }
82 |
83 | /**
84 | * @private
85 | */
86 | preprocessIncomingMessages = (event) => {
87 | const { data: eventData } = event;
88 |
89 | if (rawEvents[event.type]) {
90 | return event;
91 | }
92 |
93 | if (eventData && typeof eventData === 'object') {
94 | const { meta, data } = eventData;
95 |
96 | if (meta) {
97 | this.readMessageMetaData(meta);
98 | event.data = data;
99 | }
100 | }
101 |
102 | return event;
103 | };
104 |
105 | /**
106 | * @private
107 | */
108 | readMessageMetaData(meta) {
109 | const { historySize } = meta;
110 | const { onHistorySizeUpdate } = this.handlers;
111 |
112 | if (onHistorySizeUpdate) {
113 | onHistorySizeUpdate(historySize);
114 | }
115 | }
116 |
117 | /**
118 | * @private
119 | */
120 | webViewInitializeHandler = () => {
121 | this.handshake(this.initData).then(this.webViewInitializedHandler);
122 | };
123 |
124 | /**
125 | * @private
126 | */
127 | webViewInitializedHandler = () => {
128 | this.initialized = true;
129 | /*
130 | * WebView may be reinitialized
131 | * this.initData = {};
132 | */
133 |
134 | this.handlers.onInitialized(this);
135 | };
136 |
137 | /**
138 | * @private
139 | */
140 | webViewLogHandler = (event) => {
141 | const { onLog } = this.handlers;
142 |
143 | if (!onLog) {
144 | return;
145 | }
146 |
147 | onLog(event.data);
148 | };
149 |
150 | /**
151 | * @private
152 | */
153 | webViewGlobalErrorHandler = (event) => {
154 | const { onError } = this.handlers;
155 |
156 | if (!onError) {
157 | return;
158 | }
159 |
160 | onError(composeErrorMessage(event));
161 | };
162 |
163 | addEventListener = (eventType, listener) => this.dispatcher.addEventListener(eventType, listener);
164 |
165 | hasEventListener = (eventType) => this.dispatcher.hasEventListener(eventType);
166 |
167 | removeEventListener = (eventType, listener) =>
168 | this.dispatcher.removeEventListener(eventType, listener);
169 |
170 | dispatchEvent = (event, data = null) => this.dispatcher.dispatchEvent(event, data);
171 |
172 | onMessage = (event) => this.port.callMessageListeners(event);
173 |
174 | injectJavaScript = (jsCode) => this.webView.injectJavaScript(jsCode);
175 |
176 | requestFocus = () => this.webView.requestFocus();
177 |
178 | focus = (data) => {
179 | try {
180 | // undocumented command for react-native-webview
181 | this.webView.requestFocus();
182 | } catch (error) {
183 | console.error(error);
184 | }
185 |
186 | return this.sendRequest(EditorEvent.FOCUS, data);
187 | };
188 | }
189 |
190 | export default WebViewAPI;
191 |
--------------------------------------------------------------------------------
/source/view/BaseEditorView.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View, StyleSheet } from 'react-native';
3 | import { WebView } from 'react-native-webview';
4 | import PropTypes from 'prop-types';
5 | import WebViewEditor from '../assets/webview';
6 | import WebViewAPI from '../event/WebViewAPI';
7 | import BlockingView from './BlockingView';
8 | import { generateInitScript } from './utils';
9 |
10 | const styles = StyleSheet.create({
11 | container: { position: 'relative', flex: 1 },
12 | webView: { flex: 1 },
13 | });
14 |
15 | const WHITELIST = [];
16 |
17 | class BaseEditorView extends Component {
18 | static propTypes = {
19 | onInitialized: PropTypes.func.isRequired,
20 | onHistorySizeUpdate: PropTypes.func.isRequired,
21 | onLog: PropTypes.func.isRequired,
22 | onError: PropTypes.func.isRequired,
23 | onWebViewRefUpdated: PropTypes.func,
24 | onLoad: PropTypes.func,
25 | onLoadStart: PropTypes.func,
26 | onLoadProgress: PropTypes.func,
27 | onLoadEnd: PropTypes.func,
28 | onNavigationStateChange: PropTypes.func,
29 | renderBlockingView: PropTypes.func,
30 | autoUpdateInterval: PropTypes.number,
31 | theme: PropTypes.string,
32 | modules: PropTypes.arrayOf(PropTypes.string),
33 | content: PropTypes.string,
34 | settings: PropTypes.shape({}),
35 | viewport: PropTypes.shape({}),
36 | allowFileAccess: PropTypes.bool,
37 | };
38 |
39 | static defaultProps = {
40 | renderBlockingView: () => ,
41 | autoUpdateInterval: 1000,
42 | theme: undefined,
43 | settings: {
44 | inputStyle: 'contenteditable',
45 | styleActiveLine: true,
46 | },
47 | viewport: {},
48 | modules: [
49 | 'addon/fold/foldgutter',
50 | 'addon/edit/matchbrackets',
51 | 'addon/edit/matchtags',
52 | 'addon/search/match-highlighter',
53 | 'addon/edit/closebrackets',
54 | 'addon/edit/closetag',
55 | 'addon/fold/foldcode',
56 | 'addon/fold/foldgutter',
57 | 'addon/fold/brace-fold',
58 | 'addon/fold/comment-fold',
59 | 'addon/fold/indent-fold',
60 | 'addon/fold/xml-fold',
61 | ],
62 | content: '',
63 | onWebViewRefUpdated: undefined,
64 | onLoad: undefined,
65 | onLoadStart: undefined,
66 | onLoadProgress: undefined,
67 | onLoadEnd: undefined,
68 | onNavigationStateChange: undefined,
69 | allowFileAccess: true,
70 | };
71 |
72 | api = null;
73 |
74 | webView = null;
75 |
76 | constructor(props) {
77 | super(props);
78 |
79 | this.editor = new WebViewEditor(props.theme, props.modules);
80 |
81 | this.baseUrlHref = 'https://actualwave.com/react-native-codeditor';
82 | this.baseUrlIndex = 0;
83 | this.updateBaseUrl();
84 |
85 | const { settings, theme, content, viewport, autoUpdateInterval } = props;
86 |
87 | this.state = {
88 | initialized: false,
89 | source: {
90 | html: this.editor.toString(),
91 | baseUrl: this.editorBaseUrl,
92 | },
93 | initScript: generateInitScript(settings, theme, content, viewport, autoUpdateInterval),
94 | };
95 |
96 | this.api = new WebViewAPI({
97 | onInitialized: (api) => this.onWebViewInitialized(api),
98 | onHistorySizeUpdate: (size) => this.onWebViewHistorySizeUpdate(size),
99 | onLog: (log) => this.onWebViewLog(log),
100 | onError: (error) => this.onWebViewGlobalError(error),
101 | });
102 | }
103 |
104 | updateBaseUrl() {
105 | this.baseUrl = `${this.baseUrlHref}/${++this.baseUrlIndex}`;
106 | }
107 |
108 | componentDidUpdate({
109 | theme: oldTheme,
110 | modules: oldModules,
111 | settings: oldSettings,
112 | viewport: oldViewport,
113 | }) {
114 | const { theme, modules, settings, viewport } = this.props;
115 |
116 | if (oldTheme !== theme || oldModules !== modules) {
117 | this.updateEditorHtml();
118 | }
119 |
120 | if (oldTheme !== theme || oldSettings !== settings || oldViewport !== viewport) {
121 | this.updateInitScript();
122 | }
123 | }
124 |
125 | updateEditorHtml({ theme, modules } = this.props) {
126 | this.editor.setTheme(theme);
127 | this.editor.resetModules([...modules]);
128 |
129 | const html = this.editor.toString();
130 |
131 | this.setState({
132 | source: {
133 | html,
134 | baseUrl: this.editorBaseUrl,
135 | },
136 | initialized: false,
137 | });
138 | }
139 |
140 | updateInitScript({ settings, theme, content, viewport, autoUpdateInterval } = this.props) {
141 | this.setState({
142 | initScript: generateInitScript(settings, theme, content, viewport, autoUpdateInterval),
143 | initialized: false,
144 | });
145 | }
146 |
147 | get editorBaseUrl() {
148 | return this.baseUrl;
149 | }
150 |
151 | onWebViewInitialized(api) {
152 | this.setState({ initialized: true }, () => {
153 | const { onInitialized } = this.props;
154 |
155 | onInitialized(api);
156 | });
157 | }
158 |
159 | onWebViewLog(log) {
160 | const { onLog } = this.props;
161 |
162 | onLog(...log);
163 | }
164 |
165 | onWebViewGlobalError(error) {
166 | const { onError } = this.props;
167 |
168 | onError(error);
169 | }
170 |
171 | onWebViewHistorySizeUpdate(size) {
172 | const { onHistorySizeUpdate } = this.props;
173 |
174 | onHistorySizeUpdate(size);
175 | }
176 |
177 | handleWebViewReference = (webView) => {
178 | const { content, settings, onWebViewRefUpdated } = this.props;
179 | this.webView = webView;
180 |
181 | this.setState({
182 | initialized: false,
183 | });
184 |
185 | if (webView) {
186 | this.api.initialize(webView, { content, settings });
187 | }
188 |
189 | if (onWebViewRefUpdated) {
190 | onWebViewRefUpdated(webView);
191 | }
192 | };
193 |
194 | renderBlockingView() {
195 | const { renderBlockingView } = this.props;
196 | const { initialized } = this.state;
197 |
198 | if (!initialized) {
199 | return renderBlockingView();
200 | }
201 | return null;
202 | }
203 |
204 | handleLoadError = (...args) => console.log('WebView Load Error:', ...args);
205 |
206 | render() {
207 | const { initScript } = this.state;
208 | const {
209 | onLoad,
210 | onLoadStart,
211 | onLoadProgress,
212 | onLoadEnd,
213 | onNavigationStateChange,
214 | allowFileAccess,
215 | } = this.props;
216 |
217 | return (
218 |
219 |
235 | {this.renderBlockingView()}
236 |
237 | );
238 | }
239 | }
240 |
241 | export default BaseEditorView;
242 |
--------------------------------------------------------------------------------
/files/initscript.js:
--------------------------------------------------------------------------------
1 | const { createMessagePortDispatcher } = MessagePortDispatcher;
2 |
3 | let _initializeId;
4 | let editor = null;
5 |
6 | const generateViewportParamsString = (intial, scalable, width, max, min) => {
7 | const params = {
8 | width,
9 | 'initial-scale': intial,
10 | 'maximum-scale': max,
11 | 'minimum-scale': min,
12 | 'user-scalable': scalable ? 'yes' : 'no',
13 | };
14 |
15 | return Object.keys(params)
16 | .filter((name) => params[name] !== undefined)
17 | .map((name) => `${name}=${params[name]}`)
18 | .join(', ');
19 | };
20 |
21 | const setViewportParams = (
22 | intial = 1,
23 | scalable = true,
24 | width = 'device-width',
25 | max = undefined,
26 | min = undefined,
27 | ) => {
28 | const content = generateViewportParamsString(intial, scalable, width, max, min);
29 | let meta = document.querySelector('meta[name="viewport"]');
30 |
31 | if (meta) {
32 | meta.setAttribute('content', content);
33 | } else {
34 | meta = document.createElement('meta');
35 | meta.setAttribute('name', 'viewport');
36 | meta.setAttribute('content', content);
37 | document.querySelector('head').prepend(meta);
38 | }
39 | };
40 |
41 | /*
42 | Set viewport scaling options
43 | */
44 | const setViewport = ({
45 | intialScale,
46 | userScalable,
47 | viewportWidth,
48 | maximumScale,
49 | minimumScale,
50 | } = {}) => setViewportParams(intialScale, userScalable, viewportWidth, maximumScale, minimumScale);
51 |
52 | const augmentData = (data) => ({
53 | meta: {
54 | historySize: editor ? editor.historySize() : null,
55 | },
56 | data,
57 | });
58 |
59 | const augmentEvent = ({ type, data }) => ({
60 | type,
61 | data: augmentData(data),
62 | });
63 |
64 | const dispatcherTarget = {
65 | postMessage: (data) => {
66 | window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify(data));
67 | },
68 | addEventListener: (eventType, listener) => {
69 | document.addEventListener(eventType, listener);
70 | window.addEventListener(eventType, listener);
71 | },
72 | };
73 |
74 | const dispatcher = createMessagePortDispatcher(dispatcherTarget, null, null, augmentEvent);
75 |
76 | window.onerror = (message, source, lineno, colno, error) => {
77 | const data = {
78 | message: message || !error ? String(message) : error.message,
79 | source,
80 | lineno,
81 | colno,
82 | };
83 |
84 | if (error) {
85 | data.error = Object.assign({ message: message }, error);
86 | }
87 |
88 | dispatcher.dispatchEvent('wvGlobalError', data);
89 | };
90 |
91 | window.log = (...message) => {
92 | dispatcher.dispatchEvent('wvLog', message);
93 | };
94 |
95 | /**
96 | * Generate API event request/response pair
97 | * @param {*} type
98 | * @param {*} handler
99 | * @param {*} responseType
100 | */
101 | const listenForAPIEvent = (type, handler, responseType) =>
102 | dispatcher.addEventListener(type, (event) => {
103 | if (responseType === undefined) {
104 | responseType = `${type}Response`;
105 | }
106 |
107 | const result = handler(event);
108 |
109 | if (responseType) {
110 | dispatcher.dispatchEvent(responseType, result);
111 | }
112 | });
113 |
114 | const setEditorSettings = (settings) =>
115 | Object.keys(settings).forEach((key) => {
116 | editor.setOption(key, settings[key]);
117 | });
118 |
119 | const getPosFromIndex = (pos) => {
120 | if (typeof pos === 'number') {
121 | return editor.posFromIndex(pos);
122 | }
123 |
124 | return pos;
125 | };
126 |
127 | const alterPosWithIndex = (pos) => {
128 | if (!pos) {
129 | return pos;
130 | }
131 |
132 | return {
133 | ...pos,
134 | index: editor.indexFromPos(pos),
135 | };
136 | };
137 |
138 | /**
139 | * Initialize all API event listeners
140 | */
141 | const initEventListeners = (autoUpdateInterval) => {
142 | listenForAPIEvent('setViewport', (event) => {
143 | setViewport(event.data || {});
144 | });
145 |
146 | listenForAPIEvent('getOption', ({ data: option }) => editor.getOption(option));
147 |
148 | listenForAPIEvent('setOption', ({ data: { option, value } }) => editor.setOption(option, value));
149 |
150 | listenForAPIEvent('setValue', (event) => {
151 | const newValue = event.data || '';
152 | const currentValue = editor.getValue();
153 |
154 | if (newValue !== currentValue) {
155 | editor.setValue(newValue);
156 | }
157 | });
158 |
159 | listenForAPIEvent('getValue', () => editor.getValue());
160 |
161 | listenForAPIEvent('resetValue', (event) => {
162 | const newValue = event.data || '';
163 | const currentValue = editor.getValue();
164 |
165 | if (newValue !== currentValue) {
166 | editor.setValue(newValue);
167 | }
168 |
169 | editor.clearHistory();
170 | });
171 |
172 | listenForAPIEvent('focus', () => editor.focus());
173 |
174 | listenForAPIEvent('getCursor', ({ data }) => alterPosWithIndex(editor.getCursor(data)));
175 |
176 | listenForAPIEvent('setCursor', ({ data }) => {
177 | const { line, ch, options } = getPosFromIndex(data);
178 |
179 | editor.setCursor(line, ch, options);
180 | });
181 |
182 | listenForAPIEvent('getSelection', () => editor.getSelection());
183 |
184 | listenForAPIEvent('setSelection', ({ data: { anchor, head, options } }) =>
185 | editor.setSelection(getPosFromIndex(anchor), getPosFromIndex(head), options),
186 | );
187 |
188 | listenForAPIEvent('replaceSelection', (event) => editor.replaceSelection(event.data));
189 |
190 | listenForAPIEvent('cancelSelection', (event) => editor.setSelection(editor.getCursor('head')));
191 |
192 | listenForAPIEvent('historyUndo', () => {
193 | editor.undo();
194 | });
195 |
196 | listenForAPIEvent('execCommand', ({ data: { command, args = [] } }) => {
197 | editor.execCommand(command, ...args);
198 | });
199 |
200 | listenForAPIEvent('historyRedo', () => {
201 | editor.redo();
202 | });
203 |
204 | listenForAPIEvent('historyClear', () => {
205 | editor.clearHistory();
206 | });
207 |
208 | listenForAPIEvent('historyRead', () => editor.getHistory());
209 |
210 | listenForAPIEvent('historyWrite', (event) => {
211 | editor.setHistory(event.data);
212 | });
213 |
214 | listenForAPIEvent('historySize', () => editor.historySize());
215 |
216 | listenForAPIEvent('scrollToCursor', (event) => {
217 | const margin = event.data;
218 |
219 | editor.scrollIntoView(null, margin);
220 | });
221 |
222 | listenForAPIEvent('updateSettings', (event) => {
223 | const settings = event.data;
224 |
225 | setEditorSettings(settings);
226 | });
227 |
228 | listenForAPIEvent('handshake', () => clearInterval(_initializeId), 'initialized');
229 |
230 | let waitingForUpdate = false;
231 |
232 | if (autoUpdateInterval) {
233 | editor.on('change', () => {
234 | if (!waitingForUpdate) {
235 | waitingForUpdate = true;
236 |
237 | setTimeout(() => {
238 | waitingForUpdate = false;
239 | dispatcher.dispatchEvent('autoUpdate', editor.getValue());
240 | }, autoUpdateInterval);
241 | }
242 | });
243 | }
244 | };
245 |
246 | /**
247 | * Entry point of WebView code editor, inits API and starts handshake
248 | * @param {*} settings
249 | */
250 | const runEditor = (settings, autoUpdateInterval = 0) => {
251 | editor = new CodeMirror(document.body, settings);
252 | editor.setSize('100%', '100%');
253 |
254 | initEventListeners(autoUpdateInterval);
255 |
256 | _initializeId = setInterval(() => dispatcher.dispatchEvent('initialize'), 500);
257 | };
258 |
--------------------------------------------------------------------------------
/source/assets/webview_bundle.json:
--------------------------------------------------------------------------------
1 | {
2 | "initscript.js": "const { createMessagePortDispatcher } = MessagePortDispatcher;\n\nlet _initializeId;\nlet editor = null;\n\nconst generateViewportParamsString = (intial, scalable, width, max, min) => {\n const params = {\n width,\n 'initial-scale': intial,\n 'maximum-scale': max,\n 'minimum-scale': min,\n 'user-scalable': scalable ? 'yes' : 'no',\n };\n\n return Object.keys(params)\n .filter((name) => params[name] !== undefined)\n .map((name) => `${name}=${params[name]}`)\n .join(', ');\n};\n\nconst setViewportParams = (\n intial = 1,\n scalable = true,\n width = 'device-width',\n max = undefined,\n min = undefined,\n) => {\n const content = generateViewportParamsString(intial, scalable, width, max, min);\n let meta = document.querySelector('meta[name=\"viewport\"]');\n\n if (meta) {\n meta.setAttribute('content', content);\n } else {\n meta = document.createElement('meta');\n meta.setAttribute('name', 'viewport');\n meta.setAttribute('content', content);\n document.querySelector('head').prepend(meta);\n }\n};\n\n/*\n Set viewport scaling options\n*/\nconst setViewport = ({\n intialScale,\n userScalable,\n viewportWidth,\n maximumScale,\n minimumScale,\n} = {}) => setViewportParams(intialScale, userScalable, viewportWidth, maximumScale, minimumScale);\n\nconst augmentData = (data) => ({\n meta: {\n historySize: editor ? editor.historySize() : null,\n },\n data,\n});\n\nconst augmentEvent = ({ type, data }) => ({\n type,\n data: augmentData(data),\n});\n\nconst dispatcherTarget = {\n postMessage: (data) => {\n window.ReactNativeWebView && window.ReactNativeWebView.postMessage(JSON.stringify(data));\n },\n addEventListener: (eventType, listener) => {\n document.addEventListener(eventType, listener);\n window.addEventListener(eventType, listener);\n },\n};\n\nconst dispatcher = createMessagePortDispatcher(dispatcherTarget, null, null, augmentEvent);\n\nwindow.onerror = (message, source, lineno, colno, error) => {\n const data = {\n message: message || !error ? String(message) : error.message,\n source,\n lineno,\n colno,\n };\n\n if (error) {\n data.error = Object.assign({ message: message }, error);\n }\n\n dispatcher.dispatchEvent('wvGlobalError', data);\n};\n\nwindow.log = (...message) => {\n dispatcher.dispatchEvent('wvLog', message);\n};\n\n/**\n * Generate API event request/response pair\n * @param {*} type\n * @param {*} handler\n * @param {*} responseType\n */\nconst listenForAPIEvent = (type, handler, responseType) =>\n dispatcher.addEventListener(type, (event) => {\n if (responseType === undefined) {\n responseType = `${type}Response`;\n }\n\n const result = handler(event);\n\n if (responseType) {\n dispatcher.dispatchEvent(responseType, result);\n }\n });\n\nconst setEditorSettings = (settings) =>\n Object.keys(settings).forEach((key) => {\n editor.setOption(key, settings[key]);\n });\n\n const getPosFromIndex = (pos) => {\n if (typeof pos === 'number') {\n return editor.posFromIndex(pos);\n }\n\n return pos;\n };\n\n const alterPosWithIndex = (pos) => {\n if (!pos) {\n return pos;\n }\n\n return {\n ...pos,\n index: editor.indexFromPos(pos),\n };\n };\n\n/**\n * Initialize all API event listeners\n */\nconst initEventListeners = (autoUpdateInterval) => {\n listenForAPIEvent('setViewport', (event) => {\n setViewport(event.data || {});\n });\n\n listenForAPIEvent('getOption', ({ data: option }) => editor.getOption(option));\n\n listenForAPIEvent('setOption', ({ data: { option, value } }) => editor.setOption(option, value));\n\n listenForAPIEvent('setValue', (event) => {\n const newValue = event.data || '';\n const currentValue = editor.getValue();\n\n if (newValue !== currentValue) {\n editor.setValue(newValue);\n }\n });\n\n listenForAPIEvent('getValue', () => editor.getValue());\n\n listenForAPIEvent('resetValue', (event) => {\n const newValue = event.data || '';\n const currentValue = editor.getValue();\n\n if (newValue !== currentValue) {\n editor.setValue(newValue);\n }\n\n editor.clearHistory();\n });\n\n listenForAPIEvent('focus', () => editor.focus());\n\n listenForAPIEvent('getCursor', ({ data }) => alterPosWithIndex(editor.getCursor(data)));\n\n listenForAPIEvent('setCursor', ({ data }) => {\n const { line, ch, options } = getPosFromIndex(data);\n\n editor.setCursor(line, ch, options);\n });\n\n listenForAPIEvent('getSelection', () => editor.getSelection());\n\n listenForAPIEvent('setSelection', ({ data: { anchor, head, options } }) =>\n editor.setSelection(getPosFromIndex(anchor), getPosFromIndex(head), options),\n );\n\n listenForAPIEvent('replaceSelection', (event) => editor.replaceSelection(event.data));\n\n listenForAPIEvent('cancelSelection', (event) => editor.setSelection(editor.getCursor('head')));\n\n listenForAPIEvent('historyUndo', () => {\n editor.undo();\n });\n\n listenForAPIEvent('execCommand', ({ data: { command, args = [] } }) => {\n editor.execCommand(command, ...args);\n });\n\n listenForAPIEvent('historyRedo', () => {\n editor.redo();\n });\n\n listenForAPIEvent('historyClear', () => {\n editor.clearHistory();\n });\n\n listenForAPIEvent('historyRead', () => editor.getHistory());\n\n listenForAPIEvent('historyWrite', (event) => {\n editor.setHistory(event.data);\n });\n\n listenForAPIEvent('historySize', () => editor.historySize());\n\n listenForAPIEvent('scrollToCursor', (event) => {\n const margin = event.data;\n\n editor.scrollIntoView(null, margin);\n });\n\n listenForAPIEvent('updateSettings', (event) => {\n const settings = event.data;\n\n setEditorSettings(settings);\n });\n\n listenForAPIEvent('handshake', () => clearInterval(_initializeId), 'initialized');\n\n let waitingForUpdate = false;\n\n if (autoUpdateInterval) {\n editor.on('change', () => {\n if (!waitingForUpdate) {\n waitingForUpdate = true;\n\n setTimeout(() => {\n waitingForUpdate = false;\n dispatcher.dispatchEvent('autoUpdate', editor.getValue());\n }, autoUpdateInterval);\n }\n });\n }\n};\n\n/**\n * Entry point of WebView code editor, inits API and starts handshake\n * @param {*} settings\n */\nconst runEditor = (settings, autoUpdateInterval = 0) => {\n editor = new CodeMirror(document.body, settings);\n editor.setSize('100%', '100%');\n\n initEventListeners(autoUpdateInterval);\n\n _initializeId = setInterval(() => dispatcher.dispatchEvent('initialize'), 500);\n};\n",
3 | "editor.html": "\n\n\n \n \n Code Editor\n \n \n\n\n\n\n",
4 | "messageport-dispatcher.min.js": "!function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?t(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],t):t((e=e||self).MessagePortDispatcher={})}(this,function(e){\"use strict\";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,\"default\")?e.default:e}function s(e,t){return e(t={exports:{}},t.exports),t.exports}var r=s(function(e,t){Object.defineProperty(t,\"__esModule\",{value:!0});const s=(e=>(t,s)=>Boolean(t&&e.call(t,s)))(Object.prototype.hasOwnProperty);t.hasOwn=s,t.default=s}),n=t(r),i=(r.hasOwn,s(function(e,t){Object.defineProperty(t,\"__esModule\",{value:!0});var s,n=(s=r)&&\"object\"==typeof s&&\"default\"in s?s.default:s;const i=e=>\"object\"==typeof e&&null!==e;class o{constructor(e,t=null){this.type=e,this.data=t,this.defaultPrevented=!1}toJSON(){return{type:this.type,data:this.data}}isDefaultPrevented(){return this.defaultPrevented}preventDefault(){this.defaultPrevented=!0}}const a=(e,t)=>{let s=e;return i(e)||(s=new o(String(e),t)),s};class h{constructor(e,t,s){this.index=-1,this.immediatelyStopped=!1,this.stopImmediatePropagation=()=>{this.immediatelyStopped=!0},this.listeners=e,this.onStopped=t,this.onComplete=s}run(e,t){let s;const{listeners:r}=this;for(this.augmentEvent(e),this.index=0;this.index{this._runners.splice(this._runners.indexOf(e),1)}}createList(e,t){const s=parseInt(t,10),r=this.getPrioritiesByKey(e),i=String(s);let o;return n(r,i)?o=r[i]:(o=[],r[i]=o),o}getPrioritiesByKey(e){let t;return n(this._listeners,e)?t=this._listeners[e]:(t={},this._listeners[e]=t),t}add(e,t,s){const r=this.createList(e,s);r.indexOf(t)<0&&r.push(t)}has(e){let t,s=!1;const r=this.getPrioritiesByKey(e);if(r)for(t in r)if(n(r,t)){s=!0;break}return s}remove(e,t){const s=this.getPrioritiesByKey(e);if(s){const e=Object.getOwnPropertyNames(s),{length:r}=e;for(let n=0;n=0&&(i.splice(o,1),i.length||delete s[r],this._runners.forEach(e=>{e.listenerRemoved(i,o)}))}}}removeAll(e){delete this._listeners[e]}createRunner(e,t){const s=new h(e,t,this.removeRunner);return this._runners.push(s),s}call(e,t){const s=this.getPrioritiesByKey(e.type);let r=!1;const n=()=>{r=!0};if(s){const i=Object.getOwnPropertyNames(s).sort((e,t)=>e-t),{length:o}=i;for(let a=0;anew c(e),t.getEvent=a,t.isObject=i}));t(i);i.Event,i.EventDispatcher;var o=i.createEventDispatcher,a=i.getEvent,h=i.isObject;const l=()=>`MP/${Math.ceil(1e4*Math.random())}/${Date.now()}`,c=e=>\"function\"==typeof e.toJSON?e.toJSON():JSON.stringify(e),d=e=>{if(h(e))return e;try{return JSON.parse(e)}catch(e){}};class p{constructor(e,t){this.event=e,this.dispatcherId=t}toJSON(){return{event:c(this.event),dispatcherId:this.dispatcherId}}}const u=e=>h(e)&&n(e,\"dispatcherId\")&&n(e,\"event\"),v=e=>{const t=d(e);if(t&&u(t)){const{event:e,dispatcherId:s}=t;return new p(d(e),s)}return null};class f{constructor(e=null,t=null,s=null,r=null){this.dispatcherId=l(),this.targetOrigin=\"*\",this.target=e||self,this.customPostMessageHandler=t,this.senderEventPreprocessor=r,this.sender=o(),this.receiver=o(s),this.target.addEventListener(\"message\",e=>this._postMessageListener(e))}addEventListener(e,t,s){this.receiver.addEventListener(e,t,s)}hasEventListener(e){return this.receiver.hasEventListener(e)}removeEventListener(e,t){this.receiver.removeEventListener(e,t)}removeAllEventListeners(e){this.receiver.removeAllEventListeners(e)}dispatchEvent(e,t,s){let r=a(e,t);this.senderEventPreprocessor&&(r=this.senderEventPreprocessor.call(this,r));const n=c(new p(r,this.dispatcherId));return this._postMessageHandler(n,s)}_postMessageHandler(e,t){const s=this.customPostMessageHandler;return s?s.call(this,e,this.targetOrigin,t):this.target.postMessage(e,this.targetOrigin,t)}_postMessageListener(e){e=e.nativeEvent||e;const t=v(e.data);t&&(t.dispatcherId===this.dispatcherId?this.sender.dispatchEvent(t.event):this.receiver.dispatchEvent(t.event))}}const g=(e,t,s,r)=>new f(e,t,s,r),m=(e,t=null)=>()=>(t||(t=g(e())),t),P=m(()=>self),E=m(()=>parent),y=m(()=>top);e.MessagePortDispatcher=f,e.MessagePortEvent=p,e.createMessagePortDispatcher=g,e.default=f,e.factory=m,e.getForParent=E,e.getForSelf=P,e.getForTop=y,e.isMessagePortEvent=u,e.parseMessagePortEvent=v,Object.defineProperty(e,\"__esModule\",{value:!0})});\n//# sourceMappingURL=messageport-dispatcher.min.js.map\n"
5 | }
--------------------------------------------------------------------------------