├── .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 | [](https://badge.fury.io/js/%40natterstefan%2Freact-editor-js)
4 | [](https://travis-ci.com/natterstefan/react-editor-js)
5 | [](https://cypress.io)
6 | [](http://commitizen.github.io/cz-cli/)
7 | [](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 |
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 |
--------------------------------------------------------------------------------