├── .editorconfig
├── .eslintrc.json
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE.md
└── workflows
│ ├── setup-workflows.yml
│ ├── stalebot.yml
│ ├── test-all.yml
│ └── update-deps.yml
├── .gitignore
├── .nvmrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── karma.conf.js
├── nightwatch.conf.js
├── package.json
├── rollup.config.js
├── samples
├── README.md
├── basic-typescript
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.tsx
│ │ ├── index.html
│ │ └── index.tsx
│ ├── tsconfig.json
│ └── webpack.config.js
├── basic
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── index.html
│ │ └── index.jsx
│ └── webpack.config.js
├── component-events
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── CKEditor.jsx
│ │ ├── Sidebar.jsx
│ │ ├── index.html
│ │ └── index.jsx
│ └── webpack.config.js
├── component
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── Sidebar.jsx
│ │ ├── index.html
│ │ └── index.jsx
│ └── webpack.config.js
├── editor-url
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── index.html
│ │ └── index.jsx
│ └── webpack.config.js
├── hook-events
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── CKEditor.jsx
│ │ ├── Sidebar.jsx
│ │ ├── index.html
│ │ └── index.jsx
│ └── webpack.config.js
├── hook
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── CKEditor.jsx
│ │ ├── Sidebar.jsx
│ │ ├── index.html
│ │ └── index.jsx
│ └── webpack.config.js
├── re-order
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── index.html
│ │ └── index.jsx
│ └── webpack.config.js
├── router
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── index.html
│ │ └── index.jsx
│ └── webpack.config.js
├── ssr
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── index.jsx
│ │ ├── renderMarkup.jsx
│ │ └── server.js
│ ├── webpack.config.js
│ └── webpack.config.server.js
├── state-lifting
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── App.jsx
│ │ ├── CKEditor.jsx
│ │ ├── TextAreaEditor.jsx
│ │ ├── index.html
│ │ └── index.jsx
│ └── webpack.config.js
└── umd
│ ├── README.md
│ ├── build.js
│ ├── package.json
│ ├── sandbox.config.json
│ └── src
│ └── index.html
├── scripts
├── bump.js
├── e2e-runner.js
├── nightwatch-runner.js
├── units-runner.js
└── utils.js
├── src
├── CKEditor.tsx
├── events.ts
├── index.ts
├── modules.ts
├── registerEditorEventHandler.ts
├── types.ts
├── useCKEditor.ts
└── utils.ts
├── tests
├── e2e
│ ├── basic-typescript.js
│ ├── basic.js
│ ├── component-events.js
│ ├── component.js
│ ├── hook-events.js
│ ├── hook.js
│ ├── re-order.js
│ ├── ssr.js
│ ├── state-lifting.js
│ └── umd.js
└── unit
│ ├── CKEditor.test.tsx
│ ├── common.test.tsx
│ ├── events.test.ts
│ ├── index.ts
│ ├── registerEditorEventHandler.test.ts
│ ├── useCKEditor.test.tsx
│ ├── utils.test.ts
│ └── utils.tsx
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Configurations to normalize the IDE behavior.
2 | # http://editorconfig.org/
3 |
4 | root = true
5 |
6 | [*]
7 | indent_style = tab
8 | tab_width = 4
9 | charset = utf-8
10 | end_of_line = lf
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "eslint-config-ckeditor5",
5 | "plugin:react/recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:react-hooks/recommended"
8 | ],
9 | "settings": {
10 | "react": {
11 | "version": "detect"
12 | }
13 | },
14 | "rules": {
15 | "@typescript-eslint/explicit-module-boundary-types": "off",
16 | "@typescript-eslint/no-explicit-any": "off",
17 | "no-use-before-define": "off",
18 | "@typescript-eslint/no-use-before-define": [
19 | "error",
20 | {
21 | "functions": false
22 | }
23 | ],
24 | "@typescript-eslint/consistent-type-imports": "off",
25 | "@typescript-eslint/no-invalid-void-type": "off",
26 | "@typescript-eslint/ban-ts-comment": "off",
27 | "@typescript-eslint/no-var-requires": "off"
28 | },
29 | "env": {
30 | "browser": true
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
3 | *.htaccess eol=lf
4 | *.cgi eol=lf
5 | *.sh eol=lf
6 |
7 | *.css text
8 | *.htm text
9 | *.html text
10 | *.js text
11 | *.json text
12 | *.php text
13 | *.txt text
14 | *.md text
15 |
16 | *.png -text
17 | *.gif -text
18 | *.jpg -text
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Are you reporting a feature request or a bug?
2 |
3 |
10 |
11 | ## Provide detailed reproduction steps (if any)
12 |
13 |
25 |
26 | 1. …
27 | 2. …
28 | 3. …
29 |
30 | ### Expected result
31 |
32 | *What is the expected result of the above steps?*
33 |
34 | ### Actual result
35 |
36 | *What is the actual result of the above steps?*
37 |
38 | ## Other details
39 |
40 | * Browser: …
41 | * OS: …
42 | * Integration version: …
43 | * CKEditor version: …
44 | * Installed CKEditor plugins: …
45 |
--------------------------------------------------------------------------------
/.github/workflows/setup-workflows.yml:
--------------------------------------------------------------------------------
1 | name: Setup and update common workflows
2 |
3 | on:
4 | schedule:
5 | - cron: "0 2 * * *"
6 | workflow_dispatch:
7 | inputs:
8 | config:
9 | description: 'Config'
10 | required: false
11 | default: ''
12 |
13 | jobs:
14 | update:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout default branch
19 | # https://github.com/marketplace/actions/checkout
20 | uses: actions/checkout@v2
21 | with:
22 | token: ${{ secrets.GH_WORKFLOWS_TOKEN }}
23 |
24 | - name: Read config
25 | run: |
26 | CONFIG='{}'
27 | if [[ ! -z '${{ github.event.inputs.config }}' ]]; then
28 | CONFIG='${{ github.event.inputs.config }}'
29 | elif [[ -f "./.github/workflows-config.json" ]]; then
30 | CONFIG=$( jq -c .setupWorkflows './.github/workflows-config.json' )
31 | fi
32 | echo "CONFIG=$CONFIG" >> $GITHUB_ENV
33 | echo "Workflow config: $CONFIG"
34 |
35 | - name: Process config
36 | run: |
37 | AS_PR=$(echo '${{ env.CONFIG }}' | jq -r ".pushAsPullRequest")
38 | if [[ "$AS_PR" == "true" ]]; then
39 | echo "AS_PR=1" >> $GITHUB_ENV
40 | else
41 | echo "AS_PR=0" >> $GITHUB_ENV
42 | fi
43 | BRANCH_SOURCE=$(git rev-parse --abbrev-ref HEAD)
44 | if [[ "$AS_PR" == "true" ]]; then
45 | BRANCH_SOURCE="t/setup-workflows-update_$BRANCH_SOURCE"
46 | fi
47 | echo "BRANCH_SOURCE=$BRANCH_SOURCE" >> $GITHUB_ENV
48 |
49 | - name: Check if update branch already exists
50 | if: env.AS_PR == 1
51 | run: |
52 | if [[ $(git ls-remote --heads | grep ${{ env.BRANCH_SOURCE }} | wc -c) -ne 0 ]]; then
53 | echo "SHOULD_CANCEL=1" >> $GITHUB_ENV
54 | fi
55 |
56 | - name: Cancel build if update branch already exists
57 | if: env.SHOULD_CANCEL == 1
58 | # https://github.com/marketplace/actions/cancel-this-build
59 | uses: andymckay/cancel-action@0.2
60 |
61 | - name: Wait for cancellation
62 | if: env.SHOULD_CANCEL == 1
63 | # https://github.com/marketplace/actions/wait-sleep
64 | uses: jakejarvis/wait-action@master
65 | with:
66 | time: '60s'
67 |
68 | - name: Checkout common workflows repository
69 | # https://github.com/marketplace/actions/checkout
70 | uses: actions/checkout@v2
71 | with:
72 | path: ckeditor4-workflows-common
73 | repository: ckeditor/ckeditor4-workflows-common
74 | ref: master
75 |
76 | - name: Setup workflows directory
77 | run: |
78 | mkdir -p .github/workflows
79 |
80 | - name: Synchronize workflow files
81 | run: |
82 | rsync -a --include='*/' --include='*.yml' --exclude='*' ./ckeditor4-workflows-common/workflows/ ./.github/workflows/
83 | if [[ $(git status ./.github/workflows/ --porcelain) ]]; then
84 | echo "HAS_CHANGES=1" >> $GITHUB_ENV
85 | fi
86 |
87 | - name: Cleanup common workflows artifacts
88 | run: |
89 | rm -rf ckeditor4-workflows-common
90 |
91 | - name: Checkout PR branch
92 | if: env.HAS_CHANGES == 1
93 | run: |
94 | git checkout -b "t/${{ env.BRANCH_SOURCE }}"
95 |
96 | - name: Add changes
97 | if: env.HAS_CHANGES == 1
98 | run: |
99 | git config --local user.email "${{ secrets.GH_BOT_EMAIL }}"
100 | git config --local user.name "${{ secrets.GH_BOT_USERNAME }}"
101 | git add .github/workflows
102 | git commit -m "Update common workflows."
103 |
104 | - name: Push changes
105 | if: env.HAS_CHANGES == 1
106 | # https://github.com/marketplace/actions/github-push
107 | uses: ad-m/github-push-action@master
108 | with:
109 | github_token: ${{ secrets.GH_WORKFLOWS_TOKEN }}
110 | branch: ${{ env.BRANCH_SOURCE }}
111 |
112 | - name: Create PR
113 | if: env.HAS_CHANGES == 1 && env.AS_PR == 1
114 | # https://github.com/marketplace/actions/github-pull-request-action
115 | uses: repo-sync/pull-request@v2
116 | with:
117 | source_branch: "${{ env.BRANCH_SOURCE }}"
118 | destination_branch: "${{ github.ref }}"
119 | pr_title: "Update 'setup-workflows' workflow"
120 | pr_body: "Update 'setup-workflows' workflow."
121 | github_token: ${{ secrets.GITHUB_TOKEN }}
122 |
--------------------------------------------------------------------------------
/.github/workflows/stalebot.yml:
--------------------------------------------------------------------------------
1 | name: Close stale issues and pull requests
2 |
3 | on:
4 | schedule:
5 | - cron: "0 9 * * *"
6 |
7 | jobs:
8 | stale:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/stale@v3
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
14 | stale-issue-message: "It's been a while since we last heard from you. We are marking this issue as stale due to inactivity. Please provide the requested feedback or the issue will be closed after next 7 days."
15 | stale-pr-message: "It's been a while since we last heard from you. We are marking this pull request as stale due to inactivity. Please provide the requested feedback or the pull request will be closed after next 7 days."
16 | close-issue-message: "There was no activity regarding this issue in the last 14 days. We're closing it for now. Still, feel free to provide us with requested feedback so that we can reopen it."
17 | close-pr-message: "There was no activity regarding this pull request in the last 14 days. We're closing it for now. Still, feel free to provide us with requested feedback so that we can reopen it."
18 | stale-issue-label: 'stale'
19 | stale-pr-label: 'stale'
20 | exempt-issue-labels: 'status:confirmed,status:blocked'
21 | exempt-pr-labels: 'status:blocked'
22 | close-issue-label: 'resolution:expired'
23 | close-pr-label: 'pr:frozen ❄'
24 | remove-stale-when-updated: true
25 | days-before-issue-stale: 7
26 | days-before-pr-stale: 14
27 | days-before-close: 7
28 |
--------------------------------------------------------------------------------
/.github/workflows/test-all.yml:
--------------------------------------------------------------------------------
1 | name: Test all
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | - stable
8 | - major
9 | push:
10 | tags:
11 | - v*
12 | branches:
13 | - master
14 | - stable
15 | - major
16 |
17 | jobs:
18 | # Runs unit tests for all configured React version
19 | test-units:
20 | runs-on: ubuntu-20.04
21 | name: Run unit tests
22 | steps:
23 | - name: Checkout code
24 | uses: actions/checkout@v2
25 |
26 | - name: Setup node
27 | uses: actions/setup-node@v2
28 | with:
29 | node-version: 14
30 |
31 | - name: Setup Chrome
32 | uses: browser-actions/setup-chrome@latest
33 | with:
34 | chrome-version: stable
35 |
36 | # Set variables required for further steps.
37 | # CHROME_BIN is required by Karma.
38 | # REACT_VERSION is set to "current" for pull request events and "all" for other events.
39 | - name: Set test variables
40 | run: |
41 | export CHROME_BIN=$(which chrome);
42 | if [ -z ${GITHUB_HEAD_REF} ]; then echo "REACT_VERSION=all"; else echo "REACT_VERSION=current"; fi >> $GITHUB_ENV;
43 |
44 | - name: Install dependencies
45 | run: npm install
46 |
47 | # Run tests with the help of Xvfb, since there is no screen output available (required for locally installed browsers).
48 | - name: Run tests
49 | uses: GabrielBB/xvfb-action@v1
50 | env:
51 | CKEDITOR_LICENSE_KEY: ${{ secrets.CKEDITOR_LICENSE_KEY }}
52 | with:
53 | run: npm run test:units -- react ${{ env.REACT_VERSION }}
54 |
--------------------------------------------------------------------------------
/.github/workflows/update-deps.yml:
--------------------------------------------------------------------------------
1 | name: Update NPM dependencies
2 |
3 | on:
4 | schedule:
5 | - cron: "0 5 1,15 * *"
6 | workflow_dispatch:
7 | inputs:
8 | config:
9 | description: 'Config'
10 | required: false
11 | default: ''
12 |
13 | jobs:
14 | update:
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | deps: [production, dev-only]
20 |
21 | steps:
22 | - name: Setup node
23 | # https://github.com/marketplace/actions/setup-node-js-environment
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: '12'
27 |
28 | - name: Checkout default branch
29 | # https://github.com/marketplace/actions/checkout
30 | uses: actions/checkout@v2
31 |
32 | - name: Read config
33 | run: |
34 | npm i -g npm@7
35 | npm -v
36 | CONFIG='{}'
37 | if [[ ! -z '${{ github.event.inputs.config }}' ]]; then
38 | CONFIG='${{ github.event.inputs.config }}'
39 | elif [[ -f "./.github/workflows-config.json" ]]; then
40 | CONFIG=$( jq -c .updateDeps './.github/workflows-config.json' )
41 | fi
42 | echo "CONFIG=$CONFIG" >> $GITHUB_ENV
43 | echo "Workflow config: $CONFIG"
44 |
45 | - name: Check env variables
46 | run: |
47 | if [[ -z "${{ secrets.GH_BOT_USERNAME }}" ]]; then
48 | echo "Expected 'GH_BOT_USERNAME' secret variable to be set."
49 | exit 1
50 | fi
51 | if [[ -z "${{ secrets.GH_BOT_EMAIL }}" ]]; then
52 | echo "Expected 'GH_BOT_EMAIL' secret variable to be set."
53 | exit 1
54 | fi
55 | BRANCH_TARGET=$(echo '${{ env.CONFIG }}' | jq -r ".targetBranch")
56 | if [[ -z "$BRANCH_TARGET" || "$BRANCH_TARGET" == "null" ]]; then
57 | # Since 'Fetch changes' step fetches default branch, we just get branch name (which will be default one).
58 | BRANCH_TARGET=$(git rev-parse --abbrev-ref HEAD)
59 | fi
60 | echo "BRANCH_TARGET=$BRANCH_TARGET" >> $GITHUB_ENV
61 |
62 | - name: Check if update branch already exists
63 | run: |
64 | echo "BRANCH_UPDATE=deps-update_${{ env.BRANCH_TARGET }}_${{ matrix.deps }}" >> $GITHUB_ENV
65 | if [[ $(git ls-remote --heads | grep deps-update_${{ env.BRANCH_TARGET }}_${{ matrix.deps }} | wc -c) -ne 0 ]]; then
66 | echo "BRANCH_UPDATE=0" >> $GITHUB_ENV
67 | fi
68 |
69 | - name: Update NPM dependencies
70 | if: env.BRANCH_UPDATE != 0
71 | run: |
72 | npm i
73 | npm install -g npm-check
74 | git checkout -b ${{ env.BRANCH_UPDATE }}
75 | npm-check -y --${{ matrix.deps }}
76 |
77 | - name: Add changes
78 | if: env.BRANCH_UPDATE != 0
79 | run: |
80 | if [[ $(git diff origin/${{ env.BRANCH_TARGET }} | wc -c) -ne 0 ]]; then
81 | git config --local user.email "${{ secrets.GH_BOT_EMAIL }}"
82 | git config --local user.name "${{ secrets.GH_BOT_USERNAME }}"
83 | git add package*.json
84 | git commit -m "Update NPM ${{ matrix.deps }} dependencies."
85 | echo "HAS_CHANGES=1" >> $GITHUB_ENV
86 | fi
87 |
88 | - name: Push changes
89 | if: env.HAS_CHANGES == 1
90 | # https://github.com/marketplace/actions/github-push
91 | uses: ad-m/github-push-action@master
92 | with:
93 | github_token: ${{ secrets.GITHUB_TOKEN }}
94 | branch: ${{ env.BRANCH_UPDATE }}
95 |
96 | - name: Create PR
97 | if: env.HAS_CHANGES == 1
98 | # https://github.com/marketplace/actions/github-pull-request-action
99 | uses: repo-sync/pull-request@v2
100 | with:
101 | source_branch: "${{ env.BRANCH_UPDATE }}"
102 | destination_branch: "${{ env.BRANCH_TARGET }}"
103 | pr_title: "Update NPM ${{ matrix.deps }} dependencies"
104 | pr_body: "Updated NPM ${{ matrix.deps }} dependencies."
105 | github_token: ${{ secrets.GITHUB_TOKEN }}
106 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | .DS_Store
4 | dist/
5 | samples-out
6 | react-tests/
7 | *debug.log
8 | .vscode
9 | local.log
10 | public
11 | .tmp*
12 |
13 | # Ignore package-lock.json
14 | # - we don't intent to force specific 3rd party dependency version via `package-lock.json` file
15 | # Such information should be specified in the package.json file.
16 | package-lock.json
17 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | node
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CKEditor 4 WYSIWYG Editor React Integration Changelog
2 |
3 | ⚠️️️ **CKEditor 4 (the open source edition) is no longer maintained.** ⚠️
4 |
5 | If you would like to keep access to future CKEditor 4 security patches, check the [Extended Support Model](https://ckeditor.com/ckeditor-4-support/), which guarantees **security updates and critical bug fixes until December 2028**. Alternatively, [upgrade to CKEditor 5](https://ckeditor.com/docs/ckeditor5/latest/updating/ckeditor4/migration-from-ckeditor-4.html).
6 |
7 | ## ckeditor4-react 5.2.1
8 |
9 | Other Changes:
10 |
11 | * Updated default CDN CKEditor 4 dependency to [4.25.1-lts](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4251-lts).
12 | * Updated license headers to 2025.
13 | * Updated readme files to reflect the new CKEditor 4 Extended Support Model end date.
14 |
15 | Please note that this patch release doesn't provide any security fixes. It's a part of our administrative maintenance updates.
16 |
17 | ## ckeditor4-react 5.2.0
18 |
19 | ⚠️️️ CKEditor 4 CDN dependency has been upgraded to the latest secure version. All editor versions below 4.25.0-lts can no longer be considered as secure! ⚠️
20 |
21 | Other Changes:
22 |
23 | * Updated default CDN CKEditor 4 dependency to [4.25.0-lts](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4250-lts).
24 |
25 | ## ckeditor4-react 5.1.0
26 |
27 | ⚠️️️ CKEditor 4 CDN dependency has been upgraded to the latest secure version. All editor versions below 4.24.0-lts can no longer be considered as secure! ⚠️
28 |
29 | Other Changes:
30 |
31 | * Updated default CDN CKEditor 4 dependency to [4.24.0-lts](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4240-lts).
32 |
33 | ## ckeditor4-react 5.0.0
34 |
35 | This release introduces a support for the LTS (”Long Term Support”) version of the editor, available under commercial terms (["Extended Support Model"](https://ckeditor.com/ckeditor-4-support/)).
36 |
37 | If you acquired the Extended Support Model for CKEditor 4 LTS, please read [the CKEditor 4 LTS key activation guide.](https://ckeditor.com/docs/ckeditor4/latest/support/licensing/license-key-and-activation.html)
38 |
39 | Other Changes:
40 |
41 | * Updated default CDN CKEditor 4 dependency to [4.23.0-lts](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4230-lts).
42 |
43 | ## ckeditor4-react 4.3.0
44 |
45 | Other Changes:
46 |
47 | * Updated default CDN CKEditor 4 dependency to [4.22.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4220--4221).
48 |
49 | ## ckeditor4-react 4.2.0
50 |
51 | Other Changes:
52 |
53 | * Updated default CDN CKEditor 4 dependency to [4.21.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4210).
54 |
55 | ## ckeditor4-react 4.1.2
56 |
57 | Other Changes:
58 |
59 | * Updated default CDN CKEditor 4 dependency to [4.20.2](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4202).
60 |
61 | ## ckeditor4-react 4.1.1
62 |
63 | Other Changes:
64 |
65 | * Updated default CDN CKEditor 4 dependency to [4.20.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4201).
66 |
67 | ## ckeditor4-react 4.1.0
68 |
69 | Other Changes:
70 |
71 | * Updated default CDN CKEditor 4 dependency to [4.20](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-420).
72 |
73 | ## ckeditor4-react 4.0.0
74 |
75 | **Highlights**
76 |
77 | The v4.0.0 release introduces support for React v18. You can read more about these changes in the [React v18 release notes](https://github.com/facebook/react/blob/main/CHANGELOG.md#1800-march-29-2022).
78 |
79 | Due to significant changes in React v18, the integration with CKEditor 4 is no longer compatible with the previous versions of React. Please note that this version of React also drops support for Internet Explorer 11.
80 |
81 | If you don’t want to lose support for IE11 or you haven't moved to React v18 yet, make sure to use React integration in [version 3](#ckeditor4-react-310).
82 |
83 | See the [browser compatibility table](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_react_current.html#ckeditor-4-react-compatibility) to learn more about supported browsers and React versions.
84 |
85 | BREAKING CHANGES:
86 |
87 | * [#284](https://github.com/ckeditor/ckeditor4-react/issues/284): Add support for React 18 and remove support for older versions of React.
88 |
89 | Other Changes:
90 |
91 | * Updated default CDN CKEditor 4 dependency to [4.19.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4191).
92 |
93 | ## ckeditor4-react 3.1.0
94 |
95 | Other Changes:
96 |
97 | * Updated default CDN CKEditor 4 dependency to [4.19.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4190).
98 |
99 | ## ckeditor4-react 3.0.0
100 |
101 | Other Changes:
102 |
103 | * Updated default CDN CKEditor 4 dependency to [4.18.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4180).
104 |
105 | [Web Spell Checker](https://webspellchecker.com/) ended support for WebSpellChecker Dialog on December 31st, 2021. Therefore, this plugin has been deprecated and removed from the CKEditor 4.18.0 `standard-all` preset.
106 | We strongly encourage everyone to choose one of the other available spellchecking solutions - [Spell Check As You Type (SCAYT)](https://ckeditor.com/cke4/addon/scayt) or [WProofreader](https://ckeditor.com/cke4/addon/wproofreader).
107 |
108 | ## ckeditor4-react 2.1.1
109 |
110 | Other Changes:
111 |
112 | * Updated default CDN CKEditor 4 dependency to [4.17.2](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4172).
113 | * Updated year and company name in the license headers.
114 |
115 | ## ckeditor4-react 2.1.0
116 |
117 | Other Changes:
118 |
119 | * Updated default CDN CKEditor 4 dependency to [4.17.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4171).
120 |
121 | ## ckeditor4-react 2.0.1
122 |
123 | Other Changes:
124 |
125 | * Updated default CDN CKEditor 4 dependency to [4.16.2](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4162).
126 |
127 | ## ckeditor4-react 2.0.0
128 |
129 | New Features:
130 |
131 | * [#228](https://github.com/ckeditor/ckeditor4-react/issues/226): Added support for setting editor's initial data as HTML string.
132 |
133 | ## ckeditor4-react 2.0.0-rc.2
134 |
135 | BREAKING CHANGES:
136 |
137 | * [#226](https://github.com/ckeditor/ckeditor4-react/issues/226): Updated `ckeditor4-integrations-common` dependency to version `1.0.0`.
138 |
139 | ## ckeditor4-react 2.0.0-rc.1
140 |
141 | Other Changes:
142 |
143 | * Added CHANGELOG entries for RC versions.
144 | * Improved project README.
145 |
146 | ## ckeditor4-react 2.0.0-rc.0
147 |
148 | BREAKING CHANGES:
149 |
150 | * [#124](https://github.com/ckeditor/ckeditor4-react/issues/124): Introduced support for React hooks and rewrote the component to use hooks internally.
151 |
152 | New Features:
153 |
154 | * [#159](https://github.com/ckeditor/ckeditor4-react/issues/159): Introduced support for React 17+ versions.
155 | * [#82](https://github.com/ckeditor/ckeditor4-react/issues/82): Introduced TypeScript support.
156 | * [#180](https://github.com/ckeditor/ckeditor4-react/issues/180): Introduced support for consumption of a not bundled package version by providing package in ESM, CJS and UMD formats.
157 |
158 | ## ckeditor4-react 1.4.2
159 |
160 | Other Changes:
161 |
162 | * Updated default CDN CKEditor 4 dependency to [4.16.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4161).
163 |
164 | ## ckeditor4-react 1.4.1
165 |
166 | Fixed Issues:
167 |
168 | * [#114](https://github.com/ckeditor/ckeditor4-react/issues/114), [#127](https://github.com/ckeditor/ckeditor4-react/issues/127): Fixed: The editor uses initial stale data if `data` property changed before editor reaches [`instanceReady` state](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html#event-instanceReady).
169 |
170 | ## ckeditor4-react 1.4.0
171 |
172 | Other Changes:
173 |
174 | * Updated default CDN CKEditor 4 dependency to [4.16.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-416).
175 | * Updated [`ckeditor4-integrations-common`](https://www.npmjs.com/package/ckeditor4-integrations-common) package to `0.2.0` version.
176 | * Updated year in license headers.
177 |
178 | ## ckeditor4-react 1.3.0
179 |
180 | New Features:
181 |
182 | * [#125](https://github.com/ckeditor/ckeditor4-react/issues/125): Added `name` property for easier referencing editor instances with [`CKEDITOR.instances`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html#property-instances). Thanks to [Julien Castelain](https://github.com/julien)!
183 | * [#129](https://github.com/ckeditor/ckeditor4-react/issues/129): Added `onNamespaceLoaded` property executed when [`CKEDITOR` namespace](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html) is loaded, which can be used for its easier customization.
184 |
185 | ## ckeditor4-react 1.2.1
186 |
187 | Other Changes:
188 |
189 | * Updated the default CKEditor 4 CDN dependency to [4.15.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4151).
190 |
191 | ## ckeditor4-react 1.2.0
192 |
193 | Fixed Issues:
194 |
195 | * [#94](https://github.com/ckeditor/ckeditor4-react/issues/94): Fixed: The [`editor-incorrect-element`](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_errors.html#editor-incorrect-element) error is thrown due to `null` element reference when editor instance is destroyed before initialization completes. Thanks to [Christoph Dörfel](https://github.com/Garbanas)!
196 |
197 | Other Changes:
198 |
199 | * Updated the default CKEditor 4 CDN dependency to [4.15.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-415).
200 |
201 | ## ckeditor4-react 1.1.1
202 |
203 | Other Changes:
204 |
205 | * Updated the default CKEditor 4 CDN dependency to [4.14.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4141).
206 |
207 | ## ckeditor4-react 1.1.0
208 |
209 | Fixed Issues:
210 |
211 | * [#57](https://github.com/ckeditor/ckeditor4-react/issues/57): Fixed: The CKEditor 4 WYSIWYG editor React integration gives an [`editor-element-conflict` error](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_errors.html#editor-element-conflict).
212 |
213 | Other Changes:
214 |
215 | * Updated the default CKEditor 4 CDN dependency to [4.14.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-414).
216 |
217 | ## ckeditor4-react 1.0.1
218 |
219 | Other Changes:
220 |
221 | * Updated the default CKEditor 4 CDN dependency to [4.13.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4131).
222 |
223 | ## ckeditor4-react 1.0.0
224 |
225 | New Features:
226 |
227 | * [#15](https://github.com/ckeditor/ckeditor4-react/issues/15): Introduced support for Server Side Rendering.
228 |
229 | Fixed Issues:
230 |
231 | * [#46](https://github.com/ckeditor/ckeditor4-react/issues/46): Fixed: The React integration tries to destroy a non-existent editor instance in some cases. Thanks to [Oleg Kachmar](https://github.com/prokach)!
232 | * [#44](https://github.com/ckeditor/ckeditor4-react/issues/44): Fixed: An error thrown when changing routes quickly.
233 | * [#49](https://github.com/ckeditor/ckeditor4-react/issues/49): Fixed: A "Cannot read property 'getEditor' of null" error thrown when the component is unmounted.
234 | * [#56](https://github.com/ckeditor/ckeditor4-react/issues/56): Fixed: CKEditor crashes when unmounting with a "Cannot read property 'destroy' of null" error.
235 |
236 | Other Changes:
237 |
238 | * Updated the default CKEditor 4 CDN dependency to [`4.13.0`](https://github.com/ckeditor/ckeditor4-react/commit/7b34d2c4f896ced08e66359faca28194ed7e8ef4).
239 |
240 | ## ckeditor4-react 1.0.0-beta.2
241 |
242 | New Features:
243 |
244 | * [#47](https://github.com/ckeditor/ckeditor4-react/issues/47): Exposed the `CKEDITOR` namespace before loading the editor instance. Thanks to [Nick Rattermann](https://github.com/nratter)!
245 | * [#48](https://github.com/ckeditor/ckeditor4-react/pull/48): Added `CKEDITOR.displayName` for easy debugging and testing. Thanks to [Florent Berthelot](https://github.com/FBerthelot)!
246 |
247 | Other Changes:
248 |
249 | * Updated the default CKEditor 4 CDN dependency to [`4.12.1`](https://github.com/ckeditor/ckeditor4-react/commit/e72c2fb2d8e107419fe209c436c909915237a109).
250 |
251 | ## ckeditor4-react 1.0.0-beta
252 |
253 | Other Changes:
254 |
255 | * Updated the `LICENSE.md` file with all development and production dependencies.
256 |
257 | ## ckeditor4-react 0.1.1
258 |
259 | Other Changes:
260 |
261 | * Updated all CKEditor 4 dependencies to the `4.11.4` version.
262 | * `README.md` file improvements.
263 |
264 | ## ckeditor4-react 0.1.0
265 |
266 | The first beta release of the CKEditor 4 WYSIWYG Editor React Integration.
267 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ### Install dependencies
4 |
5 | After cloning this repository, install necessary dependencies:
6 |
7 | ```
8 | npm install
9 | ```
10 |
11 | ### Development
12 |
13 | ```
14 | npm run develop
15 | ```
16 |
17 | After running this command unit tests will start in watch mode and you are ready to develop. Please note that source code will be transpiled with `babel` so there is no type checking with TypeScript for this command. In order to validate types with TS run `npm run types:check` or run a full build (see below).
18 |
19 | By default Karma will run tests on Chrome so make sure that its binary is available on your system. If you have Chrome installed but Karma cannot find its binary for some reason, you might need to set `CHROME_BIN` environment variable, e.g. in your `.bashrc` file: `export CHROME_BIN=path_to_chrome_bin`.
20 |
21 | ### Build
22 |
23 | #### Watch mode
24 |
25 | Run the following command in order to start builds in watch mode:
26 |
27 | ```
28 | npm start
29 | ```
30 |
31 | This command emits bundles in following formats: `umd`, `esm`, `cjs`. Type declarations are not be emitted.
32 |
33 | #### Run build once
34 |
35 | Run `npm run build` command to run build once. TypeScript declarations are emitted.
36 |
37 | ### Tests
38 |
39 | #### Quick feedback
40 |
41 | As noted above, run `npm run develop` to quickly run a full suite of unit tests on currently installed React version.
42 |
43 | To run more a complete test script, just run the following command:
44 |
45 | ```
46 | npm test
47 | ```
48 |
49 | This command makes a single run of linter, type checker and unit tests.
50 |
51 | #### Full suite of unit tests
52 |
53 | In order to run unit tests on all supported React versions:
54 |
55 | ```
56 | npm run test:units:all
57 | ```
58 |
59 | #### Full suite of E2E tests
60 |
61 | E2E tests are to ensure that library _build files_ can be used with minimal setup on the consumer end and that they work on all supported browsers. E2E tests will run on examples inside `samples` directory. Tests will be run for each supported React version.
62 |
63 | All tests will run on BrowserStack, so make sure that environment variables `BROWSER_STACK_USERNAME` and `BROWSER_STACK_ACCESS_KEY` are set beforehand (values must be kept secret). In addition set `BROWSER_STACK_BROWSER` variable to one of the following values: `chrome`, `safari`, `ie`, `firefox`, `edge`.
64 |
65 | E2E tests are meant to run in CI environment but it's possible to run them locally as well. For example:
66 |
67 | ```
68 | BROWSER_STACK_USERNAME=name BROWSER_STACK_ACCESS_KEY=key BROWSER_STACK_BROWSER=safari npm run test:e2e:all
69 | ```
70 |
71 | Or for a single sample:
72 |
73 | ```
74 | BROWSER_STACK_USERNAME=name BROWSER_STACK_ACCESS_KEY=key BROWSER_STACK_BROWSER=safari npm run test:e2e:all -- --sample basic
75 | ```
76 |
77 | ### Samples
78 |
79 | Before running any sample locally, make sure to expose root package as a link and build the library:
80 |
81 | ```
82 | npm link && npm run build
83 | ```
84 |
85 | It's enough to `npm link` the root package once. Run build on every change in root `src` folder.
86 |
87 | Example apps are located in `samples` directory. Each sample is a self-contained application that has mandatory `start` and `build` scripts. In order to run a sample go thorugh the following steps:
88 |
89 | 1. `cd` into sample, e.g. `cd samples/basic`
90 | 2. Run `npm install`
91 | 3. Link root package: `npm link ckeditor4-react`
92 | 4. Start example: `npm start`
93 | 5. Navigate to `localhost:8080`
94 |
95 | It's important to re-run `npm link ckeditor4-react` in a sample folder anytime `npm install` operation was performed in that sample!
96 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Software License Agreement
2 | ==========================
3 |
4 | ## Software License Agreement for CKEditor 4 LTS React component (5.0 and above)
5 |
6 | **CKEditor 4 WYSIWYG editor component for React** – https://github.com/ckeditor/ckeditor4-react
7 | Copyright (c) 2003-2025, [CKSource](http://cksource.com) Holding sp. z o.o. All rights reserved.
8 |
9 | CKEditor 4 LTS ("Long Term Support") is available under exclusive terms of the [Extended Support Model](https://ckeditor.com/ckeditor-4-support/). [Contact us](https://ckeditor.com/contact/) to obtain a commercial license.
10 |
11 | ## Software License Agreement for CKEditor 4 React component 4.3.0 and below
12 |
13 | **CKEditor 4 WYSIWYG editor component for React** – https://github.com/ckeditor/ckeditor4-react
14 | Copyright (c) 2003-2025, [CKSource](http://cksource.com) Holding sp. z o.o. All rights reserved.
15 |
16 | Licensed under the terms of any of the following licenses at your
17 | choice:
18 |
19 | - GNU General Public License Version 2 or later (the "GPL")
20 | http://www.gnu.org/licenses/gpl.html
21 |
22 | - GNU Lesser General Public License Version 2.1 or later (the "LGPL")
23 | http://www.gnu.org/licenses/lgpl.html
24 |
25 | - Mozilla Public License Version 1.1 or later (the "MPL")
26 | http://www.mozilla.org/MPL/MPL-1.1.html
27 |
28 | Sources of Intellectual Property Included in CKEditor
29 | -----------------------------------------------------
30 |
31 | Where not otherwise indicated, all CKEditor content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource with their express permission.
32 |
33 | The following libraries are included in CKEditor 4 WYSIWYG editor component for React under the [MIT license](https://opensource.org/licenses/MIT):
34 | - load-script
35 | - prop-types (c) 2013-present, Facebook, Inc.
36 |
37 | The following libraries are included in CKEditor 4 WYSIWYG editor component for React under the [0BSD license](https://opensource.org/licenses/0BSD):
38 | - tslib (c) Microsoft Corporation
39 |
40 | Trademarks
41 | ----------
42 |
43 | **CKEditor** is a trademark of [CKSource](http://cksource.com) Holding sp. z o.o. All other brand and product names are trademarks, registered trademarks or service marks of their respective holders.
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CKEditor 4 WYSIWYG editor component for React [](https://twitter.com/intent/tweet?text=Check%20out%20CKEditor%204%20React%20integration&url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fckeditor4-react)
2 |
3 | [](https://www.npmjs.com/package/ckeditor4-react)
4 | [](https://github.com/ckeditor/ckeditor4-react)
5 |
6 | 
7 |
8 | [](http://eepurl.com/c3zRPr)
9 | [](https://twitter.com/ckeditor)
10 |
11 | ## ⚠️ CKEditor 4: End of Life and Extended Support Model until Dec 2028
12 |
13 | CKEditor 4 was launched in 2012 and reached its End of Life (EOL) on June 30, 2023.
14 |
15 | A special edition, **[CKEditor 4 LTS](https://ckeditor.com/ckeditor-4-support/)** ("Long Term Support"), is available under commercial terms (["Extended Support Model"](https://ckeditor.com/ckeditor-4-support/)) for anyone looking to **extend the coverage of security updates and critical bug fixes**.
16 |
17 | With CKEditor 4 LTS, security updates and critical bug fixes are guaranteed until December 2028.
18 |
19 | ## About this repository
20 |
21 | ### Master branch = CKEditor 4 LTS React Component
22 |
23 | After June 30, 2023 the `master` version of the [LICENSE.md](https://github.com/ckeditor/ckeditor4/blob/master/LICENSE.md) file changed to reflect the license of CKEditor 4 LTS available under the Extended Support Model.
24 |
25 | This repository now contains the source code of CKEditor 4 LTS React Component that is protected by copyright law.
26 |
27 | ### Getting CKEditor 4 (Open Source)
28 |
29 | You may continue using CKEditor React Component 4.3.0 and below under the open source license terms. Please note, however, that the open source version no longer comes with any security updates, so your application will be at risk.
30 |
31 | In order to download the open source version of CKEditor 4 React Component, use ****tags 4.3.0 and below****. CKEditor React Component 4.3.0 was the last version available under the open source license terms.
32 |
33 | ## About this package
34 |
35 | Official [CKEditor 4](https://ckeditor.com/ckeditor-4/) WYSIWYG editor component for React.
36 |
37 | We are looking forward to your feedback! You can report any issues, ideas or feature requests on the [integration issues page](https://github.com/ckeditor/ckeditor4-react/issues/new).
38 |
39 | 
40 |
41 | ## Usage
42 |
43 | ```jsx
44 | import React from 'react';
45 | import { CKEditor } from 'ckeditor4-react';
46 |
47 | function App() {
48 | return ;
49 | }
50 |
51 | export default App;
52 | ```
53 |
54 | ## Documentation and examples
55 |
56 | See the [CKEditor 4 WYSIWYG Editor React Integration](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_react_current.html) article in the [CKEditor 4 documentation](https://ckeditor.com/docs/ckeditor4/latest).
57 |
58 | You can also check out [CKEditor 4 WYSIWYG Editor React Integration example](https://ckeditor.com/docs/ckeditor4/latest/examples/react.html) in [CKEditor 4 Examples](https://ckeditor.com/docs/ckeditor4/latest/examples/).
59 |
60 | For even more examples, check out ready-to-fork samples inside [samples](samples) directory. Each sample is a self-contained app that can be [forked via GitHub](https://docs.github.com/en/github/getting-started-with-github/splitting-a-subfolder-out-into-a-new-repository) or via services such as [CodeSandbox](https://codesandbox.io/). For instance, in order to clone `basic` sample, use [this link](https://githubbox.com/ckeditor/ckeditor4-react/tree/master/samples/basic).
61 |
62 | ## React support
63 |
64 | The CKEditor 4 React integration was tested with React 18.
65 |
66 | ## TypeScript support
67 |
68 | TypeScript 3.5+ is supported.
69 |
70 | ## Browser support
71 |
72 | The CKEditor 4 React integration works with all the [supported browsers](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_browsers.html#officially-supported-browsers) except for Internet Explorer.
73 |
74 | Previous versions of `ckeditor4-react` also support Internet Explorer 11 (requires additional polyfill for `Promise`).
75 |
76 | ## Contributing
77 |
78 | See [CONTRIBUTING.md](CONTRIBUTING.md).
79 |
80 | ## License
81 |
82 | Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
83 |
84 | For full details about the license, please check the `LICENSE.md` file.
85 |
86 | ### CKEditor 4 React Component 4.3.0 and below for CKEditor 4 Open Source
87 |
88 | Licensed under the terms of any of the following licenses at your
89 | choice:
90 |
91 | * [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html),
92 | * [GNU Lesser General Public License Version 2.1 or later](http://www.gnu.org/licenses/lgpl.html),
93 | * [Mozilla Public License Version 1.1 or later (the "MPL")](http://www.mozilla.org/MPL/MPL-1.1.html).
94 |
95 | ### CKEditor 4 React Component 5.0 and above for CKEditor 4 LTS ("Long Term Support")
96 |
97 | CKEditor 4 LTS React Component (starting from version 5.0) is available under a commercial license only.
98 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* eslint-disable @typescript-eslint/no-var-requires */
3 |
4 | const babel = require( '@rollup/plugin-babel' ).babel;
5 | const nodeResolve = require( '@rollup/plugin-node-resolve' ).nodeResolve;
6 | const replace = require( '@rollup/plugin-replace' );
7 | const commonJs = require( '@rollup/plugin-commonjs' );
8 | const polyfillNode = require( 'rollup-plugin-polyfill-node' );
9 | const progress = require( 'rollup-plugin-progress' );
10 |
11 | module.exports = function( config ) {
12 | config.set( {
13 | browsers: [ 'Chrome' ],
14 |
15 | frameworks: [ 'jasmine' ],
16 |
17 | files: [
18 | // Uses single point of entry for improved build performance.
19 | { pattern: 'tests/unit/index.ts', watched: false }
20 | ],
21 |
22 | client: {
23 | jasmine: {
24 | random: false
25 | },
26 | args: [
27 | process.env.CKEDITOR_LICENSE_KEY
28 | ]
29 | },
30 |
31 | preprocessors: {
32 | 'tests/unit/index.ts': [ 'rollup' ]
33 | },
34 |
35 | reporters: [ 'mocha' ],
36 |
37 | rollupPreprocessor: {
38 | output: {
39 | format: 'iife',
40 | name: 'CKEditor4React'
41 | },
42 | watch: {
43 | skipWrite: true
44 | },
45 | plugins: [
46 | !config.silentBuildLogs && progress(),
47 | babel( {
48 | babelHelpers: 'bundled',
49 | presets: [
50 | '@babel/preset-env',
51 | '@babel/preset-typescript',
52 | '@babel/preset-react'
53 | ],
54 | extensions: [ '.ts', '.tsx', '.js' ],
55 | exclude: 'node_modules/**'
56 | } ),
57 | nodeResolve( {
58 | preferBuiltins: false,
59 | extensions: [ '.ts', '.tsx', '.js' ]
60 | } ),
61 | commonJs(),
62 | polyfillNode(),
63 | replace( {
64 | preventAssignment: true,
65 | values: {
66 | 'process.env.REQUESTED_REACT_VERSION': `"${
67 | process.env.REQUESTED_REACT_VERSION || ''
68 | }"`,
69 | 'process.env.NODE_ENV': '"test"'
70 | }
71 | } )
72 | ].filter( Boolean ),
73 | onwarn( warning, rollupWarn ) {
74 | if (
75 | // Reduces noise for circular deps.
76 | // https://github.com/rollup/rollup/issues/2271
77 | warning.code !== 'CIRCULAR_DEPENDENCY' &&
78 | // Prevents namespace warning when bundling RTL.
79 | // https://github.com/testing-library/react-testing-library/issues/790
80 | warning.code !== 'NAMESPACE_CONFLICT'
81 | ) {
82 | rollupWarn( warning );
83 | }
84 | }
85 | }
86 | } );
87 | };
88 |
--------------------------------------------------------------------------------
/nightwatch.conf.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | /**
4 | * !!! Important !!!
5 | *
6 | * Nightwatch for BrowserStack is supposed to be run via `scripts/nightwatch-runner.js`.
7 | */
8 |
9 | // The following environment variables must be set (e.g. in .bashrc or via CLI) before running `nightwatch-runner`.
10 | const bsUser = process.env.BROWSER_STACK_USERNAME;
11 | const bsKey = process.env.BROWSER_STACK_ACCESS_KEY;
12 | const bsBrowser = process.env.BROWSER_STACK_BROWSER;
13 |
14 | // The following environment variables should be set before running `nightwatch-runner` but are optional.
15 | const bsBuildName = process.env.BROWSER_STACK_BUILD_NAME;
16 |
17 | // The following variables will be set via `nightwatch-runner` script.
18 | // It's important that `localIdentifier` passed to `browserstack-local` and to Nightwatch config match.
19 | const bsLocalIdentifier = process.env.BROWSER_STACK_LOCAL_IDENTIFIER;
20 |
21 | const browsers = {
22 | chrome: {
23 | os: 'Windows',
24 | os_version: '10'
25 | },
26 | edge: {
27 | os: 'Windows',
28 | os_version: '10'
29 | },
30 | firefox: {
31 | os: 'Windows',
32 | os_version: '10'
33 | },
34 | ie: {
35 | os: 'Windows',
36 | os_version: '10'
37 | },
38 | safari: {
39 | os: 'OS X',
40 | os_version: 'Catalina'
41 | }
42 | };
43 |
44 | const nightwatchConfig = {
45 | selenium: {
46 | start_process: false,
47 | host: 'hub-cloud.browserstack.com',
48 | port: 443
49 | },
50 |
51 | output_folder: '.tmp-e2e-output',
52 |
53 | test_settings: {
54 | default: {
55 | desiredCapabilities: {
56 | build: bsBuildName,
57 | 'browserstack.user': bsUser,
58 | 'browserstack.key': bsKey,
59 | 'browserstack.debug': true,
60 | 'browserstack.local': true,
61 | 'browserstack.localIdentifier': bsLocalIdentifier,
62 | browser: bsBrowser,
63 | version: 'latest',
64 | ...browsers[ bsBrowser ]
65 | }
66 | }
67 | }
68 | };
69 |
70 | for ( const i in nightwatchConfig.test_settings ) {
71 | const config = nightwatchConfig.test_settings[ i ];
72 | config.selenium_host = nightwatchConfig.selenium.host;
73 | config.selenium_port = nightwatchConfig.selenium.port;
74 | }
75 |
76 | module.exports = nightwatchConfig;
77 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ckeditor4-react",
3 | "version": "5.2.1",
4 | "description": "Official React component for CKEditor 4 – the best browser-based rich text editor.",
5 | "license": "SEE LICENSE IN LICENSE.md",
6 | "main": "dist/index.cjs.js",
7 | "module": "dist/index.esm.js",
8 | "types": "dist/index.d.ts",
9 | "files": [
10 | "dist/"
11 | ],
12 | "scripts": {
13 | "build": "rollup -c && npm run types:emit",
14 | "bump": "node ./scripts/bump.js",
15 | "develop": "karma start",
16 | "lint": "eslint --ext .js,.ts,.tsx src tests scripts",
17 | "postversion": "git rm -r --cached dist/ && git commit -m \"Clean after release [ci skip]\" && git push origin && git push origin --tags",
18 | "preversion": "npm test",
19 | "start": "rollup -c -w",
20 | "test:e2e:all": "npm run test:e2e -- --react all",
21 | "test:e2e": "node ./scripts/e2e-runner.js",
22 | "test:units:all": "npm run test:units -- --react all",
23 | "test:units": "node ./scripts/units-runner.js",
24 | "test": "npm run lint && npm run types:check && npm run test:units -- --silent-build-logs=true",
25 | "types:check": "tsc --noEmit",
26 | "types:emit": "tsc src/index.ts --jsx react --emitDeclarationOnly --declaration --declarationDir dist",
27 | "version": "npm run build && git add -f dist/"
28 | },
29 | "repository": {
30 | "type": "git",
31 | "url": "git+https://github.com/ckeditor/ckeditor4-react.git"
32 | },
33 | "keywords": [
34 | "wysiwyg",
35 | "rich text",
36 | "editor",
37 | "html",
38 | "contentEditable",
39 | "editing",
40 | "react",
41 | "react-component",
42 | "react-hooks",
43 | "react hooks",
44 | "ckeditor",
45 | "ckeditor4",
46 | "ckeditor 4"
47 | ],
48 | "author": "CKSource (http://cksource.com/)",
49 | "bugs": {
50 | "url": "https://github.com/ckeditor/ckeditor4-react/issues"
51 | },
52 | "homepage": "https://github.com/ckeditor/ckeditor4-react#readme",
53 | "peerDependencies": {
54 | "ckeditor4": "^4.25.1",
55 | "react": "^18"
56 | },
57 | "devDependencies": {
58 | "@babel/core": "^7.20.12",
59 | "@babel/preset-env": "^7.20.2",
60 | "@babel/preset-react": "^7.18.6",
61 | "@babel/preset-typescript": "^7.18.6",
62 | "@rollup/plugin-babel": "^5.3.1",
63 | "@rollup/plugin-commonjs": "^22.0.2",
64 | "@rollup/plugin-node-resolve": "^13.3.0",
65 | "@rollup/plugin-replace": "^4.0.0",
66 | "@rollup/plugin-typescript": "^8.5.0",
67 | "@testing-library/jasmine-dom": "^1.3.3",
68 | "@testing-library/react": "^13.4.0",
69 | "@types/jasmine": "^4.3.1",
70 | "@types/prop-types": "^15.7.5",
71 | "@types/react": "^18.0.28",
72 | "@typescript-eslint/eslint-plugin": "^5.51.0",
73 | "@typescript-eslint/parser": "^5.52.0",
74 | "browserstack-local": "^1.5.1",
75 | "chalk": "^4.1.2",
76 | "ckeditor4": "^4.25.1",
77 | "eslint": "^8.34.0",
78 | "eslint-config-ckeditor5": "^4.1.1",
79 | "eslint-plugin-react": "^7.32.2",
80 | "eslint-plugin-react-hooks": "^4.6.0",
81 | "jasmine": "^4.5.0",
82 | "karma": "^6.4.1",
83 | "karma-babel-preprocessor": "^8.0.2",
84 | "karma-chrome-launcher": "^3.1.1",
85 | "karma-jasmine": "^5.1.0",
86 | "karma-mocha-reporter": "^2.2.5",
87 | "karma-rollup-preprocessor": "^7.0.8",
88 | "minimist": "^1.2.8",
89 | "nightwatch": "2.0.10",
90 | "react": "^18.2.0",
91 | "react-dom": "^18.2.0",
92 | "rollup": "^2.79.1",
93 | "rollup-plugin-cleanup": "^3.2.1",
94 | "rollup-plugin-polyfill-node": "^0.10.2",
95 | "rollup-plugin-progress": "^1.1.2",
96 | "rollup-plugin-terser": "^7.0.2",
97 | "semver": "^7.3.8",
98 | "shelljs": "^0.8.5",
99 | "tree-kill": "^1.2.2",
100 | "tslib": "^2.5.0",
101 | "typescript": "^4.9.5"
102 | },
103 | "dependencies": {
104 | "ckeditor4-integrations-common": "^1.0.0",
105 | "prop-types": "^15.8.1"
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | import commonJs from '@rollup/plugin-commonjs';
4 | import typescript from '@rollup/plugin-typescript';
5 | import { nodeResolve } from '@rollup/plugin-node-resolve';
6 | import replace from '@rollup/plugin-replace';
7 | import { terser } from 'rollup-plugin-terser';
8 | import cleanup from 'rollup-plugin-cleanup';
9 | import pkg from './package.json';
10 |
11 | const input = 'src/index.ts';
12 | const external = Object.keys( pkg.peerDependencies || {} );
13 | const banner = `/**
14 | * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
15 | * For licensing, see LICENSE.md.
16 | */`;
17 |
18 | export default [
19 | // Creates `umd` build that can be directly consumed via tag (development mode).
20 | // Setting `NODE_ENV` value to `development` enables dev utils such as prop-types.
21 | {
22 | input,
23 | external,
24 | output: {
25 | banner,
26 | format: 'umd',
27 | file: 'dist/index.umd.development.js',
28 | name: 'CKEditor4React',
29 | globals: {
30 | react: 'React'
31 | }
32 | },
33 | plugins: [
34 | typescript(),
35 | nodeResolve(),
36 | commonJs(),
37 | replace( {
38 | preventAssignment: true,
39 | values: {
40 | 'process.env.NODE_ENV': '"development"'
41 | }
42 | } ),
43 | cleanupPlugin()
44 | ]
45 | },
46 | // Creates `umd` build that can be directly consumed via tag (production mode).
47 | // Setting `NODE_ENV` value to `production` disables dev utils.
48 | {
49 | input,
50 | external,
51 | output: {
52 | banner,
53 | format: 'umd',
54 | file: 'dist/index.umd.production.min.js',
55 | name: 'CKEditor4React',
56 | globals: {
57 | react: 'React'
58 | }
59 | },
60 | plugins: [
61 | typescript(),
62 | nodeResolve(),
63 | commonJs(),
64 | replace( {
65 | preventAssignment: true,
66 | values: {
67 | 'process.env.NODE_ENV': '"production"'
68 | }
69 | } ),
70 | cleanupPlugin(),
71 | terser()
72 | ]
73 | },
74 | // Creates `cjs` build that can be further optimized downstream.
75 | {
76 | input,
77 | external: external.concat( [
78 | 'ckeditor4-integrations-common',
79 | 'prop-types'
80 | ] ),
81 | output: {
82 | banner,
83 | format: 'cjs',
84 | file: 'dist/index.cjs.js'
85 | },
86 | plugins: [ typescript(), cleanupPlugin() ]
87 | },
88 | // Creates `esm` build that can be further optimized downstream.
89 | {
90 | input,
91 | external: external.concat( [
92 | 'ckeditor4-integrations-common',
93 | 'prop-types'
94 | ] ),
95 | output: {
96 | banner,
97 | format: 'esm',
98 | file: 'dist/index.esm.js'
99 | },
100 | plugins: [ typescript(), cleanupPlugin() ]
101 | }
102 | ];
103 |
104 | /**
105 | * Prepares `cleanup` plugin to remove unnecessary 3rd party licenses, e.g.
106 | *
107 | * - `tslib` 0BSD license (3rd parties are not required to include it)
108 | * - CKSource licenses from dependencies
109 | *
110 | * https://github.com/microsoft/tslib/blob/master/LICENSE.txt
111 | * https://github.com/microsoft/tslib/issues/47
112 | *
113 | * @returns {Plugin} configured plugin
114 | */
115 | function cleanupPlugin() {
116 | return cleanup( {
117 | extensions: [ 'ts', 'tsx', 'js' ],
118 | comments: [
119 | /Copyright (c) Microsoft Corporation./,
120 | /@license Copyright (c) 2003-2025, CKSource Holding sp. z o.o./
121 | ]
122 | } );
123 | }
124 |
--------------------------------------------------------------------------------
/samples/README.md:
--------------------------------------------------------------------------------
1 | # Samples
2 |
3 | Each sample is a self-contained app.
4 |
5 | ## Running samples
6 |
7 | To run any sample you need to perform basic setup steps:
8 |
9 | ```sh
10 | cd samples/sample_name
11 | npm i
12 | npm run build
13 | npm run start
14 | ```
15 |
16 | The sample will be available under `localhost:8080`.
17 |
18 | You may also use forked samples linked below.
19 |
20 | ## Available samples
21 |
22 | | Sample | Description | Sample |
23 | | ------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
24 | | [basic](basic) | Basic showcase of `CKEditor` component. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/basic) |
25 | | [basic-typescript](basic-typecript) | Basic showcase of `CKEditor` component with example webpack and TypeScript integration. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/basic-typescript) |
26 | | [component](component) | Advanced showcase of `CKEditor` component. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/component) |
27 | | [component-events](component-events) | Showcase of using editor events with `CKEditor` component. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/component-events) |
28 | | [editor-url](editor-url) | Example usage of custom `editorUrl`. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/editor-url) |
29 | | [hook](hook) | Showcase of `useCKEditor` hook. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/hook) |
30 | | [hook-events](hook-events) | Showcase of using editor events with `useCKEditor` hook. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/hook-events) |
31 | | [router](router) | Example of using `CKEditor` along with `react-router`. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/router) |
32 | | [re-order](re-order) | Example of handling multiple instances of `CKEditor` component (re-ordering case). | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/re-order) |
33 | | [ssr](ssr) | Showcase of using `CKEditor` with React server-side rendering. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/ssr) |
34 | | [state-lifting](state-lifting) | Editor's state is lifted higher up React tree to establish connection with a custom `textarea`. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/state-lifting) |
35 | | [umd](umd) | Showcase of how `umd` build of `ckeditor4-react` package can be used. | [Link](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/umd) |
36 |
--------------------------------------------------------------------------------
/samples/basic-typescript/README.md:
--------------------------------------------------------------------------------
1 | # Basic example with TypeScript
2 |
3 | Basic showcase of `CKEditor` component with example webpack and TypeScript integration.
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/basic-typescript).
6 |
--------------------------------------------------------------------------------
/samples/basic-typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4-react": "latest",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.18.6",
19 | "@babel/preset-env": "^7.18.6",
20 | "@types/react": "^18.0.15",
21 | "@types/react-dom": "^18.0.6",
22 | "babel-loader": "^8.2.5",
23 | "html-webpack-plugin": "^5.5.0",
24 | "ts-loader": "^9.3.1",
25 | "typescript": "^4.7.4",
26 | "webpack": "^5.73.0",
27 | "webpack-cli": "^4.10.0",
28 | "webpack-dev-server": "^4.9.3"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/samples/basic-typescript/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { CKEditor } from 'ckeditor4-react';
3 |
4 | function App() {
5 | return (
6 |
7 |
8 | { /* Remember to add the license key to the CKEditor 4 configuration:
9 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default App;
18 |
--------------------------------------------------------------------------------
/samples/basic-typescript/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/samples/basic-typescript/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | if ( !element ) {
8 | throw new Error( 'Missing root element' );
9 | }
10 |
11 | createRoot( element ).render(
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/samples/basic-typescript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "es6",
4 | "target": "es5",
5 | "moduleResolution": "node",
6 | "strict": true,
7 | "jsx": "react"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/samples/basic-typescript/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
5 |
6 | module.exports = {
7 | entry: './src/index.tsx',
8 | mode: 'development',
9 | devtool: 'source-map',
10 | output: {
11 | path: path.resolve( __dirname, './public' ),
12 | filename: 'bundle.js'
13 | },
14 | resolve: {
15 | extensions: [ '.js', '.ts', '.tsx' ],
16 | alias: {
17 | react$: path.resolve( __dirname, './node_modules/react' ),
18 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
19 | }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.js$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [ '@babel/preset-env' ]
30 | }
31 | }
32 | },
33 | {
34 | test: /\.tsx?$/,
35 | use: 'ts-loader',
36 | exclude: /node_modules/
37 | }
38 | ]
39 | },
40 | plugins: [
41 | new HtmlWebpackPlugin( {
42 | title: 'Example',
43 | template: 'src/index.html'
44 | } )
45 | ]
46 | };
47 |
--------------------------------------------------------------------------------
/samples/basic/README.md:
--------------------------------------------------------------------------------
1 | # Basic example
2 |
3 | This is a basic showcase of `CKEditor4` component.
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/basic).
6 |
--------------------------------------------------------------------------------
/samples/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4-react": "latest",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.18.6",
19 | "@babel/preset-env": "^7.18.6",
20 | "@babel/preset-react": "^7.18.6",
21 | "babel-loader": "^8.2.5",
22 | "html-webpack-plugin": "^5.5.0",
23 | "webpack": "^5.73.0",
24 | "webpack-cli": "^4.10.0",
25 | "webpack-dev-server": "^4.9.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/basic/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CKEditor } from 'ckeditor4-react';
3 |
4 | /**
5 | * `initData` can be either string with markup or JSX:
6 | *
7 | *
8 | *
9 | * Or:
10 | *
11 | * Hello world!} />
12 | *
13 | */
14 | function App() {
15 | return (
16 |
17 |
18 | { /* Remember to add the license key to the CKEditor 4 configuration:
19 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
20 |
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default App;
31 |
--------------------------------------------------------------------------------
/samples/basic/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/samples/basic/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/basic/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
5 |
6 | module.exports = {
7 | entry: './src/index.jsx',
8 | mode: 'development',
9 | devtool: 'source-map',
10 | output: {
11 | path: path.resolve( __dirname, './public' ),
12 | filename: 'bundle.js'
13 | },
14 | resolve: {
15 | extensions: [ '.js', '.jsx' ],
16 | alias: {
17 | react$: path.resolve( __dirname, './node_modules/react' ),
18 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
19 | }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
30 | }
31 | }
32 | }
33 | ]
34 | },
35 | plugins: [
36 | new HtmlWebpackPlugin( {
37 | title: 'Example',
38 | template: 'src/index.html'
39 | } )
40 | ]
41 | };
42 |
--------------------------------------------------------------------------------
/samples/component-events/README.md:
--------------------------------------------------------------------------------
1 | # Editor events with `CKEditor` component
2 |
3 | Showcase of using editor events when using `CKEditor` component.
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/component-events).
6 |
--------------------------------------------------------------------------------
/samples/component-events/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4-react": "latest",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.18.6",
19 | "@babel/preset-env": "^7.18.6",
20 | "@babel/preset-react": "^7.18.6",
21 | "babel-loader": "^8.2.5",
22 | "html-webpack-plugin": "^5.5.0",
23 | "webpack": "^5.73.0",
24 | "webpack-cli": "^4.10.0",
25 | "webpack-dev-server": "^4.9.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/component-events/src/App.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Sidebar from './Sidebar';
3 | import CKEditor from './CKEditor';
4 |
5 | const { version, useRef, useState } = React;
6 |
7 | function App() {
8 | const [ events, setEvents ] = useState( [] );
9 | const [ uniqueName, setUniqueName ] = useState( getUniqueName() );
10 | const start = useRef( new Date() );
11 |
12 | const handleRemountClick = () => {
13 | setUniqueName( getUniqueName() );
14 | };
15 |
16 | const handleCustomEvent = () => {
17 | window.CKEDITOR.instances[ uniqueName ].fire( 'myCustomEvent' );
18 | };
19 |
20 | const pushEvent = ( evtName, editorName ) => {
21 | setEvents( events =>
22 | events.concat( {
23 | evtName,
24 | editor: editorName,
25 | date: new Date()
26 | } )
27 | );
28 | };
29 |
30 | return (
31 |
32 |
33 |
34 |
35 |
40 |
43 |
44 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
55 | function getUniqueName() {
56 | return Math.random()
57 | .toString( 36 )
58 | .replace( /[^a-z]+/g, '' )
59 | .substr( 0, 5 );
60 | }
61 |
62 | export default App;
63 |
--------------------------------------------------------------------------------
/samples/component-events/src/CKEditor.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import * as React from 'react';
4 | import { CKEditor } from 'ckeditor4-react';
5 |
6 | /**
7 | * `CKEditor` component accepts handlers for all editor events, e.g. `instanceReady` -> `onInstanceReady`.
8 | *
9 | * `CKEditor` component ensures referential equality between renders for event handlers.
10 | * This means that first value of an event handler will be memoized and used through the lifecycle of `CKEditor` component.
11 | */
12 | function CKEditorCmp( { pushEvent, uniqueName } ) {
13 | const handleBeforeLoad = () => {
14 | pushEvent( 'beforeLoad', '--' );
15 | };
16 |
17 | const handleDestroyed = () => {
18 | pushEvent( 'destroy', uniqueName );
19 | };
20 |
21 | const handleInstanceReady = () => {
22 | pushEvent( 'instanceReady', uniqueName );
23 | };
24 |
25 | const handleLoaded = () => {
26 | pushEvent( 'loaded', uniqueName );
27 | };
28 |
29 | const handleNamespaceLoaded = () => {
30 | pushEvent( 'namespaceLoaded', '--' );
31 | };
32 |
33 | const handleBlur = () => {
34 | pushEvent( 'blur', uniqueName );
35 | };
36 |
37 | const handleFocus = () => {
38 | pushEvent( 'focus', uniqueName );
39 | };
40 |
41 | const handleCustomEvent = () => {
42 | pushEvent( 'myCustomEvent', uniqueName );
43 | };
44 |
45 | return (
46 |
47 | { /* Remember to add the license key to the CKEditor 4 configuration:
48 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
49 |
61 |
62 | );
63 | }
64 |
65 | export default CKEditorCmp;
66 |
--------------------------------------------------------------------------------
/samples/component-events/src/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import * as React from 'react';
4 |
5 | function Sidebar( { events, start } ) {
6 | const content =
7 | events.length > 0 ? (
8 |
9 |
10 |
11 | {'Event'} |
12 | {'Editor'} |
13 | {'Elapsed [ms]'} |
14 |
15 |
16 |
17 | {[ ...events ].reverse().map( ( { evtName, editor, date }, i ) => (
18 |
19 | {evtName} |
20 | {editor} |
21 | {date.getTime() - start.getTime()} |
22 |
23 | ) )}
24 |
25 |
26 | ) : (
27 | {'No events yet!'}
28 | );
29 |
30 | return ;
31 | }
32 |
33 | export default Sidebar;
34 |
--------------------------------------------------------------------------------
/samples/component-events/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/samples/component-events/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/component-events/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
5 |
6 | module.exports = {
7 | entry: './src/index.jsx',
8 | mode: 'development',
9 | devtool: 'source-map',
10 | output: {
11 | path: path.resolve( __dirname, './public' ),
12 | filename: 'bundle.js'
13 | },
14 | resolve: {
15 | extensions: [ '.js', '.jsx' ],
16 | alias: {
17 | react$: path.resolve( __dirname, './node_modules/react' ),
18 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
19 | }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
30 | }
31 | }
32 | }
33 | ]
34 | },
35 | plugins: [
36 | new HtmlWebpackPlugin( {
37 | title: 'Example',
38 | template: 'src/index.html'
39 | } )
40 | ]
41 | };
42 |
--------------------------------------------------------------------------------
/samples/component/README.md:
--------------------------------------------------------------------------------
1 | # Editor as `CKEditor` component
2 |
3 | Advanced showcase of `CKEditor` component.
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/component).
6 |
--------------------------------------------------------------------------------
/samples/component/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4-react": "latest",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.18.6",
19 | "@babel/preset-env": "^7.18.6",
20 | "@babel/preset-react": "^7.18.6",
21 | "babel-loader": "^8.2.5",
22 | "html-webpack-plugin": "^5.5.0",
23 | "webpack": "^5.73.0",
24 | "webpack-cli": "^4.10.0",
25 | "webpack-dev-server": "^4.9.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/component/src/App.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { CKEditor } from 'ckeditor4-react';
3 | import Sidebar from './Sidebar';
4 |
5 | const { version, useReducer } = React;
6 |
7 | /**
8 | * `App` component manages state of underlying `CKEditor` and `Sidebar` components.
9 | *
10 | * `CKEditor` component memoizes certain props and it will ignore any new values. For instance, this is true for `config` and `type.
11 | * In order to force new `config` or `type` values, use keyed component.
12 | * This way `CKEditor` component is re-mounted and new instance of editor is created.
13 | */
14 | function App() {
15 | const [ { config, readOnly, type, style, toolbar, name }, dispatch ] =
16 | useReducer( reducer, {
17 | config: getConfig(),
18 | readOnly: false,
19 | type: 'classic',
20 | style: 'initial',
21 | toolbar: 'standard',
22 | name: getUniqueName()
23 | } );
24 |
25 | const handleToolbarChange = evt => {
26 | const value = evt.currentTarget.value;
27 | dispatch( { type: 'toolbar', payload: value } );
28 | };
29 |
30 | const handleReadOnlyChange = evt => {
31 | const checked = evt.currentTarget.checked;
32 | dispatch( { type: 'readOnly', payload: checked } );
33 | };
34 |
35 | const handleStyleChange = evt => {
36 | const value = evt.currentTarget.value;
37 | dispatch( { type: 'style', payload: value } );
38 | };
39 |
40 | const handleTypeChange = evt => {
41 | const value = evt.currentTarget.value;
42 | dispatch( { type: 'type', payload: value } );
43 | };
44 |
45 | return (
46 |
47 |
48 |
58 |
59 | { /* Remember to add the license key to the CKEditor 4 configuration:
60 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
61 | {`Hello from ${ type } editor!`}}
66 | name={name}
67 | readOnly={readOnly}
68 | style={getStyle( style )}
69 | type={type}
70 | />
71 |
72 |
73 |
74 |
75 | );
76 | }
77 |
78 | function reducer( state, action ) {
79 | switch ( action.type ) {
80 | case 'toolbar':
81 | return action.payload === state.toolbar ?
82 | state :
83 | {
84 | ...state,
85 | toolbar: action.payload,
86 | config: getConfig( action.payload === 'bold only' ),
87 | name: getUniqueName()
88 | };
89 | case 'type':
90 | return action.payload === state.type ?
91 | state :
92 | {
93 | ...state,
94 | type: action.payload,
95 | name: getUniqueName()
96 | };
97 | case 'readOnly':
98 | return {
99 | ...state,
100 | readOnly: action.payload
101 | };
102 | case 'style':
103 | return {
104 | ...state,
105 | style: action.payload
106 | };
107 | default:
108 | return state;
109 | }
110 | }
111 |
112 | function getConfig( boldOnly ) {
113 | return {
114 | title: 'CKEditor component',
115 | ...( boldOnly ? { toolbar: [ [ 'Bold' ] ] } : undefined )
116 | };
117 | }
118 |
119 | function getStyle( style ) {
120 | const common = {
121 | borderWidth: '1px',
122 | borderStyle: 'solid'
123 | };
124 |
125 | switch ( style ) {
126 | case 'blue':
127 | return {
128 | ...common,
129 | borderColor: 'blue'
130 | };
131 | case 'green':
132 | return {
133 | ...common,
134 | borderColor: 'green'
135 | };
136 | default:
137 | return undefined;
138 | }
139 | }
140 |
141 | function getUniqueName() {
142 | return Math.random()
143 | .toString( 36 )
144 | .replace( /[^a-z]+/g, '' )
145 | .substr( 0, 5 );
146 | }
147 |
148 | export default App;
149 |
--------------------------------------------------------------------------------
/samples/component/src/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import * as React from 'react';
4 |
5 | function Sidebar( {
6 | toolbar,
7 | style,
8 | type,
9 | onToolbarChange,
10 | onReadOnlyChange,
11 | onStyleChange,
12 | onTypeChange,
13 | readOnly
14 | } ) {
15 | return (
16 |
77 | );
78 | }
79 |
80 | export default Sidebar;
81 |
--------------------------------------------------------------------------------
/samples/component/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/samples/component/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/component/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
5 |
6 | module.exports = {
7 | entry: './src/index.jsx',
8 | mode: 'development',
9 | devtool: 'source-map',
10 | output: {
11 | path: path.resolve( __dirname, './public' ),
12 | filename: 'bundle.js'
13 | },
14 | resolve: {
15 | extensions: [ '.js', '.jsx' ],
16 | alias: {
17 | react$: path.resolve( __dirname, './node_modules/react' ),
18 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
19 | }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
30 | }
31 | }
32 | }
33 | ]
34 | },
35 | plugins: [
36 | new HtmlWebpackPlugin( {
37 | title: 'Example',
38 | template: 'src/index.html'
39 | } )
40 | ]
41 | };
42 |
--------------------------------------------------------------------------------
/samples/editor-url/README.md:
--------------------------------------------------------------------------------
1 | # Custom `editorUrl`
2 |
3 | This is an example usage of `CKEditor4` component with custom `editorUrl`.
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/editor-url).
6 |
--------------------------------------------------------------------------------
/samples/editor-url/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4": "^4.19.0",
14 | "ckeditor4-react": "latest",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0"
17 | },
18 | "devDependencies": {
19 | "@babel/core": "^7.18.6",
20 | "@babel/preset-env": "^7.18.6",
21 | "@babel/preset-react": "^7.18.6",
22 | "babel-loader": "^8.2.5",
23 | "copy-webpack-plugin": "^11.0.0",
24 | "html-webpack-plugin": "^5.5.0",
25 | "webpack": "^5.73.0",
26 | "webpack-cli": "^4.10.0",
27 | "webpack-dev-server": "^4.9.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/samples/editor-url/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CKEditor } from 'ckeditor4-react';
3 |
4 | function App() {
5 | return (
6 |
7 |
8 | { /* Remember to add the license key to the CKEditor 4 configuration:
9 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default App;
18 |
--------------------------------------------------------------------------------
/samples/editor-url/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/samples/editor-url/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/editor-url/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const CopyPlugin = require( 'copy-webpack-plugin' );
5 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
6 |
7 | module.exports = {
8 | entry: './src/index.jsx',
9 | mode: 'development',
10 | devtool: 'source-map',
11 | output: {
12 | path: path.resolve( __dirname, './public' ),
13 | filename: 'bundle.js'
14 | },
15 | resolve: {
16 | extensions: [ '.js', '.jsx' ],
17 | alias: {
18 | react$: path.resolve( __dirname, './node_modules/react' ),
19 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
20 | }
21 | },
22 | module: {
23 | rules: [
24 | {
25 | test: /\.jsx?$/,
26 | exclude: /node_modules/,
27 | use: {
28 | loader: 'babel-loader',
29 | options: {
30 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
31 | }
32 | }
33 | }
34 | ]
35 | },
36 | plugins: [
37 | new HtmlWebpackPlugin( {
38 | title: 'Example',
39 | template: 'src/index.html'
40 | } ),
41 | new CopyPlugin( {
42 | patterns: [ { from: './node_modules/ckeditor4/', to: './ckeditor4/' } ]
43 | } )
44 | ]
45 | };
46 |
--------------------------------------------------------------------------------
/samples/hook-events/README.md:
--------------------------------------------------------------------------------
1 | # Editor events with `useCKEditor` hook
2 |
3 | Showcase of using editor events when using `useCKEditor` hook.
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/hook-events).
6 |
--------------------------------------------------------------------------------
/samples/hook-events/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4-react": "latest",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.18.6",
19 | "@babel/preset-env": "^7.18.6",
20 | "@babel/preset-react": "^7.18.6",
21 | "babel-loader": "^8.2.5",
22 | "html-webpack-plugin": "^5.5.0",
23 | "webpack": "^5.73.0",
24 | "webpack-cli": "^4.10.0",
25 | "webpack-dev-server": "^4.9.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/hook-events/src/App.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {
3 | prefixEventName,
4 | stripPrefix,
5 | CKEditorEventAction
6 | } from 'ckeditor4-react';
7 | import Sidebar from './Sidebar';
8 | import CKEditor from './CKEditor';
9 |
10 | const { version, useReducer, useRef } = React;
11 |
12 | function App() {
13 | const [ { events, uniqueName }, dispatch ] = useReducer( reducer, {
14 | events: [],
15 | uniqueName: getUniqueName()
16 | } );
17 | const start = useRef( new Date() );
18 |
19 | const handleCustomEvent = () => {
20 | window.CKEDITOR.instances[ uniqueName ].fire( 'myCustomEvent' );
21 | };
22 |
23 | const handleRemountClick = () => {
24 | dispatch( { type: 'reMount', payload: getUniqueName() } );
25 | };
26 |
27 | return (
28 |
29 |
30 |
31 |
32 | { /* Remember to add the license key to the CKEditor 4 configuration:
33 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
34 |
39 |
42 |
43 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | function reducer( state, action ) {
55 | switch ( action.type ) {
56 | case 'reMount':
57 | return {
58 | ...state,
59 | uniqueName: action.payload
60 | };
61 |
62 | /**
63 | * Event names are prefixed in order to facilitate integration with dispatch from `useReducer`.
64 | * Access them via `CKEditorEventAction`.
65 | */
66 | case CKEditorEventAction.namespaceLoaded:
67 | case CKEditorEventAction.beforeLoad:
68 | return {
69 | ...state,
70 | events: state.events.concat( {
71 | evtName: stripPrefix( action.type ),
72 | editor: '--',
73 | date: new Date()
74 | } )
75 | };
76 | case CKEditorEventAction.loaded:
77 | case CKEditorEventAction.instanceReady:
78 | case CKEditorEventAction.destroy:
79 | case CKEditorEventAction.focus:
80 | case CKEditorEventAction.blur:
81 | case prefixEventName( 'myCustomEvent' ):
82 | return {
83 | ...state,
84 | events: state.events.concat( {
85 | evtName: stripPrefix( action.type ),
86 | editor: action.payload.editor.name,
87 | date: new Date()
88 | } )
89 | };
90 | default:
91 | return state;
92 | }
93 | }
94 |
95 | function getUniqueName() {
96 | return Math.random()
97 | .toString( 36 )
98 | .replace( /[^a-z]+/g, '' )
99 | .substr( 0, 5 );
100 | }
101 |
102 | export default App;
103 |
--------------------------------------------------------------------------------
/samples/hook-events/src/CKEditor.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import * as React from 'react';
4 | import { useCKEditor } from 'ckeditor4-react';
5 |
6 | const { useState } = React;
7 |
8 | /**
9 | * Pass `dispatch` from `useReducer` in order to listen to editor's events and derive state of your components as needed.
10 | */
11 | function CKEditorCmp( { dispatchEvent, uniqueName } ) {
12 | const [ element, setElement ] = useState();
13 |
14 | useCKEditor( {
15 | element,
16 | debug: true,
17 | dispatchEvent,
18 | subscribeTo: [
19 | // Subscribed default events
20 | 'namespaceLoaded',
21 | 'beforeLoad',
22 | 'instanceReady',
23 | 'focus',
24 | 'blur',
25 | 'loaded',
26 | 'destroy',
27 | // Custom events
28 | 'myCustomEvent'
29 | ]
30 | } );
31 |
32 | return ;
33 | }
34 |
35 | export default CKEditorCmp;
36 |
--------------------------------------------------------------------------------
/samples/hook-events/src/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import * as React from 'react';
4 |
5 | function Sidebar( { events, start } ) {
6 | const content =
7 | events.length > 0 ? (
8 |
9 |
10 |
11 | {'Event'} |
12 | {'Editor'} |
13 | {'Elapsed [ms]'} |
14 |
15 |
16 |
17 | {[ ...events ].reverse().map( ( { evtName, editor, date }, i ) => (
18 |
19 | {evtName} |
20 | {editor} |
21 | {date.getTime() - start.getTime()} |
22 |
23 | ) )}
24 |
25 |
26 | ) : (
27 | {'No events yet!'}
28 | );
29 |
30 | return ;
31 | }
32 |
33 | export default Sidebar;
34 |
--------------------------------------------------------------------------------
/samples/hook-events/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/samples/hook-events/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/hook-events/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
5 |
6 | module.exports = {
7 | entry: './src/index.jsx',
8 | mode: 'development',
9 | devtool: 'source-map',
10 | output: {
11 | path: path.resolve( __dirname, './public' ),
12 | filename: 'bundle.js'
13 | },
14 | resolve: {
15 | extensions: [ '.js', '.jsx' ],
16 | alias: {
17 | react$: path.resolve( __dirname, './node_modules/react' ),
18 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
19 | }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
30 | }
31 | }
32 | }
33 | ]
34 | },
35 | plugins: [
36 | new HtmlWebpackPlugin( {
37 | title: 'Example',
38 | template: 'src/index.html'
39 | } )
40 | ]
41 | };
42 |
--------------------------------------------------------------------------------
/samples/hook/README.md:
--------------------------------------------------------------------------------
1 | # Editor as `useCKEditor` hook
2 |
3 | Showcase of `useCKEditor` hook.
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/hook).
6 |
--------------------------------------------------------------------------------
/samples/hook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4-react": "latest",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.18.6",
19 | "@babel/preset-env": "^7.18.6",
20 | "@babel/preset-react": "^7.18.6",
21 | "babel-loader": "^8.2.5",
22 | "html-webpack-plugin": "^5.5.0",
23 | "webpack": "^5.73.0",
24 | "webpack-cli": "^4.10.0",
25 | "webpack-dev-server": "^4.9.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/hook/src/App.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Sidebar from './Sidebar';
3 | import CKEditor from './CKEditor';
4 |
5 | const { version, useReducer } = React;
6 |
7 | /**
8 | * `App` component manages state of underlying `CKEditor` and `Sidebar` components.
9 | *
10 | * Custom `CKEditor` component memoizes certain props and it will ignore any new values. For instance, this is true for `config` and `type`.
11 | * In order to force new `config` or `type` values, use keyed component.
12 | * This way `CKEditor` component is re-mounted and new instance of editor is created.
13 | */
14 | function App() {
15 | const [ { config, readOnly, type, style, toolbar, name }, dispatch ] =
16 | useReducer( reducer, {
17 | config: getConfig(),
18 | readOnly: false,
19 | type: 'classic',
20 | style: 'initial',
21 | toolbar: 'standard',
22 | name: getUniqueName()
23 | } );
24 |
25 | const handleToolbarChange = evt => {
26 | const value = evt.currentTarget.value;
27 | dispatch( { type: 'toolbar', payload: value } );
28 | };
29 |
30 | const handleReadOnlyChange = evt => {
31 | const checked = evt.currentTarget.checked;
32 | dispatch( { type: 'readOnly', payload: checked } );
33 | };
34 |
35 | const handleStyleChange = evt => {
36 | const value = evt.currentTarget.value;
37 | dispatch( { type: 'style', payload: value } );
38 | };
39 |
40 | const handleTypeChange = evt => {
41 | const value = evt.currentTarget.value;
42 | dispatch( { type: 'type', payload: value } );
43 | };
44 |
45 | return (
46 |
47 |
48 |
58 |
59 | { /* Remember to add the license key to the CKEditor 4 configuration:
60 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
61 |
69 |
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | function reducer( state, action ) {
77 | switch ( action.type ) {
78 | case 'toolbar':
79 | return action.payload === state.toolbar ?
80 | state :
81 | {
82 | ...state,
83 | toolbar: action.payload,
84 | config: getConfig( action.payload === 'bold only' ),
85 | name: getUniqueName()
86 | };
87 | case 'type':
88 | return action.payload === state.type ?
89 | state :
90 | {
91 | ...state,
92 | type: action.payload,
93 | name: getUniqueName()
94 | };
95 | case 'readOnly':
96 | return {
97 | ...state,
98 | readOnly: action.payload
99 | };
100 | case 'style':
101 | return {
102 | ...state,
103 | style: action.payload
104 | };
105 | default:
106 | return state;
107 | }
108 | }
109 |
110 | function getUniqueName() {
111 | return Math.random()
112 | .toString( 36 )
113 | .replace( /[^a-z]+/g, '' )
114 | .substr( 0, 5 );
115 | }
116 |
117 | function getConfig( boldOnly ) {
118 | return {
119 | title: 'CKEditor component',
120 | ...( boldOnly ? { toolbar: [ [ 'Bold' ] ] } : undefined )
121 | };
122 | }
123 |
124 | function getStyle( style ) {
125 | const common = {
126 | borderWidth: '1px',
127 | borderStyle: 'solid'
128 | };
129 |
130 | switch ( style ) {
131 | case 'blue':
132 | return {
133 | ...common,
134 | borderColor: 'blue'
135 | };
136 | case 'green':
137 | return {
138 | ...common,
139 | borderColor: 'green'
140 | };
141 | default:
142 | return {};
143 | }
144 | }
145 |
146 | export default App;
147 |
--------------------------------------------------------------------------------
/samples/hook/src/CKEditor.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import * as React from 'react';
4 | import { useCKEditor } from 'ckeditor4-react';
5 |
6 | const { useEffect, useState } = React;
7 |
8 | /**
9 | * Custom `CKEditor` component built on top of `useCKEditor` hook.
10 | */
11 | function CKEditor( { config, readOnly, type, style, name } ) {
12 | const [ element, setElement ] = useState();
13 |
14 | /**
15 | * Sets initial value of `readOnly`.
16 | */
17 | if ( config && readOnly ) {
18 | config.readOnly = readOnly;
19 | }
20 |
21 | const { editor, status } = useCKEditor( {
22 | config,
23 | element,
24 | type
25 | } );
26 |
27 | /**
28 | * Toggles `readOnly` on runtime.
29 | */
30 | useEffect( () => {
31 | if ( editor && editor.status === 'ready' ) {
32 | editor.setReadOnly( readOnly );
33 | }
34 | }, [ editor, readOnly ] );
35 |
36 | /**
37 | * Updates editor container's style on runtime.
38 | */
39 | useEffect( () => {
40 | if ( editor && status === 'ready' ) {
41 | editor.container.setStyles( style );
42 | }
43 | }, [ editor, status, style ] );
44 |
45 | return (
46 |
51 |
{`Hello from ${ type } editor!`}
52 |
53 | );
54 | }
55 |
56 | export default CKEditor;
57 |
--------------------------------------------------------------------------------
/samples/hook/src/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import * as React from 'react';
4 |
5 | function Sidebar( {
6 | toolbar,
7 | style,
8 | type,
9 | onToolbarChange,
10 | onReadOnlyChange,
11 | onStyleChange,
12 | onTypeChange,
13 | readOnly
14 | } ) {
15 | return (
16 |
77 | );
78 | }
79 |
80 | export default Sidebar;
81 |
--------------------------------------------------------------------------------
/samples/hook/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/samples/hook/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/hook/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
5 |
6 | module.exports = {
7 | entry: './src/index.jsx',
8 | mode: 'development',
9 | devtool: 'source-map',
10 | output: {
11 | path: path.resolve( __dirname, './public' ),
12 | filename: 'bundle.js'
13 | },
14 | resolve: {
15 | extensions: [ '.js', '.jsx' ],
16 | alias: {
17 | react$: path.resolve( __dirname, './node_modules/react' ),
18 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
19 | }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
30 | }
31 | }
32 | }
33 | ]
34 | },
35 | plugins: [
36 | new HtmlWebpackPlugin( {
37 | title: 'Example',
38 | template: 'src/index.html'
39 | } )
40 | ]
41 | };
42 |
--------------------------------------------------------------------------------
/samples/re-order/README.md:
--------------------------------------------------------------------------------
1 | # Multiple editor instances (re-ordering case)
2 |
3 | Example of handling multiple `CKEditor` components (re-ordering case).
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/re-order).
6 |
--------------------------------------------------------------------------------
/samples/re-order/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4-react": "latest",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.18.6",
19 | "@babel/preset-env": "^7.18.6",
20 | "@babel/preset-react": "^7.18.6",
21 | "babel-loader": "^8.2.5",
22 | "html-webpack-plugin": "^5.5.0",
23 | "webpack": "^5.73.0",
24 | "webpack-cli": "^4.10.0",
25 | "webpack-dev-server": "^4.9.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/re-order/src/App.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { CKEditor } from 'ckeditor4-react';
3 |
4 | const { version, useState } = React;
5 |
6 | const config = { height: 50, toolbar: [ [ 'Bold' ] ] };
7 |
8 | /**
9 | * Since CKEditor v4.17, editor's container element can be detached and re-attached to DOM.
10 | * Therefore, editor instances can be easily re-ordered.
11 | *
12 | * Prior to CKEditor v4.17, classic editor instance had to be re-created
13 | * anytime editor's container element was being detached and re-attached to DOM.
14 | */
15 | function App() {
16 | const [ order, setOrder ] = useState(
17 | [ 'toast', 'bagel', 'taco', 'avocado' ]
18 | );
19 |
20 | const handleReorderClick = () => {
21 | setOrder( current => shuffle( [ ...current ] ) );
22 | };
23 |
24 | return (
25 |
26 |
27 |
28 |
31 |
32 |
33 |
{'Classic editors'}
34 | {order.map( value => (
35 |
36 | { /* Remember to add the license key to the CKEditor 4 configuration:
37 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
38 |
45 |
46 | ) )}
47 |
48 |
49 |
{'Inline editors'}
50 | {order.map( value => (
51 |
52 |
59 |
60 | ) )}
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 |
70 | function shuffle( array ) {
71 | let currentIndex = array.length;
72 | let temporaryValue;
73 | let randomIndex;
74 |
75 | while ( currentIndex !== 0 ) {
76 | randomIndex = Math.floor( Math.random() * currentIndex );
77 | currentIndex -= 1;
78 |
79 | temporaryValue = array[ currentIndex ];
80 | array[ currentIndex ] = array[ randomIndex ];
81 | array[ randomIndex ] = temporaryValue;
82 | }
83 |
84 | return array;
85 | }
86 |
87 | export default App;
88 |
--------------------------------------------------------------------------------
/samples/re-order/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/samples/re-order/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/re-order/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
5 |
6 | module.exports = {
7 | entry: './src/index.jsx',
8 | mode: 'development',
9 | devtool: 'source-map',
10 | output: {
11 | path: path.resolve( __dirname, './public' ),
12 | filename: 'bundle.js'
13 | },
14 | resolve: {
15 | extensions: [ '.js', '.jsx' ],
16 | alias: {
17 | react$: path.resolve( __dirname, './node_modules/react' ),
18 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
19 | }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
30 | }
31 | }
32 | }
33 | ]
34 | },
35 | plugins: [
36 | new HtmlWebpackPlugin( {
37 | title: 'Example',
38 | template: 'src/index.html'
39 | } )
40 | ]
41 | };
42 |
--------------------------------------------------------------------------------
/samples/router/README.md:
--------------------------------------------------------------------------------
1 | # Router example
2 |
3 | This is a showcase of `CKEditor4` component with React Router.
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/router).
6 |
--------------------------------------------------------------------------------
/samples/router/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4-react": "latest",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "react-router": "^6.3.0",
17 | "react-router-dom": "^6.3.0"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.18.6",
21 | "@babel/preset-env": "^7.18.6",
22 | "@babel/preset-react": "^7.18.6",
23 | "babel-loader": "^8.2.5",
24 | "html-webpack-plugin": "^5.5.0",
25 | "webpack": "^5.73.0",
26 | "webpack-cli": "^4.10.0",
27 | "webpack-dev-server": "^4.9.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/samples/router/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CKEditor } from 'ckeditor4-react';
3 | import { HashRouter, NavLink, Route, Routes } from 'react-router-dom';
4 |
5 | function App() {
6 | return (
7 |
8 |
9 |
10 | {'Home page'}} />
11 | } />
19 |
20 |
21 |
22 | {'Home page'}
23 |
24 |
25 | {'Editor page'}
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | export default App;
35 |
--------------------------------------------------------------------------------
/samples/router/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/samples/router/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/router/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
5 |
6 | module.exports = {
7 | entry: './src/index.jsx',
8 | mode: 'development',
9 | devtool: 'source-map',
10 | output: {
11 | path: path.resolve( __dirname, './public' ),
12 | filename: 'bundle.js'
13 | },
14 | resolve: {
15 | extensions: [ '.js', '.jsx' ],
16 | alias: {
17 | react$: path.resolve( __dirname, './node_modules/react' ),
18 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
19 | }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
30 | }
31 | }
32 | }
33 | ]
34 | },
35 | plugins: [
36 | new HtmlWebpackPlugin( {
37 | title: 'Example',
38 | template: 'src/index.html'
39 | } )
40 | ]
41 | };
42 |
--------------------------------------------------------------------------------
/samples/ssr/README.md:
--------------------------------------------------------------------------------
1 | # Server-side rendering
2 |
3 | Showcase of using `CKEditor` with React server-side rendering
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/ssr).
6 |
--------------------------------------------------------------------------------
/samples/ssr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production && webpack --mode production --config webpack.config.server.js",
5 | "prestart": "npm run build",
6 | "start": "node dist/server.js"
7 | },
8 | "browserslist": [
9 | ">0.2%",
10 | "not dead",
11 | "not op_mini all"
12 | ],
13 | "dependencies": {
14 | "ckeditor4-react": "latest",
15 | "express": "^4.18.1",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0"
18 | },
19 | "devDependencies": {
20 | "@babel/core": "^7.18.6",
21 | "@babel/preset-env": "^7.18.6",
22 | "@babel/preset-react": "^7.18.6",
23 | "babel-loader": "^8.2.5",
24 | "webpack": "^5.73.0",
25 | "webpack-cli": "^4.10.0",
26 | "webpack-dev-server": "^4.9.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/samples/ssr/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CKEditor } from 'ckeditor4-react';
3 |
4 | function App() {
5 | return (
6 |
7 |
8 | { /* Remember to add the license key to the CKEditor 4 configuration:
9 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default App;
18 |
--------------------------------------------------------------------------------
/samples/ssr/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/ssr/src/renderMarkup.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOMServer from 'react-dom/server';
3 | import App from './App';
4 |
5 | function renderMarkup() {
6 | const app = ReactDOMServer.renderToString( );
7 | return `
8 |
9 |
10 |
11 |
12 |
22 |
23 |
24 | ${ app }
25 |
26 |
27 | `;
28 | }
29 |
30 | export default renderMarkup;
31 |
--------------------------------------------------------------------------------
/samples/ssr/src/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | import path from 'path';
4 | import express from 'express';
5 | import renderMarkup from './renderMarkup';
6 |
7 | const PORT = process.env.PORT || 8080;
8 | const app = express();
9 |
10 | app.get( '/', ( _, res ) => {
11 | res.send( renderMarkup() );
12 | } );
13 |
14 | app.use( express.static( path.resolve( __dirname, '../public' ) ) );
15 |
16 | app.listen( PORT, () => {
17 | console.log( `Server is listening on port ${ PORT }` );
18 | } );
19 |
--------------------------------------------------------------------------------
/samples/ssr/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 |
5 | module.exports = {
6 | entry: './src/index.jsx',
7 | mode: 'development',
8 | devtool: 'source-map',
9 | output: {
10 | path: path.resolve( __dirname, './public' ),
11 | filename: 'bundle.js'
12 | },
13 | resolve: {
14 | extensions: [ '.js', '.jsx' ],
15 | alias: {
16 | react$: path.resolve( __dirname, './node_modules/react' ),
17 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
18 | }
19 | },
20 | module: {
21 | rules: [
22 | {
23 | test: /\.jsx?$/,
24 | exclude: /node_modules/,
25 | use: {
26 | loader: 'babel-loader',
27 | options: {
28 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
29 | }
30 | }
31 | }
32 | ]
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/samples/ssr/webpack.config.server.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const config = require( './webpack.config' );
5 |
6 | module.exports = {
7 | ...config,
8 | entry: './src/server.js',
9 | output: {
10 | path: path.resolve( __dirname, './dist' ),
11 | filename: 'server.js'
12 | },
13 | target: [ 'node' ]
14 | };
15 |
--------------------------------------------------------------------------------
/samples/state-lifting/README.md:
--------------------------------------------------------------------------------
1 | # State lifting
2 |
3 | Editor's state is lifted higher up React tree to establish connection between editor and a custom `textarea`.
4 |
5 | This sample was known as two-way data binding in v1.
6 |
7 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/state-lifting).
8 |
--------------------------------------------------------------------------------
/samples/state-lifting/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "webpack --mode production",
5 | "start": "webpack serve"
6 | },
7 | "browserslist": [
8 | ">0.2%",
9 | "not dead",
10 | "not op_mini all"
11 | ],
12 | "dependencies": {
13 | "ckeditor4-react": "latest",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.18.6",
19 | "@babel/preset-env": "^7.18.6",
20 | "@babel/preset-react": "^7.18.6",
21 | "babel-loader": "^8.2.5",
22 | "html-webpack-plugin": "^5.5.0",
23 | "webpack": "^5.73.0",
24 | "webpack-cli": "^4.10.0",
25 | "webpack-dev-server": "^4.9.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/samples/state-lifting/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CKEditorEventAction } from 'ckeditor4-react';
3 | import TextAreaEditor from './TextAreaEditor';
4 | import CKEditor from './CKEditor';
5 |
6 | const { useReducer } = React;
7 |
8 | function App() {
9 | const [ state, dispatch ] = useReducer( reducer, {
10 | data: '',
11 | currentEditor: undefined
12 | } );
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 | { /* Remember to add the license key to the CKEditor 4 configuration:
22 | https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-licenseKey */ }
23 |
24 |
25 |
26 |
27 |
28 |
{`Current editor: ${ state.currentEditor || '' }`}
29 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | function reducer( state, action ) {
41 | switch ( action.type ) {
42 | case 'textareaBlur': {
43 | return {
44 | ...state,
45 | currentEditor: undefined
46 | };
47 | }
48 | case 'textareaData': {
49 | return {
50 | currentEditor: 'textarea',
51 | data: action.payload
52 | };
53 | }
54 | case 'textareaFocus': {
55 | return {
56 | ...state,
57 | currentEditor: 'textarea'
58 | };
59 | }
60 | case CKEditorEventAction.blur: {
61 | return {
62 | ...state,
63 | currentEditor:
64 | state.currentEditor !== 'textarea' ?
65 | undefined :
66 | state.currentEditor
67 | };
68 | }
69 | case CKEditorEventAction.change: {
70 | return {
71 | ...state,
72 | data: action.payload.editor.getData()
73 | };
74 | }
75 | case CKEditorEventAction.focus: {
76 | return {
77 | ...state,
78 | currentEditor: 'CKEditor'
79 | };
80 | }
81 | default: {
82 | return state;
83 | }
84 | }
85 | }
86 |
87 | export default App;
88 |
--------------------------------------------------------------------------------
/samples/state-lifting/src/CKEditor.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react';
4 | import { useCKEditor } from 'ckeditor4-react';
5 |
6 | const { useEffect, useMemo, useState } = React;
7 |
8 | function CKEditor( { dispatch, state } ) {
9 | const [ element, setElement ] = useState();
10 |
11 | const { editor } = useCKEditor( {
12 | element,
13 | debug: true,
14 | dispatchEvent: dispatch,
15 | subscribeTo: [ 'blur', 'focus', 'change' ]
16 | } );
17 |
18 | /**
19 | * Invoking `editor.setData` too often might freeze the browser.
20 | */
21 | const setEditorData = useMemo( () => {
22 | if ( editor ) {
23 | /* eslint-disable-next-line */
24 | return new CKEDITOR.tools.buffers.throttle( 500, data => {
25 | if ( editor ) {
26 | editor.setData( data );
27 | }
28 | } ).input;
29 | }
30 | }, [ editor ] );
31 |
32 | /**
33 | * Sets editor data if it comes from a different source.
34 | */
35 | useEffect( () => {
36 | if ( state.currentEditor === 'textarea' && setEditorData ) {
37 | setEditorData( state.data );
38 | }
39 | }, [ setEditorData, state ] );
40 |
41 | return ;
42 | }
43 |
44 | export default CKEditor;
45 |
--------------------------------------------------------------------------------
/samples/state-lifting/src/TextAreaEditor.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import React from 'react';
4 |
5 | const { useEffect, useState } = React;
6 |
7 | function TextAreaEditor( { dispatch, state } ) {
8 | const [ value, setValue ] = useState();
9 |
10 | const handleTextAreaChange = evt => {
11 | const value = evt.currentTarget.value;
12 | setValue( value );
13 | dispatch( { type: 'textareaData', payload: value } );
14 | };
15 |
16 | const handleBlur = () => {
17 | dispatch( { type: 'textareaBlur' } );
18 | };
19 |
20 | const handleFocus = () => {
21 | dispatch( { type: 'textareaFocus' } );
22 | };
23 |
24 | /**
25 | * Sets text area value if it comes from a different source.
26 | */
27 | useEffect( () => {
28 | if ( state.currentEditor === 'CKEditor' ) {
29 | setValue( state.data );
30 | }
31 | }, [ state ] );
32 |
33 | return (
34 |
40 | );
41 | }
42 |
43 | export default TextAreaEditor;
44 |
--------------------------------------------------------------------------------
/samples/state-lifting/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/samples/state-lifting/src/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './App';
4 |
5 | const element = document.getElementById( 'root' );
6 |
7 | createRoot( element ).render(
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/samples/state-lifting/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const HtmlWebpackPlugin = require( 'html-webpack-plugin' );
5 |
6 | module.exports = {
7 | entry: './src/index.jsx',
8 | mode: 'development',
9 | devtool: 'source-map',
10 | output: {
11 | path: path.resolve( __dirname, './public' ),
12 | filename: 'bundle.js'
13 | },
14 | resolve: {
15 | extensions: [ '.js', '.jsx' ],
16 | alias: {
17 | react$: path.resolve( __dirname, './node_modules/react' ),
18 | 'react-dom$': path.resolve( __dirname, './node_modules/react-dom' )
19 | }
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.jsx?$/,
25 | exclude: /node_modules/,
26 | use: {
27 | loader: 'babel-loader',
28 | options: {
29 | presets: [ '@babel/preset-react', '@babel/preset-env' ]
30 | }
31 | }
32 | }
33 | ]
34 | },
35 | plugins: [
36 | new HtmlWebpackPlugin( {
37 | title: 'Example',
38 | template: 'src/index.html'
39 | } )
40 | ]
41 | };
42 |
--------------------------------------------------------------------------------
/samples/umd/README.md:
--------------------------------------------------------------------------------
1 | # `umd` build
2 |
3 | Showcase of how `umd` build of `ckeditor4-react` package can be used.
4 |
5 | Demo is available [here](https://githubbox.com/ckeditor/ckeditor4-react/tree/stable/samples/umd).
6 |
--------------------------------------------------------------------------------
/samples/umd/build.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const fs = require( 'fs' );
4 | const path = require( 'path' );
5 |
6 | try {
7 | const dest = path.resolve( __dirname, 'public' );
8 |
9 | fs.mkdirSync( dest, { recursive: true } );
10 |
11 | [
12 | 'node_modules/react/umd/react.production.min.js',
13 | 'node_modules/react-dom/umd/react-dom.production.min.js',
14 | 'node_modules/ckeditor4-react/dist/index.umd.development.js',
15 | 'node_modules/babel-standalone/babel.min.js',
16 | 'src/index.html'
17 | ].forEach( filePath => {
18 | const src = path.resolve( __dirname, filePath );
19 | const fileName = path.basename( filePath );
20 | const destFile = path.resolve( dest, fileName );
21 | fs.copyFileSync( src, destFile );
22 | } );
23 | } catch ( error ) {
24 | console.error( error );
25 | process.exitCode = 1;
26 | }
27 |
--------------------------------------------------------------------------------
/samples/umd/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "node ./build.js",
5 | "start": "http-server",
6 | "prestart": "npm run build"
7 | },
8 | "browserslist": [
9 | ">0.2%",
10 | "not dead",
11 | "not op_mini all"
12 | ],
13 | "dependencies": {
14 | "babel-standalone": "^6.26.0",
15 | "ckeditor4-react": "latest",
16 | "http-server": "^14.1.1",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/samples/umd/sandbox.config.json:
--------------------------------------------------------------------------------
1 | { "template": "node" }
2 |
--------------------------------------------------------------------------------
/samples/umd/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/scripts/bump.js:
--------------------------------------------------------------------------------
1 | /* global process, require, __dirname */
2 |
3 | const fs = require( 'fs' );
4 | const path = require( 'path' );
5 | const shell = require( 'shelljs' );
6 | const pkg = require( '../package.json' );
7 |
8 | const args = process.argv;
9 |
10 | if ( !( args && args[ 2 ] && args[ 2 ].length > 2 ) ) {
11 | console.error( 'Missing CKEditor version! USAGE: npm run bump A.B.C, for example: npm run bump 4.11.5' );
12 | process.exit( 1 );
13 | }
14 |
15 | const version = args[ 2 ];
16 |
17 | // Update the CDN link in the 'src/useCKEditor.ts' file.
18 | updateCdnLink( path.resolve( __dirname, '..', 'src', 'useCKEditor.ts' ) );
19 |
20 | // Update the CDN link in the 'karma.conf.js' file.
21 | updateCdnLink( path.resolve( __dirname, '..', 'karma.conf.js' ) );
22 |
23 | // Update 'peerDependency' in 'package.json'.
24 | pkg.peerDependencies.ckeditor4 = `^${ version }`;
25 | fs.writeFileSync( path.resolve( __dirname, '..', 'package.json' ), JSON.stringify( pkg, null, ' ' ) );
26 |
27 | // Update 'devDependency' in the 'package.json' file.
28 | shell.exec( `npm install ckeditor4@${ version } --save-dev` );
29 |
30 | function updateCdnLink( path ) {
31 | const file = fs.readFileSync( path, 'utf8' );
32 | const cdnLinkRegex = /https:\/\/cdn\.ckeditor\.com\/\d\.\d+\.\d+(-lts)?/g;
33 |
34 | fs.writeFileSync( path,
35 | file.replace( cdnLinkRegex, `https://cdn.ckeditor.com/${ version }-lts` ), 'utf8' );
36 | }
37 |
--------------------------------------------------------------------------------
/scripts/e2e-runner.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const shell = require( 'shelljs' );
5 | const kill = require( 'tree-kill' );
6 | const {
7 | execCmd,
8 | execCmdSync,
9 | log,
10 | runReactTester,
11 | waitFor
12 | } = require( './utils' );
13 |
14 | const PACKAGE_PATH = path.resolve( __dirname, '..' );
15 | const TESTS_TMP_PATH = path.resolve( PACKAGE_PATH, '.tmp-e2e-react-tests' );
16 |
17 | const bsUser = process.env.BROWSER_STACK_USERNAME;
18 | const bsKey = process.env.BROWSER_STACK_ACCESS_KEY;
19 | const bsBrowser = process.env.BROWSER_STACK_BROWSER;
20 |
21 | /**
22 | * Runs E2E tests.
23 | */
24 | ( async function() {
25 | /**
26 | *
27 | * Usage: `node ./scripts/e2e-runner.js `
28 | *
29 | * Commands:
30 | *
31 | * --react Specifies react version to test. Possible values: 'all', 'current' or specific version. Defaults to: 'current'.
32 | * --sample Specifies sample to test. By default tests all samples.
33 | *
34 | */
35 | const argv = require( 'minimist' )( process.argv.slice( 2 ) );
36 | const reactVersion = argv.react || 'current';
37 | const requestedSample = argv.sample;
38 |
39 | if ( !bsUser || !bsKey || !bsBrowser ) {
40 | console.log(
41 | 'Following environment variables must be set: BROWSER_STACK_USERNAME, BROWSER_STACK_ACCESS_KEY, BROWSER_STACK_BROWSER'
42 | );
43 | process.exit( 0 );
44 | }
45 |
46 | try {
47 | log.header( 'Running E2E Tests...' );
48 |
49 | log.info( 'Building library package...' );
50 | execCmdSync( 'npm run build', PACKAGE_PATH );
51 |
52 | log.info( 'Linking library...' );
53 | execCmdSync( 'npm link', PACKAGE_PATH );
54 |
55 | log.paragraph( 'Starting tests...' );
56 | await runTests( reactVersion, requestedSample );
57 |
58 | log.success( 'Successfully completed all tests. Have a nice day!' );
59 | } catch ( error ) {
60 | log.error( 'Could not complete E2E tests!' );
61 | console.error( error );
62 | process.exitCode = 1;
63 | }
64 | }() );
65 |
66 | /**
67 | * Iterates over all E2E test suites, prepares environment for each of them, runs tests on requested React versions.
68 | *
69 | * @param {string} reactVersion React version to test. One of `all`, `current` or fixed version.
70 | */
71 | async function runTests( reactVersion, requestedSample ) {
72 | const samples = shell.ls( 'tests/e2e' );
73 | const filteredSamples = requestedSample ?
74 | samples.filter( file => file.split( '.' )[ 0 ] === requestedSample ) :
75 | samples;
76 |
77 | for ( const file of filteredSamples ) {
78 | const sample = file.split( '.' )[ 0 ];
79 |
80 | log.info( `Preparing package environment for tests/e2e/${ file }...` );
81 | shell.rm( '-rf', TESTS_TMP_PATH );
82 | shell.mkdir( TESTS_TMP_PATH );
83 | shell
84 | .ls( path.resolve( PACKAGE_PATH, 'samples', sample ) )
85 | .filter( file => ![ 'node_modules', 'public' ].includes( file ) )
86 | .forEach( file => {
87 | shell.cp(
88 | '-R',
89 | path.resolve( PACKAGE_PATH, 'samples', sample, file ),
90 | path.resolve( TESTS_TMP_PATH, file )
91 | );
92 | } );
93 |
94 | log.info( 'Installing dependencies...' );
95 | execCmdSync(
96 | 'npm install --legacy-peer-deps --loglevel error',
97 | TESTS_TMP_PATH
98 | );
99 |
100 | await runReactTester(
101 | { version: reactVersion, cwd: TESTS_TMP_PATH },
102 | executeReactTestSuite( sample )
103 | );
104 | }
105 | }
106 |
107 | /**
108 | * Prepares async function for React tester.
109 | *
110 | * @param {string} sample sample to test
111 | * @returns {function} async callback
112 | */
113 | function executeReactTestSuite( sample ) {
114 | return async function executeReactTestSuiteForSample() {
115 | log.info( 'Linking parent package...' );
116 | execCmdSync( 'npm link ckeditor4-react', TESTS_TMP_PATH );
117 |
118 | log.info( 'Building sample...' );
119 | execCmdSync( 'npm run build', TESTS_TMP_PATH );
120 |
121 | let tries = 3;
122 |
123 | while ( tries-- ) {
124 | try {
125 | log.info( `Running Nightwatch tests for sample: ${ sample }...` );
126 | await runNightwatchTests( sample );
127 | break;
128 | } catch ( error ) {
129 | if ( tries ) {
130 | log.error(
131 | `Error occurred, retrying... Tries left: ${ tries }`
132 | );
133 | await waitFor( 5000 );
134 | } else {
135 | throw error;
136 | }
137 | }
138 | }
139 | };
140 | }
141 |
142 | /**
143 | * Initiates Nightwatch tests by running nightwatch-runner script.
144 | *
145 | * @param {string} sample sample to test
146 | * @returns {Promise} async callback
147 | */
148 | async function runNightwatchTests( sample ) {
149 | let server;
150 | let testSuite;
151 |
152 | if ( sample === 'ssr' ) {
153 | server = execCmd( 'node dist/server.js', TESTS_TMP_PATH );
154 | await waitFor( 5000 );
155 | testSuite = execCmd(
156 | `node scripts/nightwatch-runner.js -t tests/e2e/${ sample }.js --bs-server http://localhost:8080 --test-sample ${ sample }`,
157 | PACKAGE_PATH
158 | );
159 | } else {
160 | const assets = path.resolve( TESTS_TMP_PATH, './public' );
161 | testSuite = execCmd(
162 | `node scripts/nightwatch-runner.js -t tests/e2e/${ sample }.js --bs-folder-path ${ assets } --test-sample ${ sample }`,
163 | PACKAGE_PATH
164 | );
165 | }
166 |
167 | return new Promise( ( resolve, reject ) => {
168 | testSuite.stdout.on( 'data', log.info );
169 | testSuite.stderr.on( 'data', log.error );
170 |
171 | testSuite.on( 'exit', code => {
172 | if ( server ) {
173 | kill( server.pid, 'SIGINT' );
174 | }
175 |
176 | if ( code > 0 ) {
177 | reject( 'nightwatch-runner script failed' );
178 | } else {
179 | resolve();
180 | }
181 | } );
182 | } );
183 | }
184 |
--------------------------------------------------------------------------------
/scripts/nightwatch-runner.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const Nightwatch = require( 'nightwatch' );
4 | const { Local } = require( 'browserstack-local' );
5 | const { log } = require( './utils' );
6 |
7 | const bsLocal = new Local();
8 | Nightwatch.bsLocal = bsLocal;
9 |
10 | // The following environment variables must be set before running `e2e-runner`.
11 | const bsUser = process.env.BROWSER_STACK_USERNAME;
12 | const bsKey = process.env.BROWSER_STACK_ACCESS_KEY;
13 |
14 | /**
15 | * Starts BrowserStack Local, then runs Nightwatch. Closes BS Local afterwards.
16 | */
17 | ( async function() {
18 | const argv = require( 'minimist' )( process.argv.slice( 2 ) );
19 | const bsFolderPath = argv[ 'bs-folder-path' ];
20 | const bsServer = argv[ 'bs-server' ];
21 | const testSample = argv[ 'test-sample' ];
22 |
23 | try {
24 | log.paragraph( 'Connecting to BrowserStack...' );
25 | await startConnection( bsFolderPath );
26 | await runNightwatchCli( testSample, bsServer );
27 | } catch ( error ) {
28 | log.error( 'An error occurred within Nightwatch runner!' );
29 | console.error( error );
30 | process.exitCode = 1;
31 | }
32 | }() );
33 |
34 | /**
35 | * Initializes connection via BrowserStack Local.
36 | *
37 | * @param {string} bsFolderPath absolute path to tested static assets
38 | * @returns {Promise} promise
39 | */
40 | function startConnection( bsFolderPath ) {
41 | return new Promise( ( resolve, reject ) => {
42 | /**
43 | * !!! Important !!!
44 | *
45 | * Set random string as identifier for BrowserStack Local session, especially if there are sessions which are run in parallel.
46 | *
47 | */
48 | const identifier = Math.random()
49 | .toString( 36 )
50 | .replace( /[^a-z]+/g, '' )
51 | .substr( 0, 5 );
52 |
53 | process.env.BROWSER_STACK_LOCAL_IDENTIFIER = identifier;
54 |
55 | bsLocal.start(
56 | {
57 | key: bsKey,
58 | folder: bsFolderPath,
59 | localIdentifier: identifier
60 | },
61 | error => {
62 | if ( error ) {
63 | reject( error );
64 | } else {
65 | resolve();
66 | }
67 | }
68 | );
69 | } );
70 | }
71 |
72 | /**
73 | * Runs Nightwatch runner. Resolves once testing is done and stops BrowserStack Local.
74 | *
75 | * @param {string} sample current test sample
76 | * @param {string} server server address from which tested app is served
77 | * @returns {Promise} promise
78 | */
79 | function runNightwatchCli(
80 | sample,
81 | bsServer = `http://${ bsUser }.browserstack.com`
82 | ) {
83 | return new Promise( ( resolve, reject ) => {
84 | process.env.NIGHTWATCH_LOCAL_SERVER = bsServer;
85 | process.env.NIGHTWATCH_TEST_SAMPLE = sample;
86 |
87 | Nightwatch.cli( argv => {
88 | /* eslint-disable-next-line new-cap */
89 | const runner = Nightwatch.CliRunner( argv );
90 |
91 | runner
92 | .setup()
93 | .runTests()
94 | .then( () => {
95 | bsLocal.stop( resolve );
96 | } )
97 | .catch( err => {
98 | bsLocal.stop( () => {
99 | reject( err );
100 | } );
101 | } );
102 | } );
103 | } );
104 | }
105 |
--------------------------------------------------------------------------------
/scripts/units-runner.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const path = require( 'path' );
4 | const shell = require( 'shelljs' );
5 | const { runReactTester, execCmdSync, log } = require( './utils' );
6 |
7 | const PACKAGE_PATH = path.resolve( __dirname, '..' );
8 | const TESTS_TMP_PATH = path.resolve( PACKAGE_PATH, '.tmp-units-react-tests' );
9 |
10 | /**
11 | * Run unit tests.
12 | */
13 | ( async function() {
14 | /**
15 | *
16 | * Usage: `node ./scripts/units-runner.js `
17 | *
18 | * Commands:
19 | *
20 | * --react Specifies react version to test. Possible values: 'all', 'current' or specific version. Defaults to: 'current'.
21 | *
22 | */
23 | const argv = require( 'minimist' )( process.argv.slice( 2 ) );
24 | const reactVersion = argv.react || 'current';
25 |
26 | try {
27 | log.header( 'Running Unit Tests...' );
28 |
29 | shell.rm( '-rf', TESTS_TMP_PATH );
30 | shell.mkdir( TESTS_TMP_PATH );
31 | [
32 | 'package.json',
33 | 'karma.conf.js',
34 | 'tsconfig.json',
35 | 'src',
36 | 'scripts',
37 | 'tests'
38 | ].forEach( file => {
39 | shell.cp(
40 | '-R',
41 | path.resolve( PACKAGE_PATH, file ),
42 | path.resolve( TESTS_TMP_PATH, file )
43 | );
44 | } );
45 |
46 | execCmdSync(
47 | 'npm install --legacy-peer-deps --loglevel error',
48 | TESTS_TMP_PATH
49 | );
50 |
51 | await runReactTester(
52 | {
53 | version: reactVersion,
54 |
55 | /**
56 | * Version 16.8.6 does not support async `act` test helper.
57 | * https://reactjs.org/blog/2019/08/08/react-v16.9.0.html#async-act-for-testing
58 | */
59 | skip: [ '16.8.6' ],
60 | cwd: TESTS_TMP_PATH
61 | },
62 | () => {
63 | return console.log(
64 | execCmdSync(
65 | 'node node_modules/.bin/karma start --single-run --silent-build-logs=true',
66 | TESTS_TMP_PATH
67 | )
68 | );
69 | }
70 | );
71 | } catch ( error ) {
72 | log.error( 'Could not complete unit tests!' );
73 | console.error( error );
74 | process.exitCode = 1;
75 | }
76 | }() );
77 |
--------------------------------------------------------------------------------
/scripts/utils.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | const { exec, execSync } = require( 'child_process' );
4 | const { red, blue, green, yellow, magenta } = require( 'chalk' );
5 | const { satisfies, minor } = require( 'semver' );
6 |
7 | /**
8 | * Logging utils.
9 | */
10 | const log = {
11 | error: msg => console.log( red( msg ) ),
12 | info: msg => console.log( blue( msg ) ),
13 | success: msg => console.log( green( msg ) ),
14 | warn: msg => console.log( yellow( msg ) ),
15 | paragraph: msg => {
16 | console.log();
17 | console.log( blue.underline( msg ) );
18 | console.log();
19 | },
20 | header: msg => {
21 | console.log( blue.bold( msg ) );
22 | console.log(
23 | magenta(
24 | Array.from( { length: msg.length } )
25 | .map( () => '=' )
26 | .join( '' )
27 | )
28 | );
29 | console.log();
30 | }
31 | };
32 |
33 | /**
34 | * Waits for x ms. Promisified version of `setTimeout`.
35 | *
36 | * @param {number} time time to wait in ms
37 | * @returns {Promise}
38 | */
39 | function waitFor( time ) {
40 | return new Promise( resolve => {
41 | setTimeout( resolve, time );
42 | } );
43 | }
44 |
45 | /**
46 | * Executes child process synchronously.
47 | *
48 | * @param {string} command command to execute
49 | * @param {string} cwd dir where to execute command
50 | * @returns {string|Buffer} command output
51 | */
52 | function execCmdSync( command, cwd = __dirname ) {
53 | return execSync( command, {
54 | encoding: 'utf-8',
55 | cwd
56 | } );
57 | }
58 |
59 | /**
60 | * Executes child process asynchronously.
61 | *
62 | * @param {string} command command to execute
63 | * @param {string} cwd dir where to execute command
64 | * @returns {string|Buffer} command output
65 | */
66 | function execCmd( command, cwd = __dirname ) {
67 | return exec( command, {
68 | encoding: 'utf-8',
69 | cwd
70 | } );
71 | }
72 |
73 | /**
74 | * Gets list of available React versions from npm.
75 | *
76 | * @returns {string[]} available React versions
77 | */
78 | function getReactVersionsFromNpm() {
79 | const commandResult = execCmdSync( 'npm view react versions --json' );
80 | const versions = JSON.parse( commandResult );
81 | return versions;
82 | }
83 |
84 | /**
85 | * Gets peered version range from `package.json`.
86 | *
87 | * @param {Object} packageInfo contents of `package.json`
88 | * @returns {string} peered version / version range
89 | */
90 | function getPeeredReactVersion( packageInfo ) {
91 | return packageInfo.peerDependencies.react;
92 | }
93 |
94 | /**
95 | * Filters versions based on requested range.
96 | *
97 | * @param {string} range version range
98 | * @param {string[]} versions list of versions
99 | * @returns {string[]} versions in requested range
100 | */
101 | function getReactVersionsInRange( range, versions ) {
102 | return versions.filter( version => {
103 | return satisfies( version, range );
104 | } );
105 | }
106 |
107 | /**
108 | * Gets latest patches for each minor version.
109 | *
110 | * @param {string[]} versions list of versions
111 | * @returns {string[]} list of latest patches
112 | */
113 | function getLatestPatches( versions ) {
114 | return versions.reduce( ( acc, version, index, array ) => {
115 | if ( isLatestPatch( index, array ) ) {
116 | acc.push( version );
117 | }
118 |
119 | return acc;
120 | }, [] );
121 | }
122 |
123 | /**
124 | * Checks if version is latest patch in a given list of versions.
125 | *
126 | * @param {number} index current index
127 | * @param {string[]} array list of versions
128 | * @returns {boolean} if version is latest patch
129 | */
130 | function isLatestPatch( index, array ) {
131 | if ( array.length == index + 1 ) {
132 | return true;
133 | }
134 |
135 | if ( minor( array[ index ] ) != minor( array[ index + 1 ] ) ) {
136 | return true;
137 | } else {
138 | return false;
139 | }
140 | }
141 |
142 | /**
143 | * Gets currently installed version of React.
144 | *
145 | * @returns {string} React version
146 | */
147 | function getCurrentReactVersion() {
148 | return require( 'react/package.json' ).version;
149 | }
150 |
151 | /**
152 | * Gets list of all React versions that can be tested.
153 | *
154 | * @returns {string[]} list of versions to test
155 | */
156 | function getAllReactVersions() {
157 | const packageInfo = require( '../package.json' );
158 | const availableVersions = getReactVersionsFromNpm();
159 | const semverRange = getPeeredReactVersion( packageInfo );
160 | const versionsInRange = getReactVersionsInRange(
161 | semverRange,
162 | availableVersions
163 | );
164 | return getLatestPatches( versionsInRange );
165 | }
166 |
167 | /**
168 | * Enhances list of React versions with `all`, `current`, `last-two`, and a fixed version.
169 | *
170 | * @returns {string[]} list of versions to be tested
171 | */
172 | function getVersionsToTest( version ) {
173 | switch ( version ) {
174 | case 'all':
175 | return getAllReactVersions();
176 | case 'current':
177 | return [ getCurrentReactVersion() ];
178 | case 'last-two':
179 | return getAllReactVersions().slice( -2 );
180 | default:
181 | return [ version ];
182 | }
183 | }
184 |
185 | /**
186 | * Gets list of React versions to test, then sequentially installs requested versions in a given path.
187 | * Custom async callback will be invoked after each installation.
188 | *
189 | * @param {object} config runner config
190 | * @param {function} cb async callback to execute after installation of new React version
191 | */
192 | async function runReactTester( { version, cwd, skip = [] }, cb ) {
193 | const versionsToTest = getVersionsToTest( version ).filter(
194 | v => skip.indexOf( v ) === -1
195 | );
196 |
197 | log.paragraph( 'Running React tester' );
198 | log.info( 'Versions that will be tested (' + versionsToTest.length + '):' );
199 | log.info( JSON.stringify( versionsToTest ) );
200 |
201 | for ( const version of versionsToTest ) {
202 | log.paragraph( `Testing React v${ version }` );
203 | log.info( 'Preparing package environment...' );
204 | execCmdSync(
205 | `npm install react@${ version } react-dom@${ version } --legacy-peer-deps --loglevel error`,
206 | cwd
207 | );
208 |
209 | process.env.REQUESTED_REACT_VERSION = version;
210 |
211 | await cb( version );
212 | }
213 | }
214 |
215 | module.exports = {
216 | execCmd,
217 | execCmdSync,
218 | runReactTester,
219 | waitFor,
220 | log
221 | };
222 |
--------------------------------------------------------------------------------
/src/CKEditor.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3 | * For licensing, see LICENSE.md.
4 | */
5 |
6 | import * as React from 'react';
7 | import * as PropTypes from 'prop-types';
8 | import {
9 | eventNameToHandlerName,
10 | defaultEvents,
11 | stripPrefix,
12 | handlerNameToEventName
13 | } from './events';
14 | import useCKEditor from './useCKEditor';
15 | import { camelToKebab, getStyle } from './utils';
16 |
17 | import {
18 | CKEditorEventDispatcher,
19 | CKEditorEventHandlerProp,
20 | CKEditorProps,
21 | CKEditorType
22 | } from './types';
23 |
24 | const { useEffect, useRef, useState } = React;
25 |
26 | /**
27 | * `CKEditor` component is a convenient wrapper around low-level hooks.
28 | * It's useful for simpler use cases. For advanced usage see `useCKEditor` hook.
29 | */
30 | function CKEditor( {
31 | config = {},
32 | debug,
33 | editorUrl,
34 | initData,
35 | name,
36 | readOnly,
37 | style,
38 | type,
39 |
40 | /**
41 | * `handlers` object must contain event handlers props only!
42 | */
43 | ...handlers
44 | }: CKEditorProps ): JSX.Element {
45 | /**
46 | * Uses `useState` instead of `useRef` to force re-render.
47 | */
48 | const [ element, setElement ] = useState( null );
49 |
50 | /**
51 | * Ensures referential equality of event handlers.
52 | */
53 | const refs = useRef( handlers );
54 |
55 | const dispatchEvent: CKEditorEventDispatcher = ( { type, payload } ) => {
56 | const handlerName = eventNameToHandlerName(
57 | stripPrefix( type )
58 | ) as keyof CKEditorEventHandlerProp;
59 | const handler = refs.current[ handlerName ];
60 |
61 | if ( handler ) {
62 | handler( payload );
63 | }
64 | };
65 |
66 | /**
67 | * `readOnly` prop takes precedence over `config.readOnly`.
68 | */
69 | if ( config && typeof readOnly === 'boolean' ) {
70 | config.readOnly = readOnly;
71 | }
72 |
73 | const { editor, status } = useCKEditor( {
74 | config,
75 | dispatchEvent,
76 | debug,
77 | editorUrl,
78 | element,
79 |
80 | /**
81 | * String nodes are handled by the hook.
82 | * `initData` as JSX is handled in the component.
83 | */
84 | initContent: typeof initData === 'string' ? initData : undefined,
85 |
86 | /**
87 | * Subscribe only to those events for which handler was supplied.
88 | */
89 | subscribeTo: Object.keys( handlers )
90 | .filter( key => key.indexOf( 'on' ) === 0 )
91 | .map( handlerNameToEventName ),
92 | type
93 | } );
94 |
95 | /**
96 | * Sets and updates styles.
97 | */
98 | useEffect( () => {
99 | const canSetStyles =
100 | type !== 'inline' &&
101 | editor &&
102 | ( status === 'loaded' || status === 'ready' );
103 |
104 | if ( style && canSetStyles ) {
105 | editor.container.setStyles( style );
106 | }
107 |
108 | return () => {
109 | if ( style && canSetStyles ) {
110 | Object.keys( style )
111 | .map( camelToKebab )
112 | .forEach( styleName => {
113 | editor.container.removeStyle( styleName );
114 | } );
115 | }
116 | };
117 | }, [ editor, status, style, type ] );
118 |
119 | /**
120 | * Toggles read-only mode on runtime.
121 | */
122 | useEffect( () => {
123 | if ( editor && status === 'ready' && typeof readOnly === 'boolean' ) {
124 | editor.setReadOnly( readOnly );
125 | }
126 | }, [ editor, status, readOnly ] );
127 |
128 | return (
129 |
134 | {typeof initData === 'string' ? null : initData}
135 |
136 | );
137 | }
138 |
139 | const propTypes = {
140 | /**
141 | * Config object passed to editor's constructor.
142 | *
143 | * A new instance of editor will be created everytime a new instance of `config` is provided.
144 | * If this is not expected behavior then ensure referential equality of `config` between renders.
145 | *
146 | * See: https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html
147 | */
148 | config: PropTypes.object,
149 |
150 | /**
151 | * Toggles debugging. Logs info related to editor lifecycle events.
152 | */
153 | debug: PropTypes.bool,
154 |
155 | /**
156 | * Url with editor's source code. Uses newest version from https://cdn.ckeditor.com domain by default.
157 | */
158 | editorUrl: PropTypes.string,
159 |
160 | /**
161 | * Initial data will be set only once during editor instance's lifecycle.
162 | */
163 | initData: PropTypes.node,
164 |
165 | /**
166 | * A unique identifier of editor instance.
167 | *
168 | * See: https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#property-name
169 | */
170 | name: PropTypes.string,
171 |
172 | /**
173 | * This prop has two-fold effect:
174 | *
175 | * - Serves as a convenience prop to start editor in read-only mode.
176 | * It's an equivalent of passing `{ readOnly: true }` in `config` but takes precedence over it.
177 | *
178 | * - Allows to toggle editor's `read-only` mode on runtime.
179 | *
180 | * See: https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html#cfg-readOnly
181 | */
182 | readOnly: PropTypes.bool,
183 |
184 | /**
185 | * Styles passed to the root element.
186 | */
187 | style: PropTypes.object,
188 |
189 | /**
190 | * Setups editor in either `classic` or `inline` mode.
191 | *
192 | * A new instance of editor will be created everytime a new value of `type` is provided.
193 | * If this is not expected behavior then ensure stable value of `type` between renders.
194 | *
195 | * See:
196 | * - https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html#method-replace
197 | * - https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html#method-inline
198 | */
199 | type: PropTypes.oneOf( [ 'classic', 'inline' ] ),
200 |
201 | /**
202 | * Event handlers.
203 | *
204 | * Each event handler's name corresponds to its respective event, e.g. `instanceReady` -> `onInstanceReady`.
205 | */
206 | ...defaultEvents.reduce( ( acc, key ) => {
207 | return {
208 | ...acc,
209 | [ eventNameToHandlerName( key ) ]: PropTypes.func
210 | };
211 | }, {} as Record )
212 | };
213 |
214 | CKEditor.propTypes = propTypes;
215 |
216 | export default CKEditor;
217 |
--------------------------------------------------------------------------------
/src/events.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3 | * For licensing, see LICENSE.md.
4 | */
5 |
6 | import { CKEditorAction } from './types';
7 |
8 | /**
9 | * Two types of events are discerned:
10 | *
11 | * - `editor` events are associated with native editor events. In addition, custom events can be specified.
12 | * - `namespace` events are additional events provided by React integration.
13 | */
14 |
15 | /**
16 | * Available `editor` events.
17 | *
18 | * See: https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html
19 | */
20 | export const events = [
21 | 'activeEnterModeChange',
22 | 'activeFilterChange',
23 | 'afterCommandExec',
24 | 'afterInsertHtml',
25 | 'afterPaste',
26 | 'afterPasteFromWord',
27 | 'afterSetData',
28 | 'afterUndoImage',
29 | 'ariaEditorHelpLabel',
30 | 'ariaWidget',
31 | 'autogrow',
32 | 'beforeCommandExec',
33 | 'beforeDestroy',
34 | 'beforeGetData',
35 | 'beforeModeUnload',
36 | 'beforeSetMode',
37 | 'beforeUndoImage',
38 | 'blur',
39 | 'change',
40 | 'configLoaded',
41 | 'contentDirChanged',
42 | 'contentDom',
43 | 'contentDomInvalidated',
44 | 'contentDomUnload',
45 | 'contentPreview',
46 | 'customConfigLoaded',
47 | 'dataFiltered',
48 | 'dataReady',
49 | 'destroy',
50 | 'dialogHide',
51 | 'dialogShow',
52 | 'dirChanged',
53 | 'doubleclick',
54 | 'dragend',
55 | 'dragstart',
56 | 'drop',
57 | 'elementsPathUpdate',
58 | 'exportPdf',
59 | 'fileUploadRequest',
60 | 'fileUploadResponse',
61 | 'floatingSpaceLayout',
62 | 'focus',
63 | 'getData',
64 | 'getSnapshot',
65 | 'insertElement',
66 | 'insertHtml',
67 | 'insertText',
68 | 'instanceReady',
69 | 'key',
70 | 'langLoaded',
71 | 'loadSnapshot',
72 | 'loaded',
73 | 'lockSnapshot',
74 | 'maximize',
75 | 'menuShow',
76 | 'mode',
77 | 'notificationHide',
78 | 'notificationShow',
79 | 'notificationUpdate',
80 | 'paste',
81 | 'pasteFromWord',
82 | 'pluginsLoaded',
83 | 'readOnly',
84 | 'removeFormatCleanup',
85 | 'required',
86 | 'resize',
87 | 'save',
88 | 'saveSnapshot',
89 | 'selectionChange',
90 | 'setData',
91 | 'stylesRemove',
92 | 'stylesSet',
93 | 'template',
94 | 'toDataFormat',
95 | 'toHtml',
96 | 'uiSpace',
97 | 'unlockSnapshot',
98 | 'updateSnapshot',
99 | 'widgetDefinition'
100 | ] as const;
101 |
102 | /**
103 | * Available `namespace` events.
104 | *
105 | * - `beforeLoad`: fired before an editor instance is created
106 | * - `namespaceLoaded`: fired after CKEDITOR namespace is created; fired only once regardless of number of editor instances
107 | */
108 | export const namespaceEvents = [ 'beforeLoad', 'namespaceLoaded' ] as const;
109 |
110 | /**
111 | * Combines `editor` and `namespace` events.
112 | */
113 | export const defaultEvents = [ ...events, ...namespaceEvents ];
114 |
115 | /**
116 | * Events as action types should be prefixed to allow easier consumption by downstream reducers.
117 | */
118 | export const EVENT_PREFIX = '__CKE__';
119 |
120 | /**
121 | * Prefixes event name: `instanceReady` -> `__CKE__instanceReady`.
122 | *
123 | * @param evtName event name
124 | * @returns prefixed event name
125 | */
126 | export function prefixEventName( evtName: string ) {
127 | return `${ EVENT_PREFIX }${ evtName }`;
128 | }
129 |
130 | /**
131 | * Strips prefix from event name. `__CKE__instanceReady` -> `instanceReady`.
132 | *
133 | * @param evtName prefixed event name
134 | * @returns event name
135 | */
136 | export function stripPrefix( prefixedEventName: string ) {
137 | return prefixedEventName.substr( EVENT_PREFIX.length );
138 | }
139 |
140 | /**
141 | * Transforms prefixed event name to a handler name, e.g. `instanceReady` -> `onInstanceReady`.
142 | *
143 | * @param evtName event name
144 | * @returns handler name
145 | */
146 | export function eventNameToHandlerName( evtName: string ) {
147 | const cap = evtName.substr( 0, 1 ).toUpperCase() + evtName.substr( 1 );
148 | return `on${ cap }`;
149 | }
150 |
151 | /**
152 | * Transforms handler name to event name, e.g. `onInstanceReady` -> `instanceReady`.
153 | *
154 | * @param evtName event name
155 | * @returns handler name
156 | */
157 | export function handlerNameToEventName( handlerName: string ) {
158 | return handlerName.substr( 2, 1 ).toLowerCase() + handlerName.substr( 3 );
159 | }
160 |
161 | /**
162 | * Provides an object with event names as keys and prefixed names as values, e.g. `{ instanceReady: __CKE__instanceReady }`.
163 | * This allows to easily mix editor event actions and own actions in downstream reducers.
164 | */
165 | export const CKEditorEventAction = [ ...events, ...namespaceEvents ].reduce(
166 | ( acc, evtName ) => {
167 | return {
168 | ...acc,
169 | [ evtName ]: prefixEventName( evtName )
170 | };
171 | },
172 | {} as CKEditorAction
173 | );
174 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3 | * For licensing, see LICENSE.md.
4 | */
5 |
6 | export * from './types';
7 |
8 | export { prefixEventName, stripPrefix, CKEditorEventAction } from './events';
9 |
10 | export * from './registerEditorEventHandler';
11 | export { default as registerEditorEventHandler } from './registerEditorEventHandler';
12 |
13 | export * from './useCKEditor';
14 | export { default as useCKEditor } from './useCKEditor';
15 |
16 | export * from './CKEditor';
17 | export { default as CKEditor } from './CKEditor';
18 |
--------------------------------------------------------------------------------
/src/modules.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3 | * For licensing, see LICENSE.md.
4 | */
5 |
6 | /**
7 | * Declares modules that miss types. This is to avoid TS compiler errors.
8 | */
9 | declare module 'ckeditor4-integrations-common';
10 |
--------------------------------------------------------------------------------
/src/registerEditorEventHandler.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3 | * For licensing, see LICENSE.md.
4 | */
5 |
6 | import { uniqueName } from './utils';
7 |
8 | import {
9 | CKEditorDefaultEvent,
10 | CKEditorEventHandler,
11 | CKEditorEventPayload,
12 | CKEditorRegisterEventArgs
13 | } from './types';
14 |
15 | /**
16 | * Registers editor event. Allows to toggle debugging mode.
17 | *
18 | * @param editor instance of editor
19 | * @param debug toggles debugger
20 | */
21 | function registerEditorEventHandler( {
22 | debug,
23 | editor,
24 | evtName,
25 | handler,
26 | listenerData,
27 | priority
28 | }: CKEditorRegisterEventArgs ) {
29 | const handlerId = debug && uniqueName();
30 |
31 | let _handler: CKEditorEventHandler = handler;
32 |
33 | if ( debug ) {
34 | _handler = function( args: CKEditorEventPayload ) {
35 | console.log( {
36 | operation: 'invoke',
37 | editor: editor.name,
38 | evtName,
39 | handlerId,
40 | data: args.data,
41 | listenerData: args.listenerData
42 | } );
43 | handler( args );
44 | };
45 | }
46 |
47 | if ( debug ) {
48 | console.log( {
49 | operation: 'register',
50 | editor: editor.name,
51 | evtName,
52 | handlerId
53 | } );
54 | }
55 |
56 | editor.on( evtName, _handler, null, listenerData, priority );
57 |
58 | return () => {
59 | if ( debug ) {
60 | console.log( {
61 | operation: 'unregister',
62 | editor: editor.name,
63 | evtName,
64 | handlerId
65 | } );
66 | }
67 |
68 | editor.removeListener( evtName, _handler );
69 | };
70 | }
71 |
72 | export default registerEditorEventHandler;
73 |
--------------------------------------------------------------------------------
/src/useCKEditor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3 | * For licensing, see LICENSE.md.
4 | */
5 |
6 | import * as React from 'react';
7 | import { getEditorNamespace } from 'ckeditor4-integrations-common';
8 | import registerEditorEventHandler from './registerEditorEventHandler';
9 | import {
10 | CKEditorEventAction,
11 | defaultEvents,
12 | EVENT_PREFIX,
13 | namespaceEvents
14 | } from './events';
15 |
16 | import {
17 | CKEditorConfig,
18 | CKEditorDefaultEvent,
19 | CKEditorHookProps,
20 | CKEditorHookResult,
21 | CKEditorInstance,
22 | CKEditorNamespace,
23 | CKEditorStatus
24 | } from './types';
25 |
26 | const { useEffect, useReducer, useRef } = React;
27 |
28 | const defEditorUrl = 'https://cdn.ckeditor.com/4.25.1-lts/standard-all/ckeditor.js';
29 | const defConfig: CKEditorConfig = {};
30 |
31 | /**
32 | * `useCKEditor` is a low-level hook that holds core logic for editor lifecycle.
33 | * It is responsible for initializing and destroying editor instance.
34 | */
35 | function useCKEditor( {
36 | config,
37 | debug,
38 | dispatchEvent,
39 | subscribeTo = defaultEvents,
40 | editorUrl,
41 | element,
42 | initContent,
43 | type = 'classic'
44 | }: CKEditorHookProps ): CKEditorHookResult {
45 | /**
46 | * Ensures stable value of `editorUrl` between renders.
47 | */
48 | const editorUrlRef = useRef( editorUrl || defEditorUrl );
49 |
50 | /**
51 | * Ensures stable value of `subscribeTo` between renders.
52 | */
53 | const subscribeToRef = useRef( subscribeTo ?? defaultEvents );
54 |
55 | /**
56 | * Ensures stable value of `debug` between renders.
57 | */
58 | const debugRef = useRef( debug );
59 |
60 | /**
61 | * Ensures referential stability of `dispatchEvent` between renders.
62 | */
63 | const dispatchEventRef = useRef( dispatchEvent );
64 |
65 | /**
66 | * Ensures referential stability of `initContent`.
67 | */
68 | const initContentRef = useRef( initContent );
69 |
70 | /**
71 | * Ensures referential stability of editor config.
72 | */
73 | const configRef = useRef( config || defConfig );
74 |
75 | /**
76 | * Ensures referential stability of editor type.
77 | */
78 | const typeRef = useRef( type );
79 |
80 | /**
81 | * Holds current editor instance and hook status.
82 | */
83 | const [ { editor, hookStatus }, dispatch ] = useReducer( reducer, {
84 | editor: undefined,
85 | hookStatus: 'init'
86 | } );
87 |
88 | /**
89 | * Main effect. It takes care of:
90 | * - fetching CKEditor from remote source
91 | * - creating new instances of editor
92 | * - registering event handlers
93 | * - destroying editor instances
94 | *
95 | * New instance of editor will be created whenever new config is passed, new DOM element is passed, or editor type is changed.
96 | */
97 | useEffect( () => {
98 | if ( element && !editor ) {
99 | dispatch( { type: 'loading' } );
100 |
101 | /**
102 | * Helper callback that dispatches `namespaceLoaded` event.
103 | */
104 | const onNamespaceLoaded = ( CKEDITOR: CKEditorNamespace ) => {
105 | if ( subscribeToRef.current.indexOf( 'namespaceLoaded' ) !== -1 ) {
106 | dispatchEventRef.current?.( {
107 | type: CKEditorEventAction.namespaceLoaded,
108 | payload: CKEDITOR
109 | } );
110 | }
111 | };
112 |
113 | const initEditor = ( CKEDITOR: CKEditorNamespace ) => {
114 | const isInline = typeRef.current === 'inline';
115 | const isReadOnly = configRef.current.readOnly;
116 |
117 | /**
118 | * Dispatches `beforeLoad` event.
119 | */
120 | if ( subscribeToRef.current.indexOf( 'beforeLoad' ) !== -1 ) {
121 | dispatchEventRef.current?.( {
122 | type: CKEditorEventAction.beforeLoad,
123 | payload: CKEDITOR
124 | } );
125 | }
126 |
127 | const editor = CKEDITOR[ isInline ? 'inline' : 'replace' ](
128 | element,
129 | configRef.current
130 | );
131 |
132 | const subscribedEditorEvents = subscribeToRef.current.filter(
133 | ( evtName: any ) => namespaceEvents.indexOf( evtName ) === -1
134 | );
135 |
136 | /**
137 | * Registers all subscribed events.
138 | */
139 | subscribedEditorEvents.forEach( evtName => {
140 | registerEditorEventHandler( {
141 | debug: debugRef.current,
142 | editor,
143 | evtName,
144 | handler: payload => {
145 | dispatchEventRef.current?.( {
146 | type: `${ EVENT_PREFIX }${ evtName }`,
147 | payload
148 | } );
149 | }
150 | } );
151 | } );
152 |
153 | /**
154 | * Registers `loaded` event for the sake of hook lifecycle.
155 | */
156 | registerEditorEventHandler( {
157 | debug: debugRef.current,
158 | editor,
159 | evtName: 'loaded',
160 | handler: () => {
161 | dispatch( { type: 'loaded' } );
162 | },
163 | priority: -1
164 | } );
165 |
166 | /**
167 | * Registers handler `instanceReady` event.
168 | */
169 | registerEditorEventHandler( {
170 | debug: debugRef.current,
171 | editor,
172 | evtName: 'instanceReady',
173 | handler: ( { editor } ) => {
174 | dispatch( { type: 'ready' } );
175 |
176 | /**
177 | * Force editability of inline editor due to an upstream issue (ckeditor/ckeditor4#3866)
178 | */
179 | if ( isInline && !isReadOnly ) {
180 | editor.setReadOnly( false );
181 | }
182 |
183 | /**
184 | * Sets initial content of editor's instance if provided.
185 | */
186 | if ( initContentRef.current ) {
187 | editor.setData( initContentRef.current, {
188 | /**
189 | * Prevents undo icon flickering.
190 | */
191 | noSnapshot: true,
192 |
193 | /**
194 | * Resets undo stack.
195 | */
196 | callback: () => {
197 | editor.resetUndo();
198 | }
199 | } );
200 | }
201 | },
202 | priority: -1
203 | } );
204 |
205 | /**
206 | * Registers `destroy` event for the sake of hook lifecycle.
207 | */
208 | registerEditorEventHandler( {
209 | debug: debugRef.current,
210 | editor,
211 | evtName: 'destroy',
212 | handler: () => {
213 | dispatch( { type: 'destroyed' } );
214 | },
215 | priority: -1
216 | } );
217 |
218 | dispatch( {
219 | type: 'unloaded',
220 | payload: editor
221 | } );
222 | };
223 |
224 | getEditorNamespace( editorUrlRef.current, onNamespaceLoaded )
225 | .then( initEditor )
226 | .catch( ( error: Error ) => {
227 | if ( process.env.NODE_ENV !== 'test' ) {
228 | console.error( error );
229 | }
230 | dispatch( { type: 'error' } );
231 | } );
232 | }
233 |
234 | return () => {
235 | if ( editor ) {
236 | editor.destroy();
237 | }
238 | };
239 | }, [ editor, element ] );
240 |
241 | return {
242 | editor,
243 | status: editor?.status,
244 | error: hookStatus === 'error',
245 | loading: hookStatus === 'loading'
246 | };
247 | }
248 |
249 | function reducer( state: HookState, action: HookAction ): HookState {
250 | switch ( action.type ) {
251 | case 'init':
252 | return { ...state, hookStatus: 'init' };
253 | case 'loading':
254 | return { ...state, hookStatus: 'loading' };
255 | case 'unloaded':
256 | return {
257 | editor: action.payload,
258 | hookStatus: 'unloaded'
259 | };
260 | case 'loaded':
261 | return {
262 | ...state,
263 | hookStatus: 'loaded'
264 | };
265 | case 'ready':
266 | return {
267 | ...state,
268 | hookStatus: 'ready'
269 | };
270 | case 'destroyed':
271 | return {
272 | editor: undefined,
273 | hookStatus: 'destroyed'
274 | };
275 | case 'error':
276 | return {
277 | editor: undefined,
278 | hookStatus: 'error'
279 | };
280 | default:
281 | return state;
282 | }
283 | }
284 |
285 | type HookInternalStatus = 'init' | 'loading' | 'error' | CKEditorStatus;
286 |
287 | interface HookState {
288 | editor?: CKEditorInstance;
289 | hookStatus?: HookInternalStatus;
290 | }
291 |
292 | interface HookAction {
293 | type: HookInternalStatus;
294 | payload?: CKEditorInstance;
295 | }
296 |
297 | export default useCKEditor;
298 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3 | * For licensing, see LICENSE.md.
4 | */
5 |
6 | import * as React from 'react';
7 | import { CKEditorStatus, CKEditorType } from './types';
8 |
9 | /**
10 | * Transforms `camelCaseValue` into `kebab-case-value`.
11 | *
12 | * @param str string to transform
13 | * @returns transformed string
14 | */
15 | export function camelToKebab( str: string ) {
16 | return str
17 | .split( /(?=[A-Z])/ )
18 | .join( '-' )
19 | .toLowerCase();
20 | }
21 |
22 | /**
23 | * Generates reasonably unique value of five lower-case letters.
24 | *
25 | * @returns unique value
26 | */
27 | export function uniqueName() {
28 | return Math.random()
29 | .toString( 36 )
30 | .replace( /[^a-z]+/g, '' )
31 | .substr( 0, 5 );
32 | }
33 |
34 | /**
35 | * Returns style for the root element.
36 | *
37 | * @param type editor type
38 | * @param status editor status
39 | * @param style custom style
40 | * @returns style
41 | */
42 | export function getStyle(
43 | type: CKEditorType,
44 | status?: CKEditorStatus,
45 | style?: React.CSSProperties | null
46 | ) {
47 | const hidden = { display: 'none', visibility: 'hidden' } as const;
48 |
49 | if ( type === 'classic' ) {
50 | return hidden;
51 | }
52 |
53 | return status === 'ready' ? style ?? undefined : hidden;
54 | }
55 |
--------------------------------------------------------------------------------
/tests/e2e/basic-typescript.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | require( './basic' );
4 |
--------------------------------------------------------------------------------
/tests/e2e/basic.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* eslint-disable mocha/handle-done-callback */
3 |
4 | // The following variables will be set via Nightwatch runner.
5 | const reactVersion = process.env.REQUESTED_REACT_VERSION;
6 | const localServer = process.env.NIGHTWATCH_LOCAL_SERVER;
7 | const testSample = process.env.NIGHTWATCH_TEST_SAMPLE;
8 |
9 | /**
10 | * Test suite for `samples/basic` example.
11 | */
12 | describe( `${ testSample } - react v${ reactVersion }`, () => {
13 | beforeEach( async browser => {
14 | await browser.url( localServer );
15 | await browser.waitForElementPresent( 'body', 1000 );
16 | } );
17 |
18 | test( 'requested version of React is running', async browser => {
19 | await browser.assert.visible(
20 | {
21 | selector: '//footer',
22 | locateStrategy: 'xpath'
23 | },
24 | `React v${ reactVersion }`
25 | );
26 | } );
27 |
28 | test( 'editor is visible', async browser => {
29 | await browser.assert.visible( '.cke_1' );
30 | } );
31 |
32 | test( 'editor initializes correctly', async browser => {
33 | await browser.frame( 0 );
34 | await browser.assert.containsText( '.cke_editable', 'Hello world!' );
35 | await browser.assert.visible( {
36 | selector: '//body[@contenteditable="true"]',
37 | locateStrategy: 'xpath'
38 | } );
39 | } );
40 | } );
41 |
--------------------------------------------------------------------------------
/tests/e2e/component-events.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* eslint-disable mocha/handle-done-callback */
3 |
4 | // The following variables will be set via Nightwatch runner.
5 | const reactVersion = process.env.REQUESTED_REACT_VERSION;
6 | const localServer = process.env.NIGHTWATCH_LOCAL_SERVER;
7 | const testSample = process.env.NIGHTWATCH_TEST_SAMPLE;
8 |
9 | /**
10 | * Test suite for `samples/component-events` example.
11 | */
12 | describe( `${ testSample } - react v${ reactVersion }`, () => {
13 | beforeEach( async browser => {
14 | await browser.url( localServer );
15 | await browser.waitForElementPresent( 'body', 1000 );
16 | } );
17 |
18 | test( 'requested version of React is running', async browser => {
19 | await browser.assert.visible(
20 | {
21 | selector: '//footer',
22 | locateStrategy: 'xpath'
23 | },
24 | `React v${ reactVersion }`
25 | );
26 | } );
27 |
28 | test( 'editor is visible', async browser => {
29 | await browser.assert.visible( '.cke_1' );
30 | } );
31 |
32 | test( 'editor events are logged in correct order', async browser => {
33 | await browser.assert.containsText(
34 | {
35 | selector: '//tbody/tr[4]',
36 | locateStrategy: 'xpath'
37 | },
38 | 'namespaceLoaded'
39 | );
40 | await browser.assert.containsText(
41 | {
42 | selector: '//tbody/tr[3]',
43 | locateStrategy: 'xpath'
44 | },
45 | 'beforeLoad'
46 | );
47 | await browser.assert.containsText(
48 | {
49 | selector: '//tbody/tr[2]',
50 | locateStrategy: 'xpath'
51 | },
52 | 'loaded'
53 | );
54 | await browser.assert.containsText(
55 | {
56 | selector: '//tbody/tr[1]',
57 | locateStrategy: 'xpath'
58 | },
59 | 'instanceReady'
60 | );
61 | await browser.click( '.btn' );
62 | await browser.assert.containsText(
63 | {
64 | selector: '//tbody/tr[4]',
65 | locateStrategy: 'xpath'
66 | },
67 | 'destroy'
68 | );
69 | await browser.assert.containsText(
70 | {
71 | selector: '//tbody/tr[1]',
72 | locateStrategy: 'xpath'
73 | },
74 | 'instanceReady'
75 | );
76 | await browser.frame( 0 );
77 | await browser.click( '.cke_editable' );
78 | await browser.frame( null );
79 | await browser.click( 'xpath', '//table' );
80 | await browser.assert.containsText(
81 | {
82 | selector: '//tbody/tr[1]',
83 | locateStrategy: 'xpath'
84 | },
85 | 'blur'
86 | );
87 | await browser.assert.containsText(
88 | {
89 | selector: '//tbody/tr[2]',
90 | locateStrategy: 'xpath'
91 | },
92 | 'focus'
93 | );
94 | } );
95 | } );
96 |
--------------------------------------------------------------------------------
/tests/e2e/component.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* eslint-disable mocha/handle-done-callback */
3 |
4 | // The following variables will be set via Nightwatch runner.
5 | const reactVersion = process.env.REQUESTED_REACT_VERSION;
6 | const localServer = process.env.NIGHTWATCH_LOCAL_SERVER;
7 | const testSample = process.env.NIGHTWATCH_TEST_SAMPLE;
8 | const bsBrowser = process.env.BROWSER_STACK_BROWSER;
9 |
10 | /**
11 | * Test suite for `samples/component` example.
12 | */
13 | describe( `${ testSample } - react v${ reactVersion }`, () => {
14 | beforeEach( async browser => {
15 | await browser.url( localServer );
16 | await browser.waitForElementPresent( 'body', 1000 );
17 | } );
18 |
19 | test( 'requested version of React is running', async browser => {
20 | await browser.assert.visible(
21 | {
22 | selector: '//footer',
23 | locateStrategy: 'xpath'
24 | },
25 | `React v${ reactVersion }`
26 | );
27 | } );
28 |
29 | test( 'editor is visible', async browser => {
30 | await browser.assert.visible( '.cke_1' );
31 | } );
32 |
33 | test( 'editor initializes correctly', async browser => {
34 | await browser.frame( 0 );
35 | await browser.assert.containsText(
36 | '.cke_editable',
37 | 'Hello from classic editor!'
38 | );
39 | await browser.assert.visible( {
40 | selector: '//body[@contenteditable="true"]',
41 | locateStrategy: 'xpath'
42 | } );
43 | } );
44 |
45 | test( 'editor toggles between classic and inline', async browser => {
46 | await browser.frame( 0 );
47 | await browser.assert.containsText(
48 | '.cke_editable',
49 | 'Hello from classic editor!'
50 | );
51 | await browser.frame( null );
52 | await browser.click( 'input[id=inline]' );
53 | await browser.assert.containsText(
54 | '.cke_editable_inline',
55 | 'Hello from inline editor!'
56 | );
57 | await browser.click( 'input[id=classic]' );
58 | await browser.frame( 0 );
59 | await browser.assert.containsText(
60 | '.cke_editable',
61 | 'Hello from classic editor!'
62 | );
63 | } );
64 |
65 | test( 'editor toggles read-only mode', async browser => {
66 | await browser.click( 'input[id=read-only]' );
67 | await browser.frame( 0 );
68 | await browser.assert.visible( {
69 | selector: '//body[@contenteditable="false"]',
70 | locateStrategy: 'xpath'
71 | } );
72 | await browser.frame( null );
73 | await browser.click( 'input[id=read-only]' );
74 | await browser.frame( 0 );
75 | await browser.assert.visible( {
76 | selector: '//body[@contenteditable="true"]',
77 | locateStrategy: 'xpath'
78 | } );
79 | } );
80 |
81 | test( 'editor changes style', async browser => {
82 | await browser.click( 'input[id=blue]' );
83 | await checkBorderStyle( 'blue' );
84 | await browser.click( 'input[id=green]' );
85 | await checkBorderStyle( 'green' );
86 |
87 | async function checkBorderStyle( color ) {
88 | if ( [ 'ie', 'safari' ].indexOf( bsBrowser ) !== -1 ) {
89 | await browser.assert.attributeContains(
90 | '.cke',
91 | 'style',
92 | `order: 1px solid ${ color }`
93 | );
94 | } else {
95 | await browser.assert.attributeContains(
96 | '.cke',
97 | 'style',
98 | `border-color: ${ color }`
99 | );
100 | }
101 | }
102 | } );
103 | } );
104 |
--------------------------------------------------------------------------------
/tests/e2e/hook-events.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | require( './component-events' );
4 |
--------------------------------------------------------------------------------
/tests/e2e/hook.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | require( './component' );
4 |
--------------------------------------------------------------------------------
/tests/e2e/re-order.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* eslint-disable mocha/handle-done-callback */
3 |
4 | // The following variables will be set via Nightwatch runner.
5 | const reactVersion = process.env.REQUESTED_REACT_VERSION;
6 | const localServer = process.env.NIGHTWATCH_LOCAL_SERVER;
7 | const testSample = process.env.NIGHTWATCH_TEST_SAMPLE;
8 |
9 | /**
10 | * Test suite for `samples/re-order` example.
11 | */
12 | describe( `${ testSample } - react v${ reactVersion }`, () => {
13 | beforeEach( async browser => {
14 | await browser.url( localServer );
15 | await browser.waitForElementPresent( 'body', 1000 );
16 | } );
17 |
18 | test( 'requested version of React is running', async browser => {
19 | await browser.assert.visible(
20 | {
21 | selector: '//footer',
22 | locateStrategy: 'xpath'
23 | },
24 | `React v${ reactVersion }`
25 | );
26 | } );
27 |
28 | test( 'editor is visible', async browser => {
29 | await browser.assert.visible( '.cke_1' );
30 | } );
31 |
32 | test( 'editors initialize correctly and keep their values after re-ordering', async browser => {
33 | await checkValues();
34 | await browser.click( '.btn' );
35 | await checkValues();
36 |
37 | async function checkValues() {
38 | for ( const value of [ 'toast', 'bagel', 'taco', 'avocado' ] ) {
39 | // Checks inline editor
40 | await browser.assert.containsText( `#${ value }-inline`, value );
41 |
42 | // Checks classic editor
43 | await browser.assert.visible( `#cke_${ value }` );
44 | }
45 | }
46 | } );
47 | } );
48 |
--------------------------------------------------------------------------------
/tests/e2e/ssr.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | require( './basic' );
4 |
--------------------------------------------------------------------------------
/tests/e2e/state-lifting.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* eslint-disable mocha/handle-done-callback */
3 |
4 | // The following variables will be set via Nightwatch runner.
5 | const reactVersion = process.env.REQUESTED_REACT_VERSION;
6 | const localServer = process.env.NIGHTWATCH_LOCAL_SERVER;
7 | const testSample = process.env.NIGHTWATCH_TEST_SAMPLE;
8 |
9 | /**
10 | * Test suite for `samples/state-lifting` example.
11 | */
12 | describe( `${ testSample } - react v${ reactVersion }`, () => {
13 | beforeEach( async browser => {
14 | await browser.url( localServer );
15 | await browser.waitForElementPresent( 'body', 1000 );
16 | } );
17 |
18 | test( 'requested version of React is running', async browser => {
19 | await browser.assert.visible(
20 | {
21 | selector: '//footer',
22 | locateStrategy: 'xpath'
23 | },
24 | `React v${ reactVersion }`
25 | );
26 | } );
27 |
28 | test( 'editor is visible', async browser => {
29 | await browser.assert.visible( '.cke_1' );
30 | } );
31 |
32 | test( 'editor sets data from outside - text area to editor', async browser => {
33 | await browser.setValue( 'xpath', '//textarea', 'textarea' );
34 | await browser.assert.containsText( '.preview', 'textarea' );
35 | await browser.frame( 0 );
36 | await browser.assert.containsText( '.cke_editable', 'textarea' );
37 | } );
38 |
39 | test.skip( 'editor sets data from outside - editor to text area', async browser => {
40 | // For the sake of IE11 set fake value on textarea. Otherwise `setValue` on iframe won't work.
41 | await browser.setValue( 'xpath', '//textarea', '' );
42 | await browser.frame( 0 );
43 | await browser.setValue(
44 | 'xpath',
45 | '//body[@contenteditable="true"]',
46 | 'CKEditor'
47 | );
48 | await browser.frame( null );
49 | await browser.assert.containsText(
50 | { selector: '//textarea', locateStrategy: 'xpath' },
51 | 'CKEditor'
52 | );
53 | await browser.assert.containsText( '.preview', 'CKEditor' );
54 | } );
55 | } );
56 |
--------------------------------------------------------------------------------
/tests/e2e/umd.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | require( './basic' );
4 |
--------------------------------------------------------------------------------
/tests/unit/CKEditor.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from '@testing-library/react';
3 | import {
4 | findByEditorName,
5 | findByClassicEditorContent,
6 | findByClassicEditorEditable,
7 | findByInlineEditorContent,
8 | findByInlineEditorEditable,
9 | findClassicEditor,
10 | findInlineEditor,
11 | queryClassicEditor,
12 | waitForValueToChange
13 | } from './utils';
14 | import { CKEditor, CKEditorEventHandler } from '../../src';
15 |
16 | function init() {
17 | describe( 'CKEditor', () => {
18 | /**
19 | * Ensures that classic editor is initialized in writable mode by default.
20 | */
21 | it( 'initializes classic editor', async () => {
22 | render( );
23 | expect( await findByClassicEditorEditable( true ) ).toBeVisible();
24 | } );
25 |
26 | /**
27 | * Ensures that initial data is set in classic editor.
28 | */
29 | it( 'initializes classic editor with initial data', async () => {
30 | render( );
31 | expect(
32 | await findByClassicEditorContent( 'Hello world!' )
33 | ).toBeVisible();
34 | } );
35 |
36 | /**
37 | * Ensures that initial data is set - JSX.
38 | */
39 | it( 'initializes classic editor with initial data as JSX', async () => {
40 | render( Hello world!} /> );
41 | expect(
42 | await findByClassicEditorContent( 'Hello world!' )
43 | ).toBeVisible();
44 | } );
45 |
46 | /**
47 | * Ensures that inline editor is initialized in writable mode.
48 | */
49 | it( 'initializes inline editor', async () => {
50 | render( );
51 | expect( await findByInlineEditorEditable( true ) ).toBeVisible();
52 | } );
53 |
54 | /**
55 | * Ensures that initial data is set in inline editor.
56 | */
57 | it( 'initializes inline editor with initial data', async () => {
58 | render( );
59 | await findByInlineEditorEditable( true );
60 | expect(
61 | await findByInlineEditorContent( 'Hello world!' )
62 | ).toBeVisible();
63 | } );
64 |
65 | /**
66 | * Ensures that initial data is set - JSX.
67 | */
68 | it( 'initializes inline editor with initial data as JSX', async () => {
69 | render( Hello world!} /> );
70 | await findByInlineEditorEditable( true );
71 | expect(
72 | await findByInlineEditorContent( 'Hello world!' )
73 | ).toBeVisible();
74 | } );
75 |
76 | /**
77 | * Ensures that classic editor is initialized as read-only.
78 | */
79 | it( 'initializes classic editor as read-only', async () => {
80 | render( );
81 | expect( await findByClassicEditorEditable( false ) ).toBeVisible();
82 | } );
83 |
84 | /**
85 | * Ensures that inline editor is initialized as read-only.
86 | */
87 | it( 'initializes inline editor as read-only', async () => {
88 | render( );
89 | expect( await findByInlineEditorEditable( false ) ).toBeVisible();
90 | } );
91 |
92 | /**
93 | * Ensures that `readOnly` prop has precedence over `config.readOnly`.
94 | */
95 | it( 'overrides `readOnly` in config with prop value', async () => {
96 | render( );
97 | expect( await findByClassicEditorEditable( true ) ).toBeVisible();
98 | } );
99 |
100 | /**
101 | * Ensures that editor is initialized with custom name.
102 | */
103 | it( 'sets editor name', async () => {
104 | render( );
105 | expect( await findByEditorName( 'my-editor' ) ).toBeVisible();
106 | expect(
107 | ( window as any ).CKEDITOR.instances[ 'my-editor' ]
108 | ).toBeDefined();
109 | } );
110 |
111 | /**
112 | * Ensures that styles are applied to classic editor's container.
113 | */
114 | it( 'sets custom styles for classic editor', async () => {
115 | const style = { border: '1px solid red' };
116 | render( );
117 | const el = await findClassicEditor();
118 | await waitForValueToChange( () => el.style.border === style.border );
119 | expect( el.style.border ).toEqual( style.border );
120 | } );
121 |
122 | /**
123 | * Ensures that styles are applied to inline editor's container.
124 | */
125 | it( 'sets custom styles for inline editor', async () => {
126 | const style = { border: '1px solid red' };
127 | render( );
128 | const el = await findInlineEditor();
129 | await waitForValueToChange( () => el.style.border === style.border );
130 | expect( el.style.border ).toEqual( style.border );
131 | } );
132 |
133 | /**
134 | * Ensures that read-only mode can be toggled after initialization.
135 | */
136 | it( 'sets editor as read-only after init', async () => {
137 | const { rerender } = render( );
138 | expect( await findByClassicEditorEditable( true ) ).toBeVisible();
139 | rerender( );
140 | expect( await findByClassicEditorEditable( false ) ).toBeVisible();
141 | rerender( );
142 | expect( await findByClassicEditorEditable( true ) ).toBeVisible();
143 | } );
144 |
145 | /**
146 | * Ensures that editor's style can be changed after initialization.
147 | */
148 | it( 'sets custom styles for editor after init', async () => {
149 | const style1 = { border: '1px solid red' };
150 | const { rerender } = render( );
151 | const el1 = await findClassicEditor();
152 | await waitForValueToChange(
153 | () => el1.style.border === style1.border
154 | );
155 | expect( el1.style.border ).toEqual( style1.border );
156 | const style2 = { border: '1px solid green' };
157 | rerender( );
158 | const el2 = await findClassicEditor();
159 | await waitForValueToChange(
160 | () => el2.style.border === style2.border
161 | );
162 | expect( el2.style.border ).toEqual( style2.border );
163 | } );
164 |
165 | /**
166 | * Ensures that initial data remains "initial". It should be set once.
167 | */
168 | it( 'does not change data after init', async () => {
169 | const { rerender } = render( );
170 | expect(
171 | await findByClassicEditorContent( 'Hello world!' )
172 | ).toBeVisible();
173 | rerender( );
174 | expect(
175 | await findByClassicEditorContent( 'Hello world!' )
176 | ).toBeVisible();
177 | } );
178 |
179 | /**
180 | * Ensures that new instance of editor is not created after passing new value of `editorUrl`.
181 | */
182 | it( 'does not re-initialize editor on `editorUrl` change', async () => {
183 | const { rerender } = render( );
184 | expect( queryClassicEditor() ).toBeNull();
185 | expect(
186 | await findByClassicEditorContent( 'Hello world!' )
187 | ).toBeVisible();
188 | rerender( );
189 | expect( queryClassicEditor() ).not.toBeNull();
190 | } );
191 |
192 | /**
193 | * Ensures that new instance of editor is not created after passing new value of event handler.
194 | */
195 | it( 'does not re-initialize editor on `onInstanceReady` change', async () => {
196 | const onInstanceReady = jasmine.createSpy( 'onInstanceReady' );
197 | const onInstanceReady2 = jasmine.createSpy( 'onInstanceReady2' );
198 | const { rerender } = render(
199 |
203 | );
204 | expect( queryClassicEditor() ).toBeNull();
205 | expect(
206 | await findByClassicEditorContent( 'Hello world!' )
207 | ).toBeVisible();
208 | rerender( );
209 | expect( queryClassicEditor() ).not.toBeNull();
210 | } );
211 |
212 | /**
213 | * Ensures that new instance of editor is not created after passing new value of `debug`.
214 | */
215 | it( 'does not re-initialize editor on `debug` change', async () => {
216 | const { rerender } = render(
217 |
218 | );
219 | expect( queryClassicEditor() ).toBeNull();
220 | expect(
221 | await findByClassicEditorContent( 'Hello world!' )
222 | ).toBeVisible();
223 | rerender( );
224 | expect( queryClassicEditor() ).not.toBeNull();
225 | } );
226 |
227 | /**
228 | * Ensures that new instance of editor is not created after passing new value of `type`.
229 | */
230 | it( 'does not re-initialize editor on `type` change', async () => {
231 | const { rerender } = render( );
232 | expect( queryClassicEditor() ).toBeNull();
233 | expect(
234 | await findByClassicEditorContent( 'Hello world!' )
235 | ).toBeVisible();
236 | rerender( );
237 | expect( queryClassicEditor() ).not.toBeNull();
238 | } );
239 |
240 | /**
241 | * Ensures that new instance of editor is not created after passing new `config`.
242 | */
243 | it( 'does not re-initialize editor on `config` change', async () => {
244 | const { rerender } = render( );
245 | expect( queryClassicEditor() ).toBeNull();
246 | expect(
247 | await findByClassicEditorContent( 'Hello world!' )
248 | ).toBeVisible();
249 | rerender( );
250 | expect( queryClassicEditor() ).not.toBeNull();
251 | } );
252 |
253 | /**
254 | * Ensures that `namespace` event handler `onBeforeLoad` is invoked.
255 | */
256 | it( 'invokes `onBeforeLoad` callback', async () => {
257 | const onBeforeLoad = jasmine.createSpy( 'onBeforeLoad' );
258 | render( );
259 | expect( await findClassicEditor() ).toBeVisible();
260 | expect( onBeforeLoad ).toHaveBeenCalledTimes( 1 );
261 | } );
262 |
263 | /**
264 | * Ensures that default `editor` event handlers are invoked.
265 | */
266 | it( 'invokes editor handlers for default events', async () => {
267 | const onDestroy = jasmine.createSpy( 'onDestroy' );
268 | const onLoaded = jasmine.createSpy( 'onLoaded' );
269 | const onInstanceReady = jasmine.createSpy( 'onInstanceReady' );
270 | const { unmount } = render(
271 |
277 | );
278 | expect( await findByClassicEditorEditable( true ) ).toBeVisible();
279 | unmount();
280 | expect( queryClassicEditor() ).toBeNull();
281 | expect( onLoaded ).toHaveBeenCalledTimes( 1 );
282 | expect( onInstanceReady ).toHaveBeenCalledTimes( 1 );
283 | expect( onDestroy ).toHaveBeenCalledTimes( 1 );
284 | } );
285 |
286 | /**
287 | * Ensures that custom `editor` event handlers are invoked.
288 | */
289 | it( 'invokes editor handlers for custom events', async () => {
290 | const windw = window as any;
291 | const onCustomEvent = jasmine.createSpy( 'onCustomEvent' );
292 | render(
293 | ;
295 | }>
296 | name="test"
297 | onCustomEvent={onCustomEvent}
298 | />
299 | );
300 | expect( await findClassicEditor() ).toBeVisible();
301 | windw.CKEDITOR.instances.test.fire( 'customEvent' );
302 | await waitForValueToChange( () => onCustomEvent.calls.count() === 1 );
303 | expect( onCustomEvent ).toHaveBeenCalledTimes( 1 );
304 | } );
305 | } );
306 | }
307 |
308 | export default init;
309 |
--------------------------------------------------------------------------------
/tests/unit/common.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | function init() {
4 | describe( 'common', () => {
5 | const requestedVersion = process.env.REQUESTED_REACT_VERSION;
6 |
7 | /**
8 | * Ensures that runtime version of React matches requested React version.
9 | * This test is intended to run through scripts that test multiple versions (e2e-runner, units-runner).
10 | * `process.env.REQUESTED_REACT_VERSION` variable will be set by test runner.
11 | */
12 | it( 'matches requested version', async () => {
13 | if ( requestedVersion ) {
14 | expect( React.version ).toEqual( requestedVersion );
15 | }
16 | } );
17 | } );
18 | }
19 |
20 | export default init;
21 |
--------------------------------------------------------------------------------
/tests/unit/events.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | prefixEventName,
3 | stripPrefix,
4 | eventNameToHandlerName,
5 | handlerNameToEventName
6 | } from '../../src/events';
7 |
8 | function init() {
9 | describe( 'events', () => {
10 | describe( 'prefixEventName', () => {
11 | it( 'prefixes event name', () => {
12 | expect( prefixEventName( 'instanceReady' ) ).toEqual(
13 | '__CKE__instanceReady'
14 | );
15 | } );
16 | } );
17 |
18 | describe( 'stripPrefix', () => {
19 | it( 'strips prefix', () => {
20 | expect( stripPrefix( '__CKE__instanceReady' ) ).toEqual(
21 | 'instanceReady'
22 | );
23 | } );
24 | } );
25 |
26 | describe( 'eventNameToHandlerName', () => {
27 | it( 'generates event handler name', () => {
28 | expect( eventNameToHandlerName( 'instanceReady' ) ).toEqual(
29 | 'onInstanceReady'
30 | );
31 | } );
32 | } );
33 |
34 | describe( 'handlerNameToEventName', () => {
35 | it( 'generates event name from handler name', () => {
36 | expect( handlerNameToEventName( 'onInstanceReady' ) ).toEqual(
37 | 'instanceReady'
38 | );
39 | } );
40 | } );
41 | } );
42 | }
43 |
44 | export default init;
45 |
--------------------------------------------------------------------------------
/tests/unit/index.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | // @ts-ignore
3 | import JasmineDOM from '@testing-library/jasmine-dom';
4 | import { configure } from '@testing-library/react';
5 | import initCommonTests from './common.test';
6 | import initUseCKEditorTests from './useCKEditor.test';
7 | import initCKEditorTests from './CKEditor.test';
8 | import initRegisterEditorEventHandler from './registerEditorEventHandler.test';
9 | import initUtilsTests from './utils.test';
10 | import initEventsTests from './events.test';
11 |
12 | describe( 'CKEditor4 React', () => {
13 | // Increases timeout so that CI can have a chance to capture changes.
14 | const timeout = 5000;
15 | const requestedVersion = process.env.REQUESTED_REACT_VERSION;
16 |
17 | beforeAll( () => {
18 | // Extends jasmine with custom RTL matchers.
19 | jasmine.getEnv().addMatchers( JasmineDOM );
20 |
21 | // Sets timeout for async utils in RTL globally.
22 | configure( { asyncUtilTimeout: timeout } );
23 |
24 | if ( !requestedVersion ) {
25 | console.warn(
26 | `REQUESTED_REACT_VERSION variable was not set. Runtime version of React is ${ React.version }.`
27 | );
28 | }
29 | } );
30 |
31 | initCommonTests();
32 | initUseCKEditorTests();
33 | initRegisterEditorEventHandler();
34 | initCKEditorTests();
35 | initUtilsTests();
36 | initEventsTests();
37 | } );
38 |
--------------------------------------------------------------------------------
/tests/unit/registerEditorEventHandler.test.ts:
--------------------------------------------------------------------------------
1 | import { registerEditorEventHandler } from '../../src';
2 |
3 | function init() {
4 | describe( 'registerEditorEventHandler', () => {
5 | const windw = window as any;
6 | const log = windw.console.log;
7 | const spyOnEventOn = jasmine.createSpy( 'editor.on' );
8 | const spyOnRemoveListener = jasmine.createSpy( 'editor.removeListener' );
9 | const createEditor = () => ( { on: spyOnEventOn, removeListener: spyOnRemoveListener } );
10 |
11 | afterEach( () => {
12 | spyOnEventOn.calls.reset();
13 | spyOnRemoveListener.calls.reset();
14 | windw.console.log = log;
15 | } );
16 |
17 | it( 'registers / unregisters event handler', async () => {
18 | const onInstanceReady = jasmine.createSpy( 'onInstanceReady' );
19 | const unregister = registerEditorEventHandler( {
20 | editor: createEditor(),
21 | evtName: 'instanceReady',
22 | handler: onInstanceReady
23 | } );
24 | expect( spyOnEventOn ).toHaveBeenCalledTimes( 1 );
25 | expect( spyOnEventOn ).toHaveBeenCalledWith( 'instanceReady', onInstanceReady, null, undefined, undefined );
26 | unregister();
27 | expect( spyOnRemoveListener ).toHaveBeenCalledTimes( 1 );
28 | expect( spyOnRemoveListener ).toHaveBeenCalledWith( 'instanceReady', onInstanceReady );
29 | } );
30 |
31 | it( 'turns on `debug` mode', async () => {
32 | windw.console.log = jasmine.createSpy( 'window.console.log' );
33 | const onInstanceReady = jasmine.createSpy( 'onInstanceReady' );
34 | registerEditorEventHandler( {
35 | editor: createEditor(),
36 | evtName: 'instanceReady',
37 | handler: onInstanceReady,
38 | debug: true
39 | } );
40 | expect( spyOnEventOn ).toHaveBeenCalledTimes( 1 );
41 | expect( spyOnEventOn ).toHaveBeenCalledWith( 'instanceReady', jasmine.any( Function ), null, undefined, undefined );
42 | expect( windw.console.log ).toHaveBeenCalled();
43 | } );
44 |
45 | it( 'uses listener data', async () => {
46 | const onInstanceReady = jasmine.createSpy( 'onInstanceReady' );
47 | registerEditorEventHandler( {
48 | editor: createEditor(),
49 | evtName: 'instanceReady',
50 | handler: onInstanceReady,
51 | listenerData: { foo: 'bar' }
52 | } );
53 | expect( spyOnEventOn ).toHaveBeenCalledTimes( 1 );
54 | expect( spyOnEventOn ).toHaveBeenCalledWith( 'instanceReady', onInstanceReady, null, { foo: 'bar' }, undefined );
55 | } );
56 |
57 | it( 'accepts priority', async () => {
58 | const onInstanceReady = jasmine.createSpy( 'onInstanceReady' );
59 | registerEditorEventHandler( {
60 | editor: createEditor(),
61 | evtName: 'instanceReady',
62 | handler: onInstanceReady,
63 | priority: 0
64 | } );
65 | expect( spyOnEventOn ).toHaveBeenCalledTimes( 1 );
66 | expect( spyOnEventOn ).toHaveBeenCalledWith( 'instanceReady', onInstanceReady, null, undefined, 0 );
67 | } );
68 | } );
69 | }
70 |
71 | export default init;
72 |
--------------------------------------------------------------------------------
/tests/unit/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { uniqueName, camelToKebab, getStyle } from '../../src/utils';
2 |
3 | function init() {
4 | describe( 'utils', () => {
5 | describe( 'camelToKebab', () => {
6 | it( 'transforms `camelCaseValue` into `kebab-case-value`', () => {
7 | expect( camelToKebab( 'camelCaseValue' ) ).toEqual(
8 | 'camel-case-value'
9 | );
10 | expect( camelToKebab( 'CamelCaseValue' ) ).toEqual(
11 | 'camel-case-value'
12 | );
13 | } );
14 | } );
15 |
16 | describe( 'uniqueName', () => {
17 | it( 'generates different string values', () => {
18 | /* eslint-disable-next-line no-self-compare */
19 | expect( uniqueName() === uniqueName() ).toBe( false );
20 | } );
21 | } );
22 |
23 | describe( 'getStyle', () => {
24 | it( 'returns `hidden` styles for classic editor', () => {
25 | expect( getStyle( 'classic' ) ).toEqual( {
26 | visibility: 'hidden',
27 | display: 'none'
28 | } );
29 | expect( getStyle( 'classic', 'ready' ) ).toEqual( {
30 | visibility: 'hidden',
31 | display: 'none'
32 | } );
33 | expect(
34 | getStyle( 'classic', 'ready', { border: '1px solid black' } )
35 | ).toEqual( {
36 | visibility: 'hidden',
37 | display: 'none'
38 | } );
39 | } );
40 |
41 | it( 'returns `hidden` styles for inline editor before it is ready', () => {
42 | expect( getStyle( 'inline', 'unloaded' ) ).toEqual( {
43 | visibility: 'hidden',
44 | display: 'none'
45 | } );
46 | } );
47 |
48 | it( 'returns undefined for inline editor before once it is ready', () => {
49 | expect( getStyle( 'inline', 'ready' ) ).toBeUndefined();
50 | } );
51 |
52 | it( 'returns custom container style for inline editor once it is ready', () => {
53 | expect(
54 | getStyle( 'inline', 'ready', { border: '1px solid black' } )
55 | ).toEqual( { border: '1px solid black' } );
56 | } );
57 | } );
58 | } );
59 | }
60 |
61 | export default init;
62 |
--------------------------------------------------------------------------------
/tests/unit/utils.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { getByText, render, waitFor } from '@testing-library/react';
3 |
4 | /**
5 | * Creates dummy element.
6 | *
7 | * @returns element
8 | */
9 | export function createDivElement() {
10 | const ref = React.createRef();
11 | render( );
12 | return ref.current;
13 | }
14 |
15 | /**
16 | * Waits for predicate to evaluate to true.
17 | *
18 | * In a typical use case of `@testing-library/react` we would rely on a combination of `waitFor` and `expect`.
19 | * However, such approach fails with Karma / Jasmine setup. Therefore, a custom `waitForValueToChange` is used.
20 | *
21 | * @param fn predicate
22 | * @returns resolved promise if predicate evaluates to true
23 | */
24 | export function waitForValueToChange( fn: () => boolean ) {
25 | return waitFor( () => {
26 | if ( !fn() ) {
27 | throw new Error();
28 | }
29 | } );
30 | }
31 |
32 | /**
33 | * Waits until wysiwyg `iframe` appears and checks if specified content exists within it.
34 | *
35 | * @param text editor content to find
36 | * @returns found html element wrapped in promise
37 | */
38 | export function findByClassicEditorContent( text: string ) {
39 | return waitFor( () => {
40 | const iframe = queryClassicEditorFrame();
41 | if ( !iframe?.contentWindow?.document.body ) {
42 | throw new Error();
43 | }
44 | return getByText( iframe.contentWindow.document.body, text );
45 | } );
46 | }
47 |
48 | /**
49 | * Waits until wysiwyg `iframe` appears and returns it based on `editable` flag.
50 | *
51 | * @param editable indicates if editor is editable
52 | * @returns found html element wrapped in promise
53 | */
54 | export function findByClassicEditorEditable( editable: boolean ) {
55 | return waitFor( () => {
56 | const iframe = queryClassicEditorFrame();
57 | const editableEl = iframe?.contentWindow?.document.querySelector(
58 | `[contenteditable=${ editable }]`
59 | );
60 | if ( !editableEl ) {
61 | throw new Error();
62 | }
63 | return editableEl;
64 | } );
65 | }
66 |
67 | /**
68 | * Finds editor by its name.
69 | *
70 | * @param text editor root element to find
71 | * @returns found html element wrapped in promise
72 | */
73 | export function findByEditorName( name: string ) {
74 | return waitFor( () => {
75 | const contentEl: HTMLElement | null = document.getElementById(
76 | `cke_${ name }`
77 | );
78 | if ( !contentEl ) {
79 | throw new Error();
80 | }
81 | return contentEl;
82 | } );
83 | }
84 |
85 | /**
86 | * Waits until inline editor appears and returns it based on `editable` flag.
87 | *
88 | * @param editable indicates if editor is editable
89 | * @returns found html element wrapped in promise
90 | */
91 | export function findByInlineEditorEditable( editable: boolean ) {
92 | return waitFor( () => {
93 | const editableEl = document.querySelector(
94 | `[contenteditable=${ editable }]`
95 | ) as HTMLElement;
96 | if ( !editableEl || editableEl.style.visibility === 'hidden' ) {
97 | throw new Error();
98 | }
99 | return editableEl;
100 | } );
101 | }
102 |
103 | /**
104 | * Waits until inline editor's content appears and checks if specified content exists within it.
105 | *
106 | * @param text editor content to find
107 | * @returns found html element wrapped in promise
108 | */
109 | export function findByInlineEditorContent( text: string ) {
110 | return waitFor( () => {
111 | const contentEl = queryInlineEditor();
112 | if ( !contentEl ) {
113 | throw new Error();
114 | }
115 | return getByText( contentEl, text );
116 | } );
117 | }
118 |
119 | /**
120 | * Finds classic editor.
121 | *
122 | * @returns found html element wrapped in promise
123 | */
124 | export function findClassicEditor() {
125 | return waitFor( () => {
126 | const editor = queryClassicEditor();
127 | if ( !editor ) {
128 | throw new Error();
129 | }
130 | return editor;
131 | } );
132 | }
133 |
134 | /**
135 | * Finds inline editor.
136 | *
137 | * @returns found html element wrapped in promise
138 | */
139 | export function findInlineEditor() {
140 | return waitFor( () => {
141 | const editor = queryInlineEditor();
142 | if ( !editor ) {
143 | throw new Error();
144 | }
145 | return editor;
146 | } );
147 | }
148 |
149 | /**
150 | * Queries classic editor. Returns found element, null otherwise.
151 | *
152 | * @returns found html element wrapped in promise
153 | */
154 | export function queryClassicEditor(): HTMLIFrameElement | null {
155 | return document.querySelector( '.cke' );
156 | }
157 |
158 | /**
159 | * Queries classic editor's content iframe. Returns found element, null otherwise.
160 | *
161 | * @returns found html element wrapped in promise
162 | */
163 | export function queryClassicEditorFrame(): HTMLIFrameElement | null {
164 | return document.querySelector( '.cke_wysiwyg_frame' );
165 | }
166 |
167 | /**
168 | * Queries inline editor. Returns found element, null otherwise.
169 | *
170 | * @returns found html element wrapped in promise
171 | */
172 | export function queryInlineEditor(): HTMLElement | null {
173 | return document.querySelector( '.cke_editable_inline' );
174 | }
175 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "es6",
4 | "target": "es5",
5 | "moduleResolution": "node",
6 | "strict": true,
7 | "jsx": "react"
8 | },
9 | "include": [
10 | "src"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------