├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md ├── style.yml └── workflows │ ├── nodejs.yml │ └── size-limit.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .storybook ├── main.js └── preview.js ├── .travis.yml ├── .vscode └── settings.json ├── @types └── vendor.d.ts ├── CHANGELOG.md ├── LICENCE ├── README.md ├── cypress.json ├── cypress ├── fixtures │ └── data.ts ├── integration │ └── index.test.ts ├── plugins │ ├── cy-ts-preprocessor.js │ └── index.js ├── support │ ├── commands.ts │ └── index.ts └── tsconfig.json ├── index.d.ts ├── jest.config.js ├── jest.setup.ts ├── package.json ├── prettier.config.js ├── src ├── __stories__ │ ├── config.ts │ ├── custom-plugin-js.tsx │ ├── custom-plugin-react.tsx │ └── index.stories.tsx ├── __tests__ │ └── index.test.tsx ├── editor.tsx └── index.tsx ├── tsconfig.json ├── tsconfig.test.json ├── webpack.config.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-editor-js", 3 | "projectOwner": "natterstefan", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "eslint", 12 | "contributors": [ 13 | { 14 | "login": "natterstefan", 15 | "name": "Stefan Natter", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/1043668?v=4", 17 | "profile": "http://twitter.com/natterstefan", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "example" 22 | ] 23 | } 24 | ], 25 | "contributorsPerLine": 7 26 | } 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | es 3 | esm 4 | lib 5 | tmp 6 | src/@types -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint-config-ns-ts'], 3 | rules: { 4 | 'class-methods-use-this': 0, 5 | 'import/extensions': 0, 6 | 'sort-keys': 0, 7 | 'react/prop-types': 0, 8 | '@typescript-eslint/interface-name-prefix': [ 9 | 2, 10 | { 11 | prefixWithI: 'always', 12 | allowUnderscorePrefix: true, 13 | }, 14 | ], 15 | '@typescript-eslint/no-use-before-define': 0, 16 | }, 17 | overrides: [ 18 | { 19 | files: [ 20 | 'jest.setup.ts', 21 | '*.test.ts', 22 | '*.test.tsx', 23 | '.storybook', 24 | '**/__stories__/**/*.ts', 25 | '**/__stories__/**/*.tsx', 26 | 'cypress/**/*.js', 27 | 'cypress/**/*.ts', 28 | ], 29 | rules: { 30 | 'import/no-extraneous-dependencies': 0, 31 | '@typescript-eslint/explicit-function-return-type': 0, 32 | '@typescript-eslint/no-explicit-any': 0, 33 | '@typescript-eslint/no-var-requires': 0, 34 | }, 35 | }, 36 | { 37 | files: ['@types/**/*.ts'], 38 | rules: { 39 | '@typescript-eslint/no-explicit-any': 0, 40 | }, 41 | }, 42 | { 43 | files: ['cypress/**/*.test.ts'], 44 | globals: { 45 | cy: true, // cypress 46 | }, 47 | rules: { 48 | // because jest is not used in cypress test files 49 | 'jest/valid-expect': 0, 50 | 'jest/expect-expect': 0, 51 | }, 52 | }, 53 | ], 54 | settings: { 55 | 'import/resolver': { 56 | node: { 57 | extensions: ['.js', '.jsx', '.ts', '.tsx', 'd.ts'], 58 | }, 59 | }, 60 | react: { 61 | version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use 62 | }, 63 | }, 64 | } 65 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code ownership for pull request reviews 2 | 3 | # Docs 4 | # https://github.com/blog/2392-introducing-code-owners 5 | # https://help.github.com/articles/about-codeowners/ 6 | 7 | # These owners will be the default owners for everything in the repo. 8 | * @natterstefan 9 | 10 | # Order is important. The last matching pattern has the most precedence. 11 | # So if a pull request only touches javascript files, only these owners 12 | # will be requested to review. 13 | 14 | # For example: 15 | # *.js @octocat @github/js 16 | 17 | # You can also use email addresses if you prefer. 18 | # docs/* docs@example.com -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 🐞 3 | about: Something isn't working as expected? Here is the right place to report. 4 | --- 5 | 6 | 13 | 14 | # Bug Report 15 | 16 | ## Relevant information 17 | 18 | 19 | 20 | ### Your Environment 21 | 22 | * Browser: **\_** 23 | * Browser version: **\_** 24 | * OS: **\_** 25 | * Node: **x.y.z** 26 | * Package Version: **x.y.z** 27 | 28 | #### Steps to reproduce 29 | 30 | 1. Step 1 31 | 2. Step 2 32 | 3. Step 3 33 | 34 | #### Observed Results 35 | 36 | * What happened? This could be a description, log output, etc. 37 | 38 | #### Expected Results 39 | 40 | * What did you expect to happen? 41 | 42 | #### Relevant Code (optional) 43 | 44 | ```js 45 | // TODO(you): code here to reproduce the problem 46 | ``` 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 💡 3 | about: Suggest a new idea. 4 | --- 5 | 6 | 13 | 14 | # Feature Request 15 | 16 | Brief explanation of the feature you have in mind. 17 | 18 | ## Basic example 19 | 20 | If you want you can include a basic code example. Omit this section if it's 21 | not applicable. 22 | 23 | ## Motivation 24 | 25 | Why are you suggesting this? What is the use case for it and what is the 26 | expected outcome? 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 🤔 3 | about: Usage question or discussion. 4 | --- 5 | 6 | 13 | 14 | # Question 15 | 16 | ## Relevant information 17 | 18 | Provide as much useful information as you can. 19 | 20 | ### Your Environment 21 | 22 | * Browser: **\_** 23 | * Browser version: **\_** 24 | * OS: **\_** 25 | * Node: **x.y.z** 26 | * Package Version: **x.y.z** 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Issue 7 | 8 | ## What I did 9 | 10 | 14 | 15 | ## What it adds, solves and/or improves 16 | 17 | 20 | 21 | ## How to test 22 | 23 | 31 | -------------------------------------------------------------------------------- /.github/style.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - bug 10 | - help wanted 11 | 12 | exemptMilestones: true 13 | 14 | # Label to use when marking an issue as stale 15 | staleLabel: stale 16 | 17 | only: issues 18 | 19 | # Comment to post when removing the stale label. 20 | unmarkComment: > 21 | Okay, it looks like this issue or feature request might still be important. 22 | We'll re-open it for now. Thank you for letting us know! 23 | 24 | # Comment to post when marking an issue as stale. Set to `false` to disable 25 | markComment: > 26 | Is this still relevant? We haven't heard from anyone in a bit. If so, 27 | please comment with any updates or additional detail. 28 | 29 | This issue has been automatically marked as stale because it has not had 30 | recent activity. It will be closed if no further activity occurs. Don't 31 | take it personally, we just need to keep a handle on things. Thank you 32 | for your contributions! 33 | 34 | # Comment to post when closing a stale issue. Set to `false` to disable 35 | closeComment: > 36 | This issue has been automatically closed because it has not had 37 | recent activity. If you believe this is still an issue, please confirm that 38 | this issue is still happening in the most recent version and reply to this 39 | thread to re-open it. -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - run: yarn --silent 29 | 30 | - run: yarn test 31 | 32 | - run: yarn build 33 | env: 34 | CI: true 35 | -------------------------------------------------------------------------------- /.github/workflows/size-limit.yml: -------------------------------------------------------------------------------- 1 | name: 'size-limit' 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | size: 10 | runs-on: ubuntu-latest 11 | env: 12 | CI_JOB_NUMBER: 1 13 | steps: 14 | - uses: actions/checkout@v1 15 | 16 | - name: Use Node.js 12.x 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12.x 20 | 21 | - run: yarn --silent 22 | 23 | - uses: andresz1/size-limit-action@v1.2.2 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS, IDE, ... 2 | .DS_Store 3 | 4 | # Logs and Testing 5 | cypress/screenshots 6 | cypress/videos 7 | 8 | # Development 9 | .awcache 10 | node_modules 11 | tmp 12 | *.log 13 | 14 | # build 15 | .awcache 16 | build 17 | dist 18 | es 19 | esm 20 | lib 21 | *.tgz 22 | storybook-static 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.16.0 -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | /** 4 | * Docs 5 | * @see https://storybook.js.org/docs/configurations 6 | */ 7 | module.exports = { 8 | stories: ['../src/**/__stories__/*.stories.(tsx|mdx)'], 9 | addons: [ 10 | { 11 | name: '@storybook/preset-typescript', 12 | options: { 13 | include: [path.resolve(__dirname, '../src')], 14 | }, 15 | }, 16 | '@storybook/addon-actions', 17 | 'storybook-readme', 18 | '@storybook/addon-knobs/register', 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { addDecorator, addParameters } from '@storybook/react' 2 | import { addReadme } from 'storybook-readme' 3 | import { create } from '@storybook/theming/create' 4 | 5 | addDecorator(addReadme) 6 | 7 | addParameters({ 8 | options: { 9 | theme: create({ 10 | base: 'light', 11 | }), 12 | isFullscreen: false, 13 | panelPosition: 'right', 14 | sortStoriesByKind: true, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # inspired by 2 | # - https://docs.cypress.io/guides/guides/continuous-integration.html#Travis 3 | # - https://docs.cypress.io/guides/guides/continuous-integration.html#Caching 4 | language: node_js 5 | 6 | node_js: 7 | - 'node' 8 | 9 | before_install: 10 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s 11 | - export PATH="$HOME/.yarn/bin:$PATH" 12 | 13 | addons: 14 | apt: 15 | packages: 16 | # Ubuntu 16+ does not install this dependency by default, so we need to 17 | # install it ourselves. It is required for tests with cypress. 18 | - libgconf-2-4 19 | 20 | install: 21 | - yarn --silent 22 | 23 | script: 24 | - yarn test --silent 25 | - yarn build 26 | - yarn size 27 | 28 | notifications: 29 | email: 30 | on_failure: change 31 | 32 | cache: 33 | yarn: true 34 | directories: 35 | - ~/.npm # cache npm's cache 36 | - ~/npm # cache latest npm 37 | # we also need to cache folder with Cypress binary 38 | - ~/.cache 39 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /@types/vendor.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@editorjs/*' 2 | 3 | // required for storybook 4 | // inspired by https://github.com/storybookjs/storybook/issues/2883#issuecomment-409839786 5 | declare module '*.md' { 6 | const value: string 7 | export default value 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-editor-js 2 | 3 | All notable changes to this project will be documented here. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 4 | 5 | 6 | ### [0.3.1](https://github.com/natterstefan/react-editor-js/compare/v0.3.0...v0.3.1) (2019-12-17) 7 | 8 | 9 | ### Fixes 10 | 11 | * still support deprecated holderId property ([10774a4](https://github.com/natterstefan/react-editor-js/commit/10774a41b002acf9398036ceee387299c2b0304d)) 12 | 13 | ## [0.3.0](https://github.com/natterstefan/react-editor-js/compare/v0.2.3...v0.3.0) (2019-12-17) 14 | 15 | 16 | ### Features 17 | 18 | * use holder instead of holderId now for the element (id) where editor will be added to ([16d30e8](https://github.com/natterstefan/react-editor-js/commit/16d30e813975ef3ecbf0bff7995b13e5fb6bdaff)) 19 | 20 | ### [0.2.3](https://github.com/natterstefan/react-editor-js/compare/v0.2.2...v0.2.3) (2019-11-26) 21 | 22 | 23 | ### Fixes 24 | 25 | * properly handle reinitializeOnPropsChange and render cycles, rename type Props ([88ce8d6](https://github.com/natterstefan/react-editor-js/commit/88ce8d676445bfdbaa2749618ea446fa2aaee38e)) 26 | 27 | ### [0.2.2](https://github.com/natterstefan/react-editor-js/compare/v0.2.1...v0.2.2) (2019-11-23) 28 | 29 | 30 | ### Features 31 | 32 | * added reinitializeOnPropsChange ([b80d900](https://github.com/natterstefan/react-editor-js/commit/b80d90024b5653c91c4c907f2bbc97efa7e82f98)) 33 | 34 | ### [0.2.1](https://github.com/natterstefan/react-editor-js/compare/v0.2.0...v0.2.1) (2019-11-13) 35 | 36 | - Release for [npmjs.com](https://www.npmjs.com/package/@natterstefan/react-editor-js): 37 | fix build 38 | 39 | ### [0.2.0](https://github.com/natterstefan/react-editor-js/compare/v0.1.1...v0.2.0) (2019-11-13) 40 | 41 | ### Features 42 | 43 | - memoizing react component ([898de34](https://github.com/natterstefan/react-editor-js/commit/898de3441a041460982e605abe6c5bf6340c81e7)) 44 | 45 | ### [0.1.1](https://github.com/natterstefan/react-editor-js/compare/v0.1.0...v0.1.1) (2019-11-09) 46 | 47 | Release for [npmjs.com](https://www.npmjs.com/package/@natterstefan/react-editor-js). 48 | 49 | ### 0.1.0 (2019-11-09) 50 | 51 | ### Features 52 | 53 | - initial setup with component, storybook and tests (cypress and jest) ([94632c5](https://github.com/natterstefan/react-editor-js/commit/94632c5d176435a06b65a8e84e8783d21e595ce4)) 54 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Stefan Natter 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-editor-js 2 | 3 | [![npm version](https://badge.fury.io/js/%40natterstefan%2Freact-editor-js.svg)](https://badge.fury.io/js/%40natterstefan%2Freact-editor-js) 4 | [![Build Status](https://travis-ci.com/natterstefan/react-editor-js.svg?branch=master)](https://travis-ci.com/natterstefan/react-editor-js) 5 | [![Cypress.io tests](https://img.shields.io/badge/cypress.io-tests-green.svg?style=flat-square)](https://cypress.io) 6 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 7 | [![Netlify Status](https://api.netlify.com/api/v1/badges/98a2eaf6-7b36-4136-adbd-38d7d68085b3/deploy-status)](https://app.netlify.com/sites/react-editor-js/deploys) 8 | 9 | Unofficial react component for Editor.js ([https://editorjs.io/][1]). 10 | 11 | ## Demo 12 | 13 | You can see [react-editor-js](https://github.com/natterstefan/react-editor-js) 14 | in action on both [codesandbox](https://codesandbox.io/s/react-editor-js-example-m9e49) 15 | and [netlify](https://react-editor-js.netlify.com/). 16 | 17 | ## Getting started 18 | 19 | ```sh 20 | npm i @natterstefan/react-editor-js --save 21 | 22 | # or 23 | yarn add @natterstefan/react-editor-js 24 | ``` 25 | 26 | ## PeerDependencies 27 | 28 | You have to install the required peerDependencies (eg. `React >= 16.8`), which 29 | are listed by the command: 30 | 31 | ```sh 32 | npm info "@natterstefan/react-editor-js@latest" peerDependencies 33 | ``` 34 | 35 | If using npm 5+, use this shortcut: 36 | 37 | ```sh 38 | npx install-peerdeps --dev @natterstefan/react-editor-js 39 | 40 | # or 41 | yarn add @natterstefan/react-editor-js -D --peer 42 | ``` 43 | 44 | ## Usage 45 | 46 | ```jsx 47 | // index.js 48 | import EditorJs from '@natterstefan/react-editor-js' 49 | 50 | const App = () => { 51 | return 52 | } 53 | ``` 54 | 55 | Whereas `data` looks similar to this [example](cypress/fixtures/data.ts). It is 56 | based on the example output presented on [editorjs.io][1]. 57 | 58 | ### Configuration 59 | 60 | `EditorJs` passes all given props straight to the `editorjs` instance. It is 61 | basically just a wrapper component in React. Take a look at the 62 | [configuration page in the editor.js documentation](https://editorjs.io/configuration) 63 | for more details. 64 | 65 | #### Advanced example with callbacks, custom element and instance access 66 | 67 | ```jsx 68 | // index.js 69 | import EditorJs from '@natterstefan/react-editor-js' 70 | 71 | const App = () => { 72 | const editor = null 73 | 74 | const onReady = () => { 75 | // https://editorjs.io/configuration#editor-modifications-callback 76 | console.log('Editor.js is ready to work!') 77 | } 78 | 79 | const onChange = () => { 80 | // https://editorjs.io/configuration#editor-modifications-callback 81 | console.log("Now I know that Editor's content changed!") 82 | } 83 | 84 | const onSave = async () => { 85 | // https://editorjs.io/saving-data 86 | try { 87 | const outputData = await editor.save() 88 | console.log('Article data: ', outputData) 89 | } catch (e) { 90 | console.log('Saving failed: ', e) 91 | } 92 | } 93 | 94 | return ( 95 |
96 | 97 | {/* docs: https://editorjs.io/configuration */} 98 | { 105 | // invoked once the editorInstance is ready 106 | editor = editorInstance 107 | }} 108 | > 109 |
110 | 111 |
112 | ) 113 | } 114 | ``` 115 | 116 | ## Plugins / Tools 117 | 118 | If you want to add [more tools](https://editorjs.io/getting-started#tools-installation) 119 | simply pass a `tools` property to the `EditorJs` component: 120 | 121 | ```jsx 122 | // index.js 123 | import EditorJs from '@natterstefan/react-editor-js' 124 | import Header from '@editorjs/header' 125 | 126 | const App = () => { 127 | return 128 | } 129 | ``` 130 | 131 | `EditorJs` already comes with a basic config for [@editorjs/paragraph](https://www.npmjs.com/package/@editorjs/paragraph) 132 | and [@editorjs/header](https://www.npmjs.com/package/@editorjs/header). Take a 133 | look on their [Github](https://github.com/editor-js) page to find more available 134 | plugins (or take a look at [the storybook example](src/__stories__/config.ts)). 135 | 136 | ## Additional Props 137 | 138 | | Name | Type | Default | Description | 139 | | :------------------------ | :-------: | :-----: | :----------------------------------------------------------------------------------------------------------------------- | 140 | | reinitializeOnPropsChange | `boolean` | `false` | editor-js is initialised again on [componentDidUpdate](https://reactjs.org/docs/react-component.html#componentdidupdate) | 141 | 142 | ## References 143 | 144 | - [Debug GitHub Action with tmate](https://github.com/marketplace/actions/debugging-with-tmate) 145 | 146 | ## Licence 147 | 148 | [MIT](LICENCE) 149 | 150 | This project is not affiliated, associated, authorized, endorsed by or in any 151 | way officially connected to EditorJS ([editorjs.io](https://editorjs.io/)). 152 | 153 | ## Contributors ✨ 154 | 155 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 |
Stefan Natter
Stefan Natter

💻 📖 💡
165 | 166 | 167 | 168 | 169 | 170 | 171 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 172 | 173 | [1]: https://editorjs.io/ 174 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "supportFile": "cypress/support/index.ts" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/fixtures/data.ts: -------------------------------------------------------------------------------- 1 | import { OutputData } from '@editorjs/editorjs' 2 | 3 | interface IDataObj extends OutputData { 4 | blocks: Array<{ 5 | type: string 6 | data: { 7 | [key: string]: any 8 | } 9 | }> 10 | } 11 | 12 | const data: IDataObj = { 13 | time: new Date().getTime(), 14 | blocks: [ 15 | { 16 | type: 'header', 17 | data: { 18 | text: 'Editor.js', 19 | level: 2, 20 | }, 21 | }, 22 | { 23 | type: 'paragraph', 24 | data: { 25 | text: 26 | 'Hey. Meet the new Editor. On this page you can see it in action — try to edit this text.', 27 | }, 28 | }, 29 | { 30 | type: 'header', 31 | data: { 32 | text: 'Key features', 33 | level: 3, 34 | }, 35 | }, 36 | { 37 | type: 'list', 38 | data: { 39 | style: 'unordered', 40 | items: [ 41 | 'It is a block-styled editor', 42 | 'It returns clean data output in JSON', 43 | 'Designed to be extendable and pluggable with a simple API', 44 | ], 45 | }, 46 | }, 47 | { 48 | type: 'header', 49 | data: { 50 | text: 'What does it mean «block-styled editor»', 51 | level: 3, 52 | }, 53 | }, 54 | { 55 | type: 'paragraph', 56 | data: { 57 | text: 58 | 'Workspace in classic editors is made of a single contenteditable element, used to create different HTML markups. Editor.js workspace consists of separate Blocks: paragraphs, headings, images, lists, quotes, etc. Each of them is an independent contenteditable element (or more complex structure) provided by Plugin and united by Editor\'s Core.', 59 | }, 60 | }, 61 | { 62 | type: 'paragraph', 63 | data: { 64 | text: 65 | 'There are dozens of ready-to-use Blocks and the simple API for creation any Block you need. For example, you can implement Blocks for Tweets, Instagram posts, surveys and polls, CTA-buttons and even games.', 66 | }, 67 | }, 68 | { 69 | type: 'header', 70 | data: { 71 | text: 'What does it mean clean data output', 72 | level: 3, 73 | }, 74 | }, 75 | { 76 | type: 'paragraph', 77 | data: { 78 | text: 79 | 'Classic WYSIWYG-editors produce raw HTML-markup with both content data and content appearance. On the contrary, Editor.js outputs JSON object with data of each Block. You can see an example below', 80 | }, 81 | }, 82 | { 83 | type: 'paragraph', 84 | data: { 85 | text: 86 | 'Given data can be used as you want: render with HTML for Web clients, render natively for mobile apps, create markup for Facebook Instant Articles or Google AMP, generate an audio version and so on.', 87 | }, 88 | }, 89 | { 90 | type: 'paragraph', 91 | data: { 92 | text: 93 | 'Clean data is useful to sanitize, validate and process on the backend.', 94 | }, 95 | }, 96 | { 97 | type: 'delimiter', 98 | data: {}, 99 | }, 100 | { 101 | type: 'paragraph', 102 | data: { 103 | text: 104 | "We have been working on this project more than three years. Several large media projects help us to test and debug the Editor, to make it's core more stable. At the same time we significantly improved the API. Now, it can be used to create any plugin for any task. Hope you enjoy. 😏", 105 | }, 106 | }, 107 | { 108 | type: 'image', 109 | data: { 110 | file: { 111 | url: 'https://capella.pics/6d8f1a84-9544-4afa-b806-5167d45baf7c.jpg', 112 | }, 113 | caption: '', 114 | withBorder: true, 115 | stretched: false, 116 | withBackground: false, 117 | }, 118 | }, 119 | ], 120 | version: '2.15.0', 121 | } 122 | 123 | export default data 124 | -------------------------------------------------------------------------------- /cypress/integration/index.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line */ 2 | /// 3 | 4 | /** 5 | * The cypress depend heavily on the storybook stories. The stories provide 6 | * the environment and pages for cypress to test the component. Changes in the 7 | * stories potentially break things here. 8 | * 9 | * Cypress Docs 10 | * - `should` syntax: https://docs.cypress.io/api/commands/should.html#Syntax 11 | * - should vs. then => https://docs.cypress.io/api/commands/should.html#Differences 12 | * should => cypress will automatically retry the callback function until 13 | * it passes or the command times out. 14 | * - how to write tests: https://docs.cypress.io/guides/getting-started/writing-your-first-test.html 15 | */ 16 | import data from '../fixtures/data' 17 | 18 | const STORY_URL = 'http://localhost:6006/?path=/story/reacteditorjs--default' 19 | const STORY_IFRAME_URL = 20 | 'http://localhost:6006/iframe.html?id=reacteditorjs--default' 21 | 22 | describe('react-editor-js', () => { 23 | it('renders properly the given data input', () => { 24 | cy.visit(STORY_IFRAME_URL) 25 | 26 | // test it editorjs was successfully rendered 27 | cy.get('#editorjs').should('be.visible') 28 | cy.get('.codex-editor').should('be.visible') 29 | 30 | // test it the given data (blocks) were successfully rendered 31 | cy.get('#editorjs').should(element => 32 | expect(element).to.contain(data.blocks[1].data.text), 33 | ) 34 | }) 35 | 36 | it('returns editorjs instance properly', () => { 37 | cy.visit(STORY_IFRAME_URL) 38 | 39 | cy.window({ timeout: 5000 }) 40 | .its('app.configuration.holder' as any) 41 | .should('equal', 'editorjs') 42 | }) 43 | 44 | it('passes callbacks like onChange properly to editorjs instance', () => { 45 | cy.visit(STORY_IFRAME_URL) 46 | 47 | cy.get('.ce-block__content h2').type('Hello, World') 48 | // test it the given data (blocks) were successfully rendered 49 | cy.get('#editorjs').should(element => 50 | expect(element).to.contain('Hello, World'), 51 | ) 52 | 53 | cy.visit(STORY_URL) 54 | cy.wait(1000) // give storybook some time to load the iframe 55 | 56 | /** 57 | * Inspired by: 58 | * - https://github.com/cypress-io/cypress/issues/136#issuecomment-309090376 59 | * - https://github.com/cypress-io/cypress/issues/136#issuecomment-341680824 60 | */ 61 | cy.get('iframe') 62 | // first prepare the changes 63 | .then($iframe => { 64 | const $body = $iframe.contents().find('body') 65 | const $element = cy.wrap($body).find('h2') 66 | $element.type('Hello, World', { delay: 350 }) 67 | }) 68 | // then test the result 69 | .then(() => { 70 | cy.wait(500) 71 | cy.get('.simplebar-wrapper ol') 72 | .first() 73 | .then(element => { 74 | expect(element).to.contain('onChange') 75 | }) 76 | }) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /cypress/plugins/cy-ts-preprocessor.js: -------------------------------------------------------------------------------- 1 | // https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/preprocessors__typescript-webpack 2 | // https://github.com/bahmutov/add-typescript-to-cypress 3 | const wp = require('@cypress/webpack-preprocessor') 4 | 5 | const webpackOptions = { 6 | resolve: { 7 | extensions: ['.ts', '.js'], 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.ts$/, 13 | exclude: [/node_modules/], 14 | use: [ 15 | { 16 | loader: 'ts-loader', 17 | }, 18 | ], 19 | }, 20 | ], 21 | }, 22 | } 23 | 24 | const options = { 25 | webpackOptions, 26 | } 27 | 28 | module.exports = wp(options) 29 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor') 2 | 3 | module.exports = on => { 4 | on('file:preprocessor', cypressTypeScriptPreprocessor) 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "../node_modules/cypress", 5 | "*/*.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // required for VS Code 2 | // inspired by https://github.com/storybookjs/storybook/issues/2883#issuecomment-409839786 3 | declare module '*.md' { 4 | const value: string 5 | export default value 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | tsConfig: 'tsconfig.test.json', 5 | }, 6 | }, 7 | setupFilesAfterEnv: ['/jest.setup.ts'], 8 | testPathIgnorePatterns: ['/(dist|node_modules|cypress)/'], 9 | transform: { 10 | '^.+\\.(t|j)sx?$': 'ts-jest', 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@natterstefan/react-editor-js", 3 | "version": "0.3.1", 4 | "description": "Unofficial react component for editorjs (https://editorjs.io/)", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/natterstefan/react-editor-js.git" 8 | }, 9 | "author": "Stefan Natter (https://twitter.com/natterstefan)", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/natterstefan/react-editor-js/issues" 13 | }, 14 | "homepage": "https://github.com/natterstefan/react-editor-js#readme", 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "main": "lib/index.js", 19 | "module": "esm/index.js", 20 | "types": "lib/index.d.ts", 21 | "files": [ 22 | "dist", 23 | "es", 24 | "esm", 25 | "lib" 26 | ], 27 | "keywords": [ 28 | "react", 29 | "editor", 30 | "editor.js", 31 | "editorjs", 32 | "@editorjs", 33 | "react-editor-js", 34 | "react-editorjs", 35 | "editorjs-component", 36 | "editor-js-component", 37 | "wysiwyg" 38 | ], 39 | "scripts": { 40 | "build": "npm run build-cjs && npm run build-es && npm run build-esm && npm run build-umd", 41 | "build-cjs": "tsc --outDir lib --module commonjs --target es5 -d --declarationMap", 42 | "build-es": "tsc --outDir es --module es2015 --target es2015 -d --declarationMap", 43 | "build-esm": "tsc --outDir esm --module es2015 --target es5 -d --declarationMap", 44 | "build-umd": "webpack --mode=production", 45 | "build-storybook": "build-storybook", 46 | "contributors-add": "all-contributors add", 47 | "contributors-generate": "all-contributors generate", 48 | "cypress": "cypress open", 49 | "cypress:run": "cypress run", 50 | "lint": "tsc --noEmit && eslint '**/*.{ts,tsx}' --quiet", 51 | "prebuild": "rimraf dist && rimraf es && rimraf esm && rimraf lib", 52 | "prepublishOnly": "npm run build && npm run prerelease", 53 | "prerelease": "npm run test", 54 | "release": "HUSKY_SKIP_HOOKS=1 standard-version", 55 | "size": "npm run size-build && size-limit", 56 | "size-build": "npm run build", 57 | "start": "yarn start-storybook -p 6006 --ci", 58 | "test": "npm run test-jest && npm run test-cypress", 59 | "test-jest": "jest", 60 | "test-cypress": "start-server-and-test start http-get://localhost:6006 cypress:run", 61 | "watch-test": "jest --watch" 62 | }, 63 | "peerDependencies": { 64 | "@editorjs/editorjs": ">=2.16", 65 | "@editorjs/header": ">=2.3", 66 | "@editorjs/paragraph": ">=2.6", 67 | "react": ">=16.8" 68 | }, 69 | "devDependencies": { 70 | "@babel/core": "7.9.0", 71 | "@bahmutov/add-typescript-to-cypress": "2.1.2", 72 | "@cypress/webpack-preprocessor": "4.1.1", 73 | "@editorjs/checklist": "1.1.0", 74 | "@editorjs/code": "2.4.1", 75 | "@editorjs/delimiter": "1.1.0", 76 | "@editorjs/editorjs": "2.16.1", 77 | "@editorjs/embed": "2.2.1", 78 | "@editorjs/header": "2.3.2", 79 | "@editorjs/image": "2.3.3", 80 | "@editorjs/inline-code": "1.3.1", 81 | "@editorjs/link": "2.1.3", 82 | "@editorjs/list": "1.4.0", 83 | "@editorjs/marker": "1.2.2", 84 | "@editorjs/paragraph": "2.6.1", 85 | "@editorjs/quote": "2.3.0", 86 | "@editorjs/raw": "2.1.1", 87 | "@editorjs/simple-image": "1.3.3", 88 | "@editorjs/table": "1.2.1", 89 | "@editorjs/warning": "1.1.1", 90 | "@size-limit/preset-small-lib": "4.4.5", 91 | "@storybook/addon-actions": "5.3.17", 92 | "@storybook/addon-knobs": "5.3.18", 93 | "@storybook/core": "5.3.17", 94 | "@storybook/core-events": "5.3.17", 95 | "@storybook/preset-typescript": "3.0.0", 96 | "@storybook/react": "5.3.17", 97 | "@types/enzyme": "3.10.5", 98 | "@types/enzyme-adapter-react-16": "1.0.6", 99 | "@types/jest": "25.2.1", 100 | "@types/react": "16.9.34", 101 | "@types/react-dom": "16.9.6", 102 | "all-contributors-cli": "6.14.2", 103 | "babel-eslint": "10.1.0", 104 | "babel-loader": "8.1.0", 105 | "commitizen": "4.0.4", 106 | "cypress": "3.8.3", 107 | "cz-conventional-changelog": "3.1.0", 108 | "enzyme": "3.11.0", 109 | "enzyme-adapter-react-16": "1.15.2", 110 | "eslint": "6.8.0", 111 | "eslint-config-airbnb": "18.1.0", 112 | "eslint-config-ns-ts": "1.1.0", 113 | "eslint-config-prettier": "6.10.1", 114 | "eslint-import-resolver-alias": "1.1.2", 115 | "eslint-plugin-import": "2.20.2", 116 | "eslint-plugin-jest": "23.8.2", 117 | "eslint-plugin-jsx-a11y": "6.2.3", 118 | "eslint-plugin-prettier": "3.1.3", 119 | "eslint-plugin-react": "7.19.0", 120 | "eslint-plugin-react-hooks": "3.0.0", 121 | "fork-ts-checker-webpack-plugin": "4.1.3", 122 | "husky": "4.2.5", 123 | "jest": "25.3.0", 124 | "lint-staged": "10.1.4", 125 | "prettier": "2.0.4", 126 | "react": "16.13.1", 127 | "react-docgen-typescript-loader": "3.7.2", 128 | "react-dom": "16.13.1", 129 | "rimraf": "3.0.2", 130 | "size-limit": "4.4.5", 131 | "standard-version": "7.1.0", 132 | "start-server-and-test": "1.11.0", 133 | "storybook-readme": "5.0.8", 134 | "terser-webpack-plugin": "2.3.5", 135 | "ts-jest": "25.4.0", 136 | "ts-loader": "7.0.1", 137 | "typescript": "3.8.3", 138 | "webpack": "4.42.1", 139 | "webpack-cli": "3.3.11" 140 | }, 141 | "husky": { 142 | "hooks": { 143 | "pre-commit": "lint-staged", 144 | "prepare-commit-msg": "exec < /dev/tty && git cz --hook" 145 | } 146 | }, 147 | "lint-staged": { 148 | "*.js": [ 149 | "npm run lint", 150 | "prettier --write", 151 | "git update-index --again", 152 | "jest --findRelatedTests" 153 | ] 154 | }, 155 | "size-limit": [ 156 | { 157 | "limit": "6 KB", 158 | "path": "dist/**/*.js", 159 | "config": "./webpack.config.js", 160 | "ignore": [ 161 | "react", 162 | "react-dom" 163 | ] 164 | }, 165 | { 166 | "limit": "6 KB", 167 | "webpack": false, 168 | "path": "lib/**/*.js" 169 | }, 170 | { 171 | "limit": "6 KB", 172 | "webpack": false, 173 | "path": "es/**/*.js" 174 | }, 175 | { 176 | "limit": "6 KB", 177 | "webpack": false, 178 | "path": "esm/**/*.js" 179 | } 180 | ], 181 | "config": { 182 | "commitizen": { 183 | "path": "./node_modules/cz-conventional-changelog" 184 | } 185 | }, 186 | "standard-version": { 187 | "changelogHeader": "# react-editor-js\n\nAll notable changes to this project will be documented here. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).\n\n", 188 | "types": [ 189 | { 190 | "type": "feat", 191 | "section": "Features" 192 | }, 193 | { 194 | "type": "fix", 195 | "section": "Fixes" 196 | }, 197 | { 198 | "type": "chore", 199 | "hidden": true 200 | }, 201 | { 202 | "type": "docs", 203 | "hidden": true 204 | }, 205 | { 206 | "type": "style", 207 | "hidden": true 208 | }, 209 | { 210 | "type": "refactor", 211 | "hidden": true 212 | }, 213 | { 214 | "type": "perf", 215 | "hidden": true 216 | }, 217 | { 218 | "type": "test", 219 | "hidden": true 220 | } 221 | ] 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const prettier = require('eslint-config-ns-ts/prettier.config') 3 | 4 | module.exports = { 5 | ...prettier, 6 | arrowParens: 'avoid', 7 | } 8 | -------------------------------------------------------------------------------- /src/__stories__/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * find more available plugins here: 3 | * - https://www.npmjs.com/search?q=%40editorjs 4 | * - or https://github.com/editor-js 5 | * 6 | * or create your own: https://editorjs.io/the-first-plugin 7 | */ 8 | import CheckList from '@editorjs/checklist' 9 | import Code from '@editorjs/code' 10 | import Delimiter from '@editorjs/delimiter' 11 | import Embed from '@editorjs/embed' 12 | import Image from '@editorjs/image' 13 | import InlineCode from '@editorjs/inline-code' 14 | import LinkTool from '@editorjs/link' 15 | import List from '@editorjs/list' 16 | import Marker from '@editorjs/marker' 17 | import Quote from '@editorjs/quote' 18 | import Raw from '@editorjs/raw' 19 | import SimpleImage from '@editorjs/simple-image' 20 | import Table from '@editorjs/table' 21 | import Warning from '@editorjs/warning' 22 | 23 | export const TOOLS = { 24 | embed: Embed, 25 | table: Table, 26 | list: List, 27 | warning: Warning, 28 | code: Code, 29 | linkTool: LinkTool, 30 | image: Image, 31 | raw: Raw, 32 | quote: Quote, 33 | marker: Marker, 34 | checklist: CheckList, 35 | delimiter: Delimiter, 36 | inlineCode: InlineCode, 37 | simpleImage: SimpleImage, 38 | } 39 | -------------------------------------------------------------------------------- /src/__stories__/custom-plugin-js.tsx: -------------------------------------------------------------------------------- 1 | type EditorJSAPI = import('@editorjs/editorjs').API 2 | 3 | type DataType = { 4 | counter: number 5 | [k: string]: any 6 | } 7 | 8 | export class CustomJs { 9 | private data: DataType = null 10 | 11 | constructor(props: { data: DataType; api: EditorJSAPI }) { 12 | const { data } = props 13 | this.data = data 14 | } 15 | 16 | createText() { 17 | return `Click me [clicked: ${this.data.counter} times]` 18 | } 19 | 20 | render() { 21 | const container = document.createElement('div') 22 | this.data.counter = 0 23 | 24 | const button = document.createElement('button') 25 | button.id = 'custom-js-button' 26 | button.style.padding = '10px' 27 | button.onclick = () => { 28 | this.data.counter++ 29 | const elem = document.getElementById('custom-js-button') 30 | elem.innerHTML = this.createText() 31 | } 32 | button.innerHTML = this.createText() 33 | container.appendChild(button) 34 | 35 | return container 36 | } 37 | 38 | save() { 39 | return { 40 | value: this.data.counter, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/__stories__/custom-plugin-react.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable no-console */ 3 | /* eslint-disable react/jsx-props-no-spreading */ 4 | import React, { useCallback, useEffect, useRef, useState } from 'react' 5 | import ReactDOM from 'react-dom' 6 | 7 | type EditorJSAPI = import('@editorjs/editorjs').API 8 | 9 | type DataType = { 10 | component: React.ComponentType 11 | [k: string]: any 12 | } 13 | 14 | export const Button = (props: any) => { 15 | const { api, data, onChange } = props 16 | const [counter, setCounter] = useState((data && data.counter) || 0) 17 | const buttonRef = useRef() 18 | 19 | const onClick = useCallback(() => { 20 | const newCounter = counter + 1 21 | setCounter(newCounter) 22 | onChange({ counter: newCounter }) 23 | }, [counter, onChange]) 24 | 25 | useEffect(() => { 26 | const buttonElement = buttonRef.current 27 | 28 | api.listeners.on(buttonElement, 'click', onClick) 29 | 30 | return () => { 31 | api.listeners.off(buttonElement, 'click', onClick) 32 | } 33 | }, [api.listeners, onClick]) 34 | 35 | return ( 36 | 44 | ) 45 | } 46 | 47 | export class CustomReact { 48 | private data: DataType = null 49 | 50 | private api: EditorJSAPI = null 51 | 52 | /** 53 | * Example config: 54 | * 55 | * ```js 56 | * const data = { 57 | * blocks: [ 58 | * { 59 | * type: 'customReact', 60 | * data: { 61 | * component: Button, 62 | * counter: 0, 63 | * }, 64 | * }, 65 | * ] 66 | * } 67 | * 68 | * const tools = { customReact: CustomReact } 69 | * ``` 70 | */ 71 | constructor({ data, api }: { data: DataType; api: EditorJSAPI }) { 72 | this.api = api 73 | this.data = data 74 | 75 | this.onChange = this.onChange.bind(this) 76 | } 77 | 78 | onChange(data: DataType) { 79 | this.data = { 80 | ...data, 81 | component: this.data.component, 82 | } 83 | } 84 | 85 | render() { 86 | const container = document.createElement('div') 87 | const Editor = this.data.component 88 | 89 | if (Editor) { 90 | ReactDOM.render( 91 | , 92 | container, 93 | ) 94 | } 95 | 96 | return container 97 | } 98 | 99 | save() { 100 | return this.data 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/__stories__/index.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | /* eslint-disable react/prop-types */ 3 | import React, { useRef, useState, MutableRefObject } from 'react' 4 | import { storiesOf } from '@storybook/react' 5 | import { action } from '@storybook/addon-actions' 6 | import { boolean, withKnobs } from '@storybook/addon-knobs' 7 | import EditorJS from '@editorjs/editorjs' 8 | 9 | import data from '../../cypress/fixtures/data' 10 | import Readme from '../../README.md' 11 | import EditorJs from '..' 12 | 13 | import { TOOLS } from './config' 14 | import { CustomJs } from './custom-plugin-js' 15 | import { CustomReact, Button } from './custom-plugin-react' 16 | 17 | const SaveButton = ({ 18 | onClick, 19 | }: { 20 | onClick: (event: React.MouseEvent) => void 21 | }) => ( 22 | 38 | ) 39 | 40 | storiesOf('ReactEditorJs', module) 41 | .addDecorator(withKnobs) 42 | .add('readme', () =>
, { 43 | readme: { 44 | content: Readme, 45 | }, 46 | }) 47 | .add('default', () => { 48 | let editorInstance: EditorJS = null 49 | 50 | const onChange = () => { 51 | action('EditorJs onChange')(editorInstance) 52 | } 53 | 54 | return ( 55 | { 60 | editorInstance = instance 61 | action('EditorJs editorInstance')(editorInstance) 62 | // added to window for cypress testing 63 | ;(window as any).app = editorInstance 64 | }} 65 | /> 66 | ) 67 | }) 68 | .add('controlled EditorJs', () => { 69 | const reinitializeOnPropsChange = boolean( 70 | 'reinitializeOnPropsChange', 71 | false, 72 | ) 73 | 74 | const App = () => { 75 | const [appData, setAppData] = useState(data) 76 | const editorInstance: MutableRefObject = useRef(null) 77 | 78 | const onSave = async () => { 79 | if (editorInstance.current) { 80 | try { 81 | const outputData = await editorInstance.current.save() 82 | action('EditorJs onSave')(outputData) 83 | setAppData(outputData) 84 | } catch (error) { 85 | action('EditorJs was not able to save data')(error) 86 | } 87 | } 88 | } 89 | 90 | const onChange = () => { 91 | action('EditorJs onChange') 92 | onSave() 93 | } 94 | 95 | return ( 96 |
97 | 98 | { 102 | editorInstance.current = instance 103 | action('EditorJs editorInstance')(instance) 104 | }} 105 | onChange={onChange} 106 | reinitializeOnPropsChange={reinitializeOnPropsChange} 107 | /> 108 |
109 | ) 110 | } 111 | 112 | return 113 | }) 114 | .add('controlled App -> Editor -> EditorJs', () => { 115 | // the ´` renders an `` component, which renders `EditorJs` 116 | const App = () => { 117 | const [appData, setAppData] = useState(data) 118 | 119 | const onChange = (newAppData: any) => { 120 | setAppData(newAppData) 121 | } 122 | 123 | return 124 | } 125 | 126 | const Editor = ({ 127 | appData, 128 | onChange, 129 | }: { 130 | appData: any 131 | onChange: (data: any) => void 132 | }) => { 133 | const editorInstance: MutableRefObject = useRef(null) 134 | 135 | const onChangeHandler = async () => { 136 | if (editorInstance.current) { 137 | try { 138 | const outputData = await editorInstance.current.save() 139 | action('EditorJs onSave')(outputData) 140 | onChange(outputData) 141 | } catch (error) { 142 | action('EditorJs was not able to save data')(error) 143 | } 144 | } 145 | } 146 | 147 | return ( 148 |
149 | 150 | { 154 | editorInstance.current = instance 155 | action('EditorJs editorInstance')(instance) 156 | }} 157 | onChange={onChangeHandler} 158 | /> 159 |
160 | ) 161 | } 162 | 163 | return 164 | }) 165 | .add('with custom holder', () => { 166 | const editorInstance: MutableRefObject = useRef(null) 167 | 168 | const onChange = () => { 169 | action('EditorJs onChange')(editorInstance.current) 170 | } 171 | 172 | return ( 173 | { 179 | editorInstance.current = instance 180 | action('EditorJs editorInstance')(editorInstance) 181 | }} 182 | > 183 |
184 | 185 | ) 186 | }) 187 | .add('with custom tool (react)', () => { 188 | const editorInstance: MutableRefObject = useRef(null) 189 | 190 | const customData = { 191 | blocks: [ 192 | { 193 | type: 'header', 194 | data: { 195 | text: 'Editor.js', 196 | level: 1, 197 | }, 198 | }, 199 | { 200 | type: 'header', 201 | data: { 202 | text: 'CustomReact Plugin', 203 | level: 2, 204 | }, 205 | }, 206 | { 207 | type: 'customReact', 208 | data: { 209 | component: Button, 210 | }, 211 | }, 212 | { 213 | type: 'header', 214 | data: { 215 | text: 'CustomJS Plugin', 216 | level: 2, 217 | }, 218 | }, 219 | { 220 | type: 'customJs', 221 | data: {}, 222 | }, 223 | ], 224 | } 225 | 226 | const onSave = async () => { 227 | if (editorInstance.current) { 228 | try { 229 | const outputData = await editorInstance.current.save() 230 | action('EditorJs onSave')(outputData) 231 | } catch (e) { 232 | action('EditorJs onSave failed')(e) 233 | } 234 | } 235 | } 236 | 237 | return ( 238 |
239 | 240 | { 244 | editorInstance.current = instance 245 | action('EditorJs editorInstance')(editorInstance) 246 | }} 247 | /> 248 |
249 | ) 250 | }) 251 | -------------------------------------------------------------------------------- /src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallow } from 'enzyme' 3 | 4 | import EditorJs from '..' 5 | 6 | describe('EditorJs', () => { 7 | it('renders div container with default holder id', () => { 8 | const wrapper = shallow() 9 | expect(wrapper.html()).toMatchInlineSnapshot( 10 | `"
"`, 11 | ) 12 | }) 13 | 14 | it('renders custom div container', () => { 15 | const wrapper = shallow( 16 | 17 |
18 | , 19 | ) 20 | expect(wrapper.html()).toMatchInlineSnapshot( 21 | `"
"`, 22 | ) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FunctionComponent, 3 | memo, 4 | MutableRefObject, 5 | ReactElement, 6 | useCallback, 7 | useEffect, 8 | useRef, 9 | } from 'react' 10 | import EditorJS from '@editorjs/editorjs' 11 | import Paragraph from '@editorjs/paragraph' 12 | import Header from '@editorjs/header' 13 | 14 | export interface IEditorJsProps extends EditorJS.EditorConfig { 15 | children?: ReactElement 16 | /** 17 | * Element id where Editor will be append 18 | * @deprecated property will be removed in next major release, 19 | * use holder instead 20 | */ 21 | holderId?: string 22 | /** 23 | * Element id where Editor will be append 24 | */ 25 | holder?: string 26 | /** 27 | * reinitialize editor.js when component did update 28 | */ 29 | reinitializeOnPropsChange?: boolean 30 | /** 31 | * returns the editorjs instance 32 | */ 33 | editorInstance?: (instance: EditorJS) => void 34 | } 35 | 36 | const DEFAULT_ID = 'editorjs' 37 | 38 | /** 39 | * EditorJs wraps editor.js in a React component and providing an API to be able 40 | * to interact with the editor.js instance. 41 | */ 42 | const EditorJs: FunctionComponent = (props): ReactElement => { 43 | const { 44 | holderId: deprecatedId, 45 | holder: customHolderId, 46 | editorInstance, 47 | reinitializeOnPropsChange, 48 | children, 49 | tools, 50 | ...otherProps 51 | } = props 52 | 53 | const instance: MutableRefObject = useRef(null) 54 | const holderId = deprecatedId || customHolderId || DEFAULT_ID 55 | 56 | /** 57 | * initialise editorjs with default settings 58 | */ 59 | const initEditor = useCallback(async () => { 60 | if (!instance.current) { 61 | instance.current = new EditorJS({ 62 | tools: { 63 | paragraph: { 64 | class: Paragraph, 65 | inlineToolbar: true, 66 | }, 67 | header: Header, 68 | ...tools, 69 | }, 70 | holder: holderId, 71 | ...otherProps, 72 | }) 73 | } 74 | 75 | // callback returns current editorjs instance once it is ready 76 | if (editorInstance) { 77 | await instance.current.isReady 78 | editorInstance(instance.current) 79 | } 80 | }, [editorInstance, holderId, otherProps, tools]) 81 | 82 | /** 83 | * destroy current editorjs instance 84 | */ 85 | const destroyEditor = useCallback(async () => { 86 | if (!instance.current) { 87 | return true 88 | } 89 | 90 | await instance.current.isReady 91 | instance.current.destroy() 92 | instance.current = null 93 | return true 94 | }, [instance]) 95 | 96 | /** 97 | * initEditor on mount and destroy it on unmount 98 | */ 99 | useEffect(() => { 100 | initEditor() 101 | return (): void => { 102 | destroyEditor() 103 | } 104 | }, []) // eslint-disable-line 105 | 106 | /** 107 | * when props change and reinitializeOnPropsChange is true, the component will 108 | * first destroy and then init EditorJS again. 109 | */ 110 | useEffect(() => { 111 | const doEffect = async () => { 112 | if (!reinitializeOnPropsChange) { 113 | return 114 | } 115 | 116 | await destroyEditor() 117 | initEditor() 118 | } 119 | 120 | doEffect() 121 | }, [destroyEditor, initEditor, instance, reinitializeOnPropsChange]) 122 | 123 | return children ||
124 | } 125 | 126 | export default memo(EditorJs) 127 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './editor' 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* inspired by https://github.com/marmelab/react-admin/blob/HEAD@%7B2019-10-20T17:02:44Z%7D/tsconfig.json */ 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "target": "es5" /* Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'. */, 6 | "lib": ["es2015", "dom"], 7 | "declaration": false /* Generates corresponding '.d.ts' file. (set to true in npm script) */, 8 | "declarationMap": false /* Generates a sourcemap for each corresponding '.d.ts' file. (set to true in npm script) */, 9 | "sourceMap": true /* Generates corresponding '.map' file. */, 10 | "removeComments": false /* Do emit comments to output. */, 11 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 12 | 13 | /* Strict Type-Checking Options */ 14 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 15 | 16 | /* Additional Checks */ 17 | "noUnusedLocals": true /* Report errors on unused locals. */, 18 | "noUnusedParameters": true /* Report errors on unused parameters. */, 19 | 20 | /* Module Resolution Options */ 21 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 22 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 23 | /* source structure and types */ 24 | "baseUrl": "./", 25 | "typeRoots": ["node_modules/@types", "@types"] 26 | }, 27 | "include": ["src", "@types"], 28 | "exclude": [ 29 | "node_modules", 30 | "**/dist", 31 | "**/es", 32 | "**/esm", 33 | "**/lib", 34 | "**/__mocks__", 35 | "**/__tests__", 36 | "**/__stories__" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "noImplicitAny": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { resolve } = require('path') 3 | 4 | const TerserPlugin = require('terser-webpack-plugin') 5 | 6 | module.exports = { 7 | entry: resolve(__dirname, 'src/index.tsx'), 8 | output: { 9 | filename: 'reacteditorjs.js', 10 | library: 'ReactEditorJs', 11 | libraryTarget: 'umd', 12 | path: resolve(__dirname, './dist'), 13 | }, 14 | // https://github.com/TypeStrong/ts-loader#devtool--sourcemaps 15 | devtool: 'source-map', 16 | mode: 'production', 17 | resolve: { 18 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(t|j)sx?$/, 24 | exclude: /node_modules/, 25 | loader: 'ts-loader', 26 | }, 27 | ], 28 | }, 29 | optimization: { 30 | minimize: true, 31 | minimizer: [ 32 | new TerserPlugin({ 33 | terserOptions: { 34 | output: { 35 | comments: false, 36 | }, 37 | }, 38 | // https://github.com/webpack-contrib/terser-webpack-plugin#extractcomments 39 | extractComments: true, 40 | // https://github.com/webpack-contrib/terser-webpack-plugin#sourcemap 41 | sourceMap: true, 42 | }), 43 | ], 44 | }, 45 | externals: [ 46 | 'react', 47 | '@editorjs/editorjs', 48 | '@editorjs/checklist', 49 | '@editorjs/code', 50 | '@editorjs/delimiter', 51 | '@editorjs/editorjs', 52 | '@editorjs/embed', 53 | '@editorjs/header', 54 | '@editorjs/image', 55 | '@editorjs/inline-code', 56 | '@editorjs/link', 57 | '@editorjs/list', 58 | '@editorjs/marker', 59 | '@editorjs/paragraph', 60 | '@editorjs/quote', 61 | '@editorjs/raw', 62 | '@editorjs/simple-image', 63 | '@editorjs/table', 64 | '@editorjs/warning', 65 | ], 66 | } 67 | --------------------------------------------------------------------------------