├── .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 | } --------------------------------------------------------------------------------