├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── new-version.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── __mocks__ └── styleMock.js ├── eslint.config.mjs ├── examples ├── app.css ├── app.tsx ├── components │ ├── Form.css │ └── Form.tsx ├── index.html └── webpack.config.ts ├── index.d.ts ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── JoditEditor.tsx ├── include.jodit.ts └── index.ts ├── tests ├── __snapshots__ │ └── smoke.test.tsx.snap ├── onchange.test.tsx ├── ref.test.tsx ├── smoke.test.tsx └── theme.test.tsx ├── tsconfig.json ├── tsconfig.types.json └── webpack.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = tab 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{LICENSE,.gitattributes,.gitignore,.npmignore,.eslintignore}] 12 | indent_style = space 13 | 14 | [*.{json,yml,md,yaspellerrc,bowerrc,babelrc,snakeskinrc,eslintrc,tsconfig,pzlrrc}] 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | **Jodit Version:** 3.4.xxxxx 5 | 6 | **Browser:** 7 | **OS:** 8 | **Is React App:** 9 | 10 | **Code** 11 | 12 | ```js 13 | // A *self-contained* demonstration of the problem follows... 14 | ``` 15 | 16 | **Expected behavior:** 17 | 18 | **Actual behavior:** 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | Fixes # 15 | -------------------------------------------------------------------------------- /.github/workflows/new-version.yml: -------------------------------------------------------------------------------- 1 | name: New version 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | repository_dispatch: 7 | types: [newversion] 8 | 9 | jobs: 10 | newversion: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token. 16 | fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. 17 | - uses: actions/setup-node@v4 #Setup Node 18 | with: 19 | node-version-file: '.nvmrc' 20 | cache: 'npm' 21 | - name: Create local changes 22 | run: | 23 | npm ci 24 | npm i jodit 25 | 26 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 27 | git config --local user.name "github-actions[bot]" 28 | 29 | npm version patch --no-git-tag-version 30 | npm run git 31 | 32 | - name: Push changes 33 | uses: ad-m/github-push-action@master 34 | with: 35 | github_token: ${{ secrets.GITHUB_TOKEN }} 36 | branch: ${{ github.ref }} 37 | tags: true 38 | 39 | - name: Trigger release action 40 | run: | 41 | curl -XPOST -u "${{ secrets.PAT_USERNAME}}:${{secrets.GITHUB_TOKEN}}" -H "Accept:application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/jodit/jodit-react/dispatches --data '{"event_type": "release" }' 42 | 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | repository_dispatch: 7 | types: [ release ] 8 | 9 | push: 10 | tags: ["*"] 11 | 12 | jobs: 13 | release: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 #Setup Node 20 | with: 21 | node-version-file: '.nvmrc' 22 | cache: 'npm' 23 | 24 | - name: Install dependencies 25 | run: | 26 | npm ci 27 | 28 | - name: Lint 29 | run: | 30 | npm run lint 31 | 32 | - name: Build 33 | run: | 34 | npm run build 35 | 36 | - name: Publish 37 | run: | 38 | NPM_TOKEN=${{ secrets.NPM_TOKEN }} npm publish ./ --access public 39 | 40 | - name: Get tag 41 | id: get_version 42 | run: | 43 | VERSION=$(node -p "require('./package.json').version") 44 | echo "VERSION=$VERSION" >> $GITHUB_ENV 45 | echo "version=$VERSION" >> $GITHUB_OUTPUT 46 | 47 | - name: Show new version 48 | run: | 49 | VERSION=${{ steps.get_version.outputs.version }} 50 | PACKAGE_NAME="jodit-react" 51 | echo "[New version npm](https://www.npmjs.com/package/${PACKAGE_NAME}/v/${VERSION})" >> $GITHUB_STEP_SUMMARY 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .idea/ 3 | /node_modules/ 4 | /examples/build/ 5 | build 6 | /coverage/ 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | eslint.config.mjs 3 | .editorconfig 4 | .gitignore 5 | .prettierrc.json 6 | tsconfig.types.json 7 | .github 8 | webpack.config.ts 9 | /coverage/ 10 | __mocks__ 11 | tests 12 | jest.config.ts 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} 2 | registry=https://registry.npmjs.org/ 3 | always-auth=true 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 4, 4 | "useTabs": true, 5 | "semi": true, 6 | "singleQuote": true, 7 | "endOfLine": "lf", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > **Tags:** 4 | > 5 | > - :boom: [Breaking Change] 6 | > - :rocket: [New Feature] 7 | > - :bug: [Bug Fix] 8 | > - :memo: [Documentation] 9 | > - :house: [Internal] 10 | > - :nail_care: [Polish] 11 | 12 | ## 5.0.9 13 | 14 | - Fixed ref forwarding issue 15 | 16 | ## 5.0.7 17 | 18 | - [Fix: Avoid "Abort async" error by utilizing waitForReady API in Jodit destruct handling #287](https://github.com/jodit/jodit-react/pull/287) 19 | - Fixed Config type issue 20 | - Support React 19 21 | 22 | 23 | ## 4.0.1 24 | 25 | - 26 | 27 | ## 1.3.19 28 | 29 | #### :rocket: New Feature 30 | 31 | - The package now re-exports imperative Jodit, so you can write plugins and use all Jodit helpers 32 | 33 | ```js 34 | import JoditEditor, { Jodit } from '../../src/'; 35 | 36 | /** 37 | * @param {Jodit} jodit 38 | */ 39 | function preparePaste(jodit) { 40 | jodit.e.on( 41 | 'paste', 42 | e => { 43 | if (confirm('Change pasted content?')) { 44 | jodit.e.stopPropagation('paste'); 45 | jodit.s.insertHTML( 46 | Jodit.modules.Helpers.getDataTransfer(e) 47 | .getData(Jodit.constants.TEXT_HTML) 48 | .replace(/a/g, 'b') 49 | ); 50 | return false; 51 | } 52 | }, 53 | { top: true } 54 | ); 55 | } 56 | Jodit.plugins.add('preparePaste', preparePaste); 57 | 58 | //... 59 | return ; 60 | ``` 61 | 62 | #### :house: Internal 63 | 64 | - Update 65 | 66 | ``` 67 | eslint-plugin-react-hooks ^4.5.0 → ^4.6.0 68 | @babel/core ^7.16.0 → ^7.19.0 69 | @babel/eslint-parser ^7.17.0 → ^7.18.9 70 | @babel/preset-env ^7.16.0 → ^7.19.0 71 | @babel/preset-react ^7.16.0 → ^7.18.6 72 | @types/react ^16.14.2 → ^18.0.18 73 | babel-loader ^8.2.2 → ^8.2.5 74 | css-loader ^3.6.0 → ^6.7.1 75 | eslint ^8.9.0 → ^8.23.0 76 | eslint-config-prettier ^8.4.0 → ^8.5.0 77 | eslint-plugin-prettier ^4.0.0 → ^4.2.1 78 | eslint-plugin-react ^7.28.0 → ^7.31.8 79 | husky ^7.0.4 → ^8.0.1 80 | lint-staged ^12.3.4 → ^13.0.3 81 | prettier ^2.5.1 → ^2.7.1 82 | style-loader ^0.20.3 → ^3.3.1 83 | webpack ^4.44.2 → ^5.74.0 84 | webpack-cli ^3.3.12 → ^4.10.0 85 | webpack-dev-server ^3.11.0 → ^4.11.0 86 | ``` 87 | 88 | ## 1.3.18 89 | 90 | #### :bug: Bug Fix 91 | 92 | - [Jodit not cleaning up after unmount #196](https://github.com/jodit/jodit-react/issues/196) 93 | 94 | ## 1.2.1 95 | 96 | #### :bug: Bug Fix 97 | 98 | - [Editor duplicates after re-render (state change) #172](https://github.com/jodit/jodit-react/issues/172) 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 It can be easy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Jodit WYSIWYG Editor 2 | 3 | [![npm](https://img.shields.io/npm/v/jodit-react.svg)](https://www.npmjs.com/package/jodit-react) 4 | [![npm](https://img.shields.io/npm/dm/jodit-react.svg)](https://www.npmjs.com/package/jodit-react) 5 | [![npm](https://img.shields.io/npm/l/jodit-react.svg)](https://www.npmjs.com/package/jodit-react) 6 | 7 | React wrapper for [Jodit](https://xdsoft.net/jodit/) 8 | 9 | > [Jodit React PRO](https://xdsoft.net/jodit/pro/) it is an extended version of Jodit React with the same API, but with a lot more features. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install jodit-react --save 15 | ``` 16 | 17 | ## Update editor version 18 | 19 | ```bash 20 | npm update jodit-react 21 | ``` 22 | 23 | ## Run demo 24 | 25 | ```bash 26 | npm install --dev 27 | npm run demo 28 | ``` 29 | 30 | and open 31 | 32 | ``` 33 | http://localhost:4000/ 34 | ``` 35 | 36 | ## Usage 37 | 38 | ### 1. Require and use Jodit-react component inside your application. 39 | 40 | ```jsx 41 | import React, { useState, useRef, useMemo } from 'react'; 42 | import JoditEditor from 'jodit-react'; 43 | 44 | const Example = ({ placeholder }) => { 45 | const editor = useRef(null); 46 | const [content, setContent] = useState(''); 47 | 48 | const config = useMemo(() => ({ 49 | readonly: false, // all options from https://xdsoft.net/jodit/docs/, 50 | placeholder: placeholder || 'Start typings...' 51 | }), 52 | [placeholder] 53 | ); 54 | 55 | return ( 56 | setContent(newContent)} // preferred to use only this option to update the content for performance reasons 62 | onChange={newContent => {}} 63 | /> 64 | ); 65 | }; 66 | ``` 67 | 68 | ### Jodit plugins 69 | 70 | You can use all Jodit features and write your [own plugin](https://xdsoft.net/jodit/docs/modules/plugin.html) for example. 71 | 72 | ```js 73 | import JoditEditor, { Jodit } from 'jodit-react'; 74 | 75 | /** 76 | * @param {Jodit} jodit 77 | */ 78 | function preparePaste(jodit) { 79 | jodit.e.on( 80 | 'paste', 81 | e => { 82 | if (confirm('Change pasted content?')) { 83 | jodit.e.stopPropagation('paste'); 84 | jodit.s.insertHTML( 85 | Jodit.modules.Helpers.getDataTransfer(e) 86 | .getData(Jodit.constants.TEXT_HTML) 87 | .replace(/a/g, 'b') 88 | ); 89 | return false; 90 | } 91 | }, 92 | { top: true } 93 | ); 94 | } 95 | Jodit.plugins.add('preparePaste', preparePaste); 96 | 97 | //... 98 | return ; 99 | ``` 100 | 101 | You can see how to write plugins [in the documentation](https://xdsoft.net/jodit/pro/docs/how-to/create-plugin.md) or [on the stand](https://xdsoft.net/jodit/pro/docs/getting-started/examples.md#jodit-example-paste-link) 102 | 103 | ## Use with Jodit PRO 104 | 105 | You can connect any Jodit constructor and set it as the `JoditConstructor` property of the component. 106 | 107 | ```jsx 108 | import React from 'react'; 109 | import JoditEditor from 'jodit-react'; 110 | import {Jodit} from 'jodit-pro'; 111 | import 'jodit-pro/es5/jodit.min.css'; 112 | // ... 113 | 114 | function App() { 115 | return ; 116 | } 117 | 118 | ``` 119 | 120 | 121 | ## License 122 | 123 | This package is available under `MIT` License. 124 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security issues 2 | 3 | If you believe you have found a security issue in the Jodit software, please contact us immediately. 4 | 5 | When reporting a suspected security problem, please bear this in mind: 6 | 7 | * Make sure to provide as many details as possible about the vulnerability. 8 | * Please do not disclose publicly any security issues until we fix them and publish security releases. 9 | 10 | Contact the security team at security@xdsoft.net. As soon as we receive the security report, we'll work promptly to confirm the issue and then to provide a security fix. 11 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = {}; 3 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules } from '@eslint/compat'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import js from '@eslint/js'; 5 | import { FlatCompat } from '@eslint/eslintrc'; 6 | import jest from 'eslint-plugin-jest'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all 14 | }); 15 | 16 | export default [ 17 | { 18 | ignores: ['node_modules/*', '**/build', 'types/*'] 19 | }, 20 | ...fixupConfigRules( 21 | compat.extends( 22 | 'eslint:recommended', 23 | 'plugin:react/recommended', 24 | 'plugin:prettier/recommended', 25 | 'plugin:react-hooks/recommended', 26 | 'plugin:@typescript-eslint/eslint-recommended', 27 | 'plugin:@typescript-eslint/recommended' 28 | ), 29 | jest.configs.recommended 30 | ), 31 | { 32 | settings: { 33 | react: { 34 | version: 'detect' 35 | } 36 | } 37 | } 38 | ]; 39 | -------------------------------------------------------------------------------- /examples/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 3 | } 4 | 5 | html, body { 6 | height: 100%; 7 | margin: 0; 8 | } 9 | 10 | body { 11 | font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; 12 | font-size: 16px; 13 | -webkit-font-smoothing: antialiased; 14 | text-rendering: optimizelegibility; 15 | line-height: 1.5; 16 | font-weight: 300; 17 | } 18 | 19 | body { 20 | display: flex; 21 | flex-direction: column; 22 | } 23 | 24 | header, 25 | #main_container { 26 | flex: 1 0 auto; 27 | } 28 | 29 | .jodit_wysiwyg { 30 | color: #000; 31 | padding: 10px; 32 | overflow-x: auto; 33 | min-height: 40px; 34 | } 35 | 36 | * { 37 | -webkit-box-sizing: border-box; 38 | -moz-box-sizing: border-box; 39 | box-sizing: border-box; 40 | } 41 | 42 | .table, .p { 43 | margin: 0 0 16px 0; 44 | } 45 | 46 | .table { 47 | border: 0; 48 | border-collapse: collapse; 49 | empty-cells: show; 50 | max-width: 100%; 51 | border-spacing: 0; 52 | *border-collapse: collapse; 53 | } 54 | 55 | . table tr { 56 | user-select: none; 57 | -o-user-select: none; 58 | -moz-user-select: none; 59 | -khtml-user-select: none; 60 | -webkit-user-select: none; 61 | -ms-user-select: none; 62 | } 63 | 64 | . table td, . table th { 65 | user-select: text; 66 | -o-user-select: text; 67 | -moz-user-select: text; 68 | -khtml-user-select: text; 69 | -webkit-user-select: text; 70 | -ms-user-select: text; 71 | } 72 | 73 | . table th, . table td { 74 | box-sizing: border-box; 75 | padding: 2px 5px; 76 | vertical-align: top; 77 | } 78 | 79 | .table td, . table th { 80 | border: 1px solid #ddd; 81 | } 82 | 83 | .container { 84 | width: 1000px; 85 | margin: 0 auto; 86 | } 87 | 88 | nav { 89 | height: 30px; 90 | background: linear-gradient(to left, #28a5f5, #1e87f0); 91 | padding: 0; 92 | overflow: hidden; 93 | } 94 | 95 | footer nav { 96 | background: #f9f9f9; 97 | margin-top: 20px; 98 | } 99 | 100 | nav > ul { 101 | padding: 0; 102 | margin: 0; 103 | } 104 | 105 | nav > ul > li { 106 | list-style: none; 107 | display: inline-block; 108 | padding: 0; 109 | margin: 0; 110 | } 111 | 112 | nav > ul > li + li { 113 | margin-left: 23px; 114 | } 115 | 116 | nav ul li a { 117 | color: #ddd; 118 | text-decoration: none; 119 | text-transform: uppercase; 120 | font-size: 0.625rem; 121 | line-height: 30px; 122 | } 123 | 124 | nav ul li a:hover { 125 | color: #aaa; 126 | text-decoration: underline; 127 | } 128 | 129 | footer nav ul li a { 130 | color: #3f3f3f; 131 | } 132 | 133 | nav ul li ul { 134 | position: absolute; 135 | margin: 0; 136 | padding: 0; 137 | background-color: #208bf1; 138 | display: none; 139 | } 140 | 141 | nav > ul > li:hover > ul { 142 | display: block; 143 | } 144 | 145 | nav ul li ul li { 146 | list-style: none; 147 | display: block; 148 | padding: 0; 149 | margin: 0; 150 | } 151 | 152 | nav ul li ul li a { 153 | padding: 5px; 154 | } 155 | 156 | .layout { 157 | display: flex; 158 | flex-direction: row; 159 | } 160 | 161 | .layout > * { 162 | 163 | } 164 | 165 | .leftside { 166 | width: 20%; 167 | padding: 10px 10px 10px 0; 168 | } 169 | 170 | .rightside { 171 | width: 80%; 172 | padding: 10px 0 10px 10px; 173 | 174 | } 175 | 176 | pre { 177 | white-space: pre-wrap; 178 | background-color: #3f3f3f; 179 | color: #fff; 180 | padding: 10px; 181 | } 182 | 183 | h1, h2, h3, h4, h5, h6 { 184 | font-weight: 500; 185 | } 186 | -------------------------------------------------------------------------------- /examples/app.tsx: -------------------------------------------------------------------------------- 1 | import './app.css'; 2 | 3 | import React, { StrictMode } from 'react'; 4 | import Form from './components/Form'; 5 | 6 | // For React < 18 7 | // import ReactDOM from 'react-dom'; 8 | // ReactDOM.render(
, document.getElementById('editor')); 9 | 10 | import { createRoot } from 'react-dom/client'; 11 | const container = document.getElementById('editor')!; 12 | const root = createRoot(container); 13 | root.render( 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /examples/components/Form.css: -------------------------------------------------------------------------------- 1 | .simple-textarea { 2 | display: block; 3 | width: 100%; 4 | min-height: 100px; 5 | } 6 | -------------------------------------------------------------------------------- /examples/components/Form.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ChangeEvent, useCallback, useState } from 'react'; 2 | 3 | import JoditEditor, { Jodit } from '../../src/'; 4 | import './Form.css'; 5 | import type { IJodit } from 'jodit/types/types/jodit'; 6 | 7 | /** 8 | * @param {Jodit} jodit 9 | */ 10 | function preparePaste(jodit: IJodit) { 11 | jodit.e.on( 12 | 'paste', 13 | e => { 14 | if (confirm('Change pasted content?')) { 15 | jodit.e.stopPropagation('paste'); 16 | jodit.s.insertHTML( 17 | Jodit.modules.Helpers.getDataTransfer(e)! 18 | .getData(Jodit.constants.TEXT_HTML) 19 | ?.replace(/a/g, 'b') ?? '' 20 | ); 21 | return false; 22 | } 23 | }, 24 | { top: true } 25 | ); 26 | } 27 | Jodit.plugins.add('preparePaste', preparePaste); 28 | 29 | const Form = () => { 30 | const [isSource, setSource] = useState(false); 31 | 32 | const [config, setConfig] = useState({ 33 | toolbarAdaptive: false, 34 | readonly: false, 35 | toolbar: true 36 | }); 37 | 38 | const [textAreaValue, setTextAreaValue] = useState('Test'); 39 | 40 | const [inputValue, setInputValue] = useState(''); 41 | 42 | const [spin, setSpin] = useState(1); 43 | 44 | const toggleToolbar = useCallback( 45 | () => 46 | setConfig(config => ({ 47 | ...config, 48 | toolbar: !config.toolbar 49 | })), 50 | [] 51 | ); 52 | 53 | const toggleReadOnly = useCallback( 54 | () => 55 | setConfig(config => ({ 56 | ...config, 57 | readonly: !config.readonly 58 | })), 59 | [] 60 | ); 61 | 62 | const handleBlurAreaChange = useCallback( 63 | (textAreaValue: string, event: MouseEvent) => { 64 | console.log('handleBlurAreaChange', textAreaValue, event); 65 | }, 66 | [] 67 | ); 68 | 69 | const handleWYSIWYGChange = useCallback((newTextAreaValue: string) => { 70 | console.log('handleWYSIWYGChange', newTextAreaValue); 71 | 72 | setTextAreaValue(newTextAreaValue); 73 | setInputValue(newTextAreaValue); 74 | 75 | return setTextAreaValue(() => newTextAreaValue); 76 | }, []); 77 | 78 | const handleNativeTextAreaChange = useCallback((e: ChangeEvent) => { 79 | const value = (e.target as HTMLTextAreaElement).value; 80 | console.log('handleNativeTextAreaChange', value); 81 | setTextAreaValue(value); 82 | setInputValue(value); 83 | }, []); 84 | 85 | const handleInputChange = useCallback( 86 | (e: ChangeEvent) => { 87 | const { value } = e.target as HTMLInputElement; 88 | setInputValue(value); 89 | handleWYSIWYGChange(value); 90 | }, 91 | [handleWYSIWYGChange] 92 | ); 93 | 94 | const handleSpin = useCallback(() => setSpin(spin => ++spin), []); 95 | 96 | const onSourceChange = useCallback((e: ChangeEvent) => { 97 | setSource((e.target as HTMLInputElement).checked); 98 | }, []); 99 | 100 | return ( 101 |
102 | 110 | 111 | {!isSource ? ( 112 | 118 | ) : ( 119 | 709 |
710 | 711 | `; 712 | 713 | exports[`Smoke Test should render without crashing 1`] = ` 714 | 715 |
718 |
723 |
726 |
729 | 734 |
737 |
740 | 747 | 773 | 774 | 781 | 807 | 808 | 815 | 841 | 842 | 849 | 875 | 876 | 883 | 909 | 910 |
911 |
914 | 921 | 948 | 952 | 956 | 957 | 960 | 961 | 962 | 963 | 964 | 971 | 998 | 1002 | 1006 | 1007 | 1010 | 1011 | 1012 | 1013 | 1014 |
1015 |
1018 |
1021 |
1024 | 1031 | 1057 | 1061 | 1065 | 1066 | 1069 | 1070 | 1071 | 1072 | 1073 | 1080 | 1106 | 1110 | 1114 | 1115 | 1118 | 1119 | 1120 | 1121 | 1122 | 1129 | 1153 | 1157 | 1161 | 1162 | 1165 | 1166 | 1167 | 1168 | 1169 | 1176 | 1218 | 1222 | 1226 | 1227 | 1230 | 1231 | 1232 | 1233 | 1234 |
1235 |
1238 | 1245 | 1271 | 1275 | 1279 | 1280 | 1283 | 1284 | 1285 | 1286 | 1287 |
1288 |
1291 |
1294 | 1301 | 1341 | 1342 |
1343 |
1344 |
1345 |
1346 |
1352 |
1360 |

1361 | Hello, world! 1362 |

1363 |
1364 |
1367 | 1419 |
1420 | 1421 | `; 1422 | -------------------------------------------------------------------------------- /tests/onchange.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { describe, it } from '@jest/globals'; 3 | import { render } from '@testing-library/react'; 4 | import JoditEditor from '../src'; 5 | 6 | describe('On change Test', () => { 7 | describe('On init', () => { 8 | it('should not call handler', () => { 9 | const onChange = jest.fn(); 10 | const value = '

Hello, World!

'; 11 | render(); 12 | expect(onChange).toHaveBeenCalledTimes(0); 13 | }); 14 | }); 15 | 16 | describe('On change text', () => { 17 | it('should call handler every time', async () => { 18 | const onChange = jest.fn(); 19 | let value = '

Hello, World!

'; 20 | const stamp = render( 21 | 22 | ); 23 | value = '

Hello!

'; 24 | stamp.rerender(); 25 | expect(onChange).toHaveBeenCalledTimes(1); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/ref.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { describe, it } from '@jest/globals'; 3 | import { render, act } from '@testing-library/react'; 4 | import '@testing-library/jest-dom'; 5 | import JoditEditor from '../src'; 6 | import type { IJodit } from 'jodit/types/types/jodit'; 7 | 8 | describe('Ref Test', () => { 9 | describe('Ref as function', () => { 10 | it('should be instance of imperative Jodit', () => { 11 | const ref = React.createRef(); 12 | 13 | const App = () => ( 14 | { 16 | ref.current = newRef; 17 | }} 18 | /> 19 | ); 20 | 21 | const elm = render(); 22 | 23 | act(() => { 24 | elm.rerender(); 25 | }); 26 | 27 | expect(ref.current?.isJodit).toBe(true); 28 | }); 29 | }); 30 | 31 | describe('Ref as object', () => { 32 | it('should be instance of imperative Jodit', () => { 33 | const ref = React.createRef(); 34 | 35 | const App = () => ; 36 | 37 | const elm = render(); 38 | 39 | act(() => { 40 | elm.rerender(); 41 | }); 42 | 43 | expect(ref.current?.isJodit).toBe(true); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/smoke.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { describe, it } from '@jest/globals'; 3 | import { render } from '@testing-library/react'; 4 | import '@testing-library/jest-dom'; 5 | import JoditEditor from '../src'; 6 | 7 | describe('Smoke Test', () => { 8 | it('should render without crashing', () => { 9 | const { asFragment, getByText } = render( 10 | Hello, world!

'} /> 11 | ); 12 | expect(getByText('Hello, world!')).toBeInTheDocument(); 13 | expect(asFragment()).toMatchSnapshot(); 14 | }); 15 | 16 | describe('Config', () => { 17 | it('should render without crashing', () => { 18 | const config = { 19 | readonly: true, 20 | sourceEditor: 'ace', 21 | disabled: true 22 | }; 23 | 24 | const { asFragment, getByText } = render( 25 | Hello, world!

'} config={config} /> 26 | ); 27 | expect(getByText('Hello, world!')).toBeInTheDocument(); 28 | expect(asFragment()).toMatchSnapshot(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/theme.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { describe, it } from '@jest/globals'; 3 | import { render } from '@testing-library/react'; 4 | import '@testing-library/jest-dom'; 5 | import JoditEditor from '../src'; 6 | 7 | describe('Theme Test', () => { 8 | it('should render with theme classname', () => { 9 | const { container } = render( 10 | 11 | ); 12 | expect( 13 | container 14 | .querySelector('.jodit-container')! 15 | .classList.contains('jodit_theme_summer') 16 | ).toBe(true); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "ESNext", 5 | "outDir": "./build", 6 | "declaration": true, 7 | "declarationDir": "./build/types", 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "moduleDetection": "force", 12 | "allowJs": true, 13 | "noUncheckedIndexedAccess": true, 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "jsx": "react-jsx", 17 | "moduleResolution": "node", 18 | "noImplicitAny": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["./tsconfig.json"], 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "jsx": "react", 6 | "declaration": true, 7 | "outDir": "./build/types", 8 | "declarationDir": "./build/types", 9 | "module": "ES2022", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true 12 | }, 13 | "files": ["./src/index"] 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import process from 'process'; 4 | 5 | export default (env: unknown, argv: { mode?: string }, dir = process.cwd()) => { 6 | const debug = !argv || !argv.mode || !argv.mode.match(/production/); 7 | 8 | return { 9 | context: dir, 10 | 11 | entry: './src/index.ts', 12 | devtool: debug ? 'inline-source-map' : false, 13 | 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(js|jsx|ts|tsx)$/, 18 | use: { 19 | loader: 'swc-loader' 20 | } 21 | }, 22 | { 23 | test: /\.css$/, 24 | use: ['style-loader', 'css-loader'] 25 | } 26 | ] 27 | }, 28 | 29 | resolve: { 30 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 31 | alias: { 32 | 'jodit-react': path.join(__dirname, './src') 33 | } 34 | }, 35 | 36 | output: { 37 | path: path.join(dir, './build/'), 38 | filename: 'jodit-react.js', 39 | library: ['JoditEditor', 'Jodit'], 40 | libraryTarget: 'umd' 41 | }, 42 | 43 | plugins: [ 44 | new webpack.DefinePlugin({ 45 | 'process.env': { 46 | NODE_ENV: JSON.stringify( 47 | debug ? 'development' : 'production' 48 | ) 49 | } 50 | }), 51 | new webpack.optimize.ModuleConcatenationPlugin() 52 | ], 53 | 54 | externals: { 55 | jodit: 'jodit', 56 | Jodit: 'Jodit', 57 | react: { 58 | root: 'React', 59 | commonjs2: 'react', 60 | commonjs: 'react', 61 | amd: 'react' 62 | }, 63 | 'react-dom': { 64 | root: 'ReactDOM', 65 | commonjs2: 'react-dom', 66 | commonjs: 'react-dom', 67 | amd: 'react-dom' 68 | } 69 | } 70 | }; 71 | }; 72 | --------------------------------------------------------------------------------