├── .nvmrc ├── .watchmanconfig ├── example ├── App.js ├── assets │ ├── icon.png │ ├── favicon.png │ ├── splash.png │ └── adaptive-icon.png ├── tsconfig.json ├── babel.config.js ├── app.json ├── package.json ├── webpack.config.js ├── metro.config.js └── src │ └── App.tsx ├── src ├── __tests__ │ └── index.test.tsx ├── html │ ├── index.ts │ ├── scripts │ │ ├── editorBundleString.d.ts │ │ ├── extensions │ │ │ ├── index.ts │ │ │ ├── TTS.ts │ │ │ ├── Cloze.ts │ │ │ └── CustomCodeBlock.ts │ │ ├── utils.ts │ │ ├── RNBridge.ts │ │ └── RNEditor.ts │ └── generateHTMLTemplate.ts ├── index.ts ├── components │ ├── RichTextEditor.styles.ts │ ├── RichTextToolbar.tsx │ └── RichTextEditor.tsx ├── index.d.ts ├── minimize │ ├── index.js │ └── needToBundle.js └── types.ts ├── .gitattributes ├── tsconfig.build.json ├── babel.config.js ├── .yarnrc.yml ├── .yarnrc ├── .editorconfig ├── lefthook.yml ├── README.md ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── tsconfig.json ├── scripts └── bootstrap.js ├── LICENSE ├── .gitignore ├── CONTRIBUTING.md ├── package.json ├── CODE_OF_CONDUCT.md └── .yarn └── plugins └── @yarnpkg └── plugin-workspace-tools.cjs /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.18.1 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | export { default } from './src/App'; 2 | -------------------------------------------------------------------------------- /src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | it.todo('write a test'); 2 | -------------------------------------------------------------------------------- /src/html/index.ts: -------------------------------------------------------------------------------- 1 | export { generateHTMLTemplate } from './generateHTMLTemplate'; 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /src/html/scripts/editorBundleString.d.ts: -------------------------------------------------------------------------------- 1 | declare const core: string; 2 | 3 | export { core }; 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": ["example"] 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NojiApp/react-native-rich-text/HEAD/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NojiApp/react-native-rich-text/HEAD/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NojiApp/react-native-rich-text/HEAD/example/assets/splash.png -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | nmHoistingLimits: workspaces 3 | 4 | yarnPath: .yarn/releases/yarn-3.6.1.cjs 5 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NojiApp/react-native-rich-text/HEAD/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | // Avoid expo-cli auto-generating a tsconfig 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { RichTextEditor } from './components/RichTextEditor'; 2 | export { RichTextToolbar } from './components/RichTextToolbar'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/html/scripts/extensions/index.ts: -------------------------------------------------------------------------------- 1 | import { Cloze } from './Cloze'; 2 | import { TTS } from './TTS'; 3 | import { CustomCodeBlock } from './CustomCodeBlock'; 4 | 5 | export const extensions = ` 6 | ${CustomCodeBlock} 7 | ${Cloze} 8 | ${TTS} 9 | `; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | files: git diff --name-only @{push} 6 | glob: "*.{js,ts,jsx,tsx}" 7 | run: npx eslint {files} 8 | types: 9 | files: git diff --name-only @{push} 10 | glob: "*.{js,ts, jsx, tsx}" 11 | run: npx tsc --noEmit 12 | commit-msg: 13 | parallel: true 14 | commands: 15 | commitlint: 16 | run: npx commitlint --edit 17 | -------------------------------------------------------------------------------- /src/components/RichTextEditor.styles.ts: -------------------------------------------------------------------------------- 1 | import { Platform, StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | webView: { 5 | flex: 0, 6 | height: '100%', 7 | backgroundColor: 'transparent', 8 | // resolving old issue with react-native-webview on Android 9 | opacity: Platform.OS === 'android' ? 0.99 : 1, 10 | }, 11 | hiddenInput: { 12 | position: 'absolute', 13 | zIndex: -999, 14 | left: -999, 15 | bottom: -999, 16 | width: 1, 17 | height: 1, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = function (api) { 5 | api.cache(true); 6 | 7 | return { 8 | presets: ['babel-preset-expo'], 9 | plugins: [ 10 | [ 11 | 'module-resolver', 12 | { 13 | extensions: ['.tsx', '.ts', '.js', '.json'], 14 | alias: { 15 | // For development, we want to alias the library to the source 16 | [pak.name]: path.join(__dirname, '..', pak.source), 17 | }, 18 | }, 19 | ], 20 | ], 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ForwardRefExoticComponent, 3 | PropsWithoutRef, 4 | RefAttributes, 5 | } from 'react'; 6 | 7 | import { 8 | RefRichTextEditor, 9 | RefRichTextToolbar, 10 | RichTextEditorProps, 11 | RichTextToolbarProps, 12 | } from './types'; 13 | 14 | declare const RichTextEditor: ForwardRefExoticComponent< 15 | PropsWithoutRef & RefAttributes 16 | >; 17 | declare const RichTextToolbar: ForwardRefExoticComponent< 18 | PropsWithoutRef & RefAttributes 19 | >; 20 | 21 | export { RichTextEditor, RichTextToolbar }; 22 | export * from './types'; 23 | -------------------------------------------------------------------------------- /src/html/scripts/utils.ts: -------------------------------------------------------------------------------- 1 | export const utils = ` 2 | function throttle(callback, delay = 1000) { 3 | let shouldWait = false; 4 | return (...args) => { 5 | if (shouldWait) return; 6 | callback(...args); 7 | shouldWait = true; 8 | setTimeout(() => { 9 | shouldWait = false; 10 | }, delay); 11 | }; 12 | } 13 | 14 | function debounce(callback, delay = 1000) { 15 | let time; 16 | return (...args) => { 17 | clearTimeout(time); 18 | time = setTimeout(() => { 19 | callback(...args); 20 | }, delay); 21 | }; 22 | } 23 | 24 | function shallowEqual(object1, object2) { 25 | return JSON.stringify(object1) === JSON.stringify(object2) 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-rich-text 2 | 3 | React Native Rich Text editor based on TipTap library 4 | 5 | Made with 💙 by [Anki Pro](https://ankipro.net/) team 6 | 7 | ## Installation 8 | 9 | ```sh 10 | yarn add @ankipro/react-native-rich-text react-native-webview 11 | ``` 12 | 13 | ## Usage 14 | 15 | For more examples [check out the Example app](https://github.com/AnkiPro/react-native-rich-text/tree/main/example) 16 | 17 | ## Contributing 18 | 19 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 20 | 21 | ## License 22 | 23 | MIT 24 | 25 | --- 26 | 27 | Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) 28 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v3 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v3 15 | with: 16 | path: | 17 | **/node_modules 18 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 19 | restore-keys: | 20 | ${{ runner.os }}-yarn- 21 | 22 | - name: Install dependencies 23 | if: steps.yarn-cache.outputs.cache-hit != 'true' 24 | run: | 25 | yarn install 26 | shell: bash 27 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "expo": "~48.0.15", 13 | "expo-status-bar": "~1.4.4", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0", 16 | "react-native": "0.74.1", 17 | "react-native-web": "0.18.10", 18 | "react-native-webview": "13.8.7" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.20.0", 22 | "@expo/webpack-config": "^0.17.2", 23 | "babel-loader": "^8.1.0", 24 | "babel-plugin-module-resolver": "^4.1.0" 25 | }, 26 | "private": true 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@ankipro/react-native-rich-text": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noStrictGenericChecks": false, 19 | "noUncheckedIndexedAccess": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/html/scripts/extensions/TTS.ts: -------------------------------------------------------------------------------- 1 | export const TTS = ` 2 | const TTS = Mark.create({ 3 | name: 'tts', 4 | excludes: '_', 5 | 6 | addOptions() { 7 | return { 8 | HTMLAttributes: {}, 9 | }; 10 | }, 11 | 12 | parseHTML() { 13 | return [ 14 | { 15 | tag: 'tts', 16 | }, 17 | ]; 18 | }, 19 | 20 | renderHTML({ HTMLAttributes }) { 21 | return ['tts', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; 22 | }, 23 | 24 | addCommands() { 25 | return { 26 | setTTS: 27 | () => 28 | ({ commands }) => 29 | commands.setMark(this.name), 30 | toggleTTS: 31 | () => 32 | ({ commands }) => 33 | commands.toggleMark(this.name), 34 | unsetTTS: 35 | () => 36 | ({ commands }) => 37 | commands.unsetMark(this.name), 38 | }; 39 | }, 40 | }); 41 | `; 42 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const child_process = require('child_process'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const args = process.argv.slice(2); 7 | const options = { 8 | cwd: process.cwd(), 9 | env: process.env, 10 | stdio: 'inherit', 11 | encoding: 'utf-8', 12 | }; 13 | 14 | if (os.type() === 'Windows_NT') { 15 | options.shell = true; 16 | } 17 | 18 | let result; 19 | 20 | if (process.cwd() !== root || args.length) { 21 | // We're not in the root of the project, or additional arguments were passed 22 | // In this case, forward the command to `yarn` 23 | result = child_process.spawnSync('yarn', args, options); 24 | } else { 25 | // If `yarn` is run without arguments, perform bootstrap 26 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 27 | } 28 | 29 | process.exitCode = result.status; 30 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 3 | const { resolver } = require('./metro.config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const node_modules = path.join(__dirname, 'node_modules'); 7 | 8 | module.exports = async function (env, argv) { 9 | const config = await createExpoWebpackConfigAsync(env, argv); 10 | 11 | config.module.rules.push({ 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | include: path.resolve(root, 'src'), 14 | use: 'babel-loader', 15 | }); 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we alias them to the versions in example's node_modules 19 | Object.assign(config.resolve.alias, { 20 | ...resolver.extraNodeModules, 21 | 'react-native-web': path.join(node_modules, 'react-native-web'), 22 | }); 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup 18 | uses: ./.github/actions/setup 19 | 20 | - name: Lint files 21 | run: yarn lint 22 | 23 | - name: Typecheck files 24 | run: yarn typecheck 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup 33 | uses: ./.github/actions/setup 34 | 35 | - name: Run unit tests 36 | run: yarn test --maxWorkers=2 --coverage 37 | 38 | build: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | 44 | - name: Setup 45 | uses: ./.github/actions/setup 46 | 47 | - name: Build package 48 | run: yarn prepack 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 @ankipro 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Yarn 64 | .yarn/* 65 | !.yarn/patches 66 | !.yarn/plugins 67 | !.yarn/releases 68 | !.yarn/sdks 69 | !.yarn/versions 70 | 71 | # Expo 72 | .expo/ 73 | 74 | # Turborepo 75 | .turbo/ 76 | 77 | # generated by bob 78 | lib/ 79 | 80 | # editor bundle 81 | src/html/scripts/editorBundleString.js 82 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const escape = require('escape-string-regexp'); 3 | const { getDefaultConfig } = require('@expo/metro-config'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }); 12 | 13 | const defaultConfig = getDefaultConfig(__dirname); 14 | 15 | module.exports = { 16 | ...defaultConfig, 17 | 18 | projectRoot: __dirname, 19 | watchFolders: [root], 20 | 21 | // We need to make sure that only one version is loaded for peerDependencies 22 | // So we block them at the root, and alias them to the versions in example's node_modules 23 | resolver: { 24 | ...defaultConfig.resolver, 25 | 26 | blacklistRE: exclusionList( 27 | modules.map( 28 | (m) => 29 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 30 | ) 31 | ), 32 | 33 | extraNodeModules: modules.reduce((acc, name) => { 34 | acc[name] = path.join(__dirname, 'node_modules', name); 35 | return acc; 36 | }, {}), 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/html/scripts/RNBridge.ts: -------------------------------------------------------------------------------- 1 | export const RNBridge = ` 2 | class RNBridge { 3 | static IS_RN = !!window.ReactNativeWebView; 4 | static messageType = { 5 | MESSAGE: 'MESSAGE', 6 | CONSOLE: 'CONSOLE', 7 | EVENT: 'EVENT' 8 | } 9 | 10 | static send(data) { 11 | if (this.IS_RN) { 12 | window.ReactNativeWebView.postMessage(JSON.stringify(data)); 13 | } 14 | } 15 | 16 | static console(data) { 17 | this.send({ type: RNBridge.messageType.CONSOLE, data}); 18 | } 19 | 20 | static message(data) { 21 | this.send({ type: RNBridge.messageType.MESSAGE, data}); 22 | } 23 | 24 | static event(event, data) { 25 | this.send({ type: RNBridge.messageType.EVENT, event, data}); 26 | } 27 | 28 | static initListener() { 29 | function handleMessage(event) { 30 | const { actionType, formatType, eventType, data, options } = JSON.parse(event.data); 31 | if (actionType === 'FORMAT') { 32 | RNEditor.applyAction(formatType, options); 33 | } 34 | if (actionType === 'UNFORMAT') { 35 | RNEditor.cancelAction(formatType, options); 36 | } 37 | if (actionType === 'EVENT') { 38 | if (eventType === 'focus') { 39 | RNEditor.instance.commands.focus('end'); 40 | } 41 | if (eventType === 'blur') { 42 | RNEditor.instance.commands.blur(); 43 | } 44 | if (eventType === 'setContent') { 45 | RNEditor.instance.commands.setContent(data); 46 | } 47 | } 48 | if (actionType === 'MESSAGE') { 49 | RNBridge.message({state: RNEditor.prevState}); 50 | } 51 | } 52 | 53 | // for iOS 54 | window.addEventListener('message', handleMessage, false); 55 | // for Android 56 | document.addEventListener('message', handleMessage, false); 57 | } 58 | } 59 | `; 60 | -------------------------------------------------------------------------------- /src/minimize/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { rollup } = require('rollup'); 4 | const terser = require('@rollup/plugin-terser'); 5 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 6 | const commonjs = require('@rollup/plugin-commonjs'); 7 | 8 | (async function () { 9 | console.log('@ankipro/react-native-rich-text: making bundle...'); 10 | 11 | // create a Rollup bundle 12 | const bundle = await rollup({ 13 | input: path.resolve(`${__dirname}/needToBundle.js`), 14 | plugins: [ 15 | commonjs(), 16 | nodeResolve(), 17 | // minify the output using the `terser` plugin 18 | terser({ 19 | format: { 20 | comments: false, 21 | quote_style: 1, 22 | }, 23 | }), 24 | ], 25 | }); 26 | 27 | // write the output bundle to a file 28 | const outputTempPath = path.resolve(`${__dirname}/bundle.temp.txt`); 29 | await bundle.write({ 30 | file: outputTempPath, 31 | format: 'iife', 32 | name: 'window', 33 | extend: true, 34 | }); 35 | 36 | let bundleString = fs.readFileSync(outputTempPath, 'utf8'); 37 | // remove output temp file 38 | fs.unlinkSync(outputTempPath); 39 | 40 | // mirror all backslashes 41 | bundleString = bundleString.replace(/\\/g, '\\\\'); 42 | // mirror all backtick quotes and template literals 43 | bundleString = bundleString.replace(/`/g, '\\`'); 44 | bundleString = bundleString.replace(/(\${)/g, '\\${'); 45 | // wrap with js constant variable 46 | bundleString = `export const core = \`\n${bundleString}\`;\n`; 47 | 48 | fs.writeFile( 49 | path.resolve(`${__dirname}/../html/scripts/editorBundleString.js`), 50 | bundleString, 51 | (err) => { 52 | if (err) console.log('@ankipro/react-native-rich-text ERROR: ', err); 53 | else { 54 | console.log( 55 | '@ankipro/react-native-rich-text: bundle was successfully built!' 56 | ); 57 | } 58 | } 59 | ); 60 | })(); 61 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unstable-nested-components */ 2 | import React, { useRef } from 'react'; 3 | 4 | import { StyleSheet, View, Button } from 'react-native'; 5 | import { 6 | FormatType, 7 | RefRichTextEditor, 8 | RefRichTextToolbar, 9 | RichTextEditor, 10 | RichTextToolbar, 11 | RichTextToolbarChildrenArgs, 12 | } from '@ankipro/react-native-rich-text'; 13 | 14 | const ACTIONS = [FormatType.bold, FormatType.italic, FormatType.underline]; 15 | 16 | export default function App() { 17 | const editorRef = useRef(null); 18 | const toolbarRef = useRef(null); 19 | 20 | const renderAction = ({ 21 | state, 22 | handleFormatPress, 23 | }: RichTextToolbarChildrenArgs) => (action: FormatType) => { 24 | const isActive = !!state?.[action]; 25 | const handlePress = handleFormatPress(action); 26 | 27 | return ( 28 |