├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── setup-workflows.yml │ ├── stalebot.yml │ └── update-deps.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── karma.conf.js ├── package.json ├── samples ├── index.html └── main.js ├── scripts └── bump.js ├── src ├── ckeditor.js └── index.js ├── tests ├── component.js ├── integration.js └── utils.js └── webpack.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | browser-tools: circleci/browser-tools@1.4.6 4 | 5 | workflows: 6 | test: 7 | jobs: 8 | - test 9 | 10 | jobs: 11 | test: 12 | docker: 13 | - image: cimg/node:12.22.11-browsers 14 | steps: 15 | - browser-tools/install-browser-tools 16 | - checkout 17 | - run: 18 | name: Install npm 19 | command: npm install --prefix=$HOME/.local install npm@7 -g 20 | - run: 21 | name: Install dependencies 22 | command: npm install 23 | - run: 24 | name: Run tests 25 | command: npm run test 26 | -------------------------------------------------------------------------------- /.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.js: -------------------------------------------------------------------------------- 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 | // Note: The ESLint configuration is mandatory for vue-cli. 7 | module.exports = { 8 | 'extends': 'ckeditor5', 9 | 'rules': { 10 | 'operator-linebreak': 0 11 | }, 12 | 'parserOptions': { 13 | 'ecmaVersion': 2018 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /.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 | 21 | 22 | 1. … 23 | 2. … 24 | 3. … 25 | 26 | ### Expected result 27 | 28 | *What is the expected result of the above steps?* 29 | 30 | ### Actual result 31 | 32 | *What is the actual result of the above steps?* 33 | 34 | ## Other details 35 | 36 | * Browser: … 37 | * OS: … 38 | * Integration version: … 39 | * CKEditor version: … 40 | * Installed CKEditor plugins: … 41 | -------------------------------------------------------------------------------- /.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/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 | coverage/ 2 | node_modules/ 3 | dist/ 4 | 5 | # Ignore package-lock.json 6 | # - we don't intent to force specific 3rd party dependency version via `package-lock.json` file 7 | # Such information should be specified in the package.json file. 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | node 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CKEditor 4 WYSIWYG Editor Vue 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-vue 3.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-vue 3.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-vue 3.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-vue 3.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-vue 2.4.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-vue 2.3.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-vue 2.2.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-vue 2.2.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-vue 2.2.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-vue 2.1.1 74 | 75 | Other Changes: 76 | 77 | * Updated default CDN CKEditor 4 dependency to [4.19.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4191). 78 | 79 | ## ckeditor4-vue 2.1.0 80 | 81 | Other Changes: 82 | 83 | * Updated default CDN CKEditor 4 dependency to [4.19.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4190). 84 | 85 | ## ckeditor4-vue 2.0.0 86 | 87 | Other Changes: 88 | 89 | * Updated default CDN CKEditor 4 dependency to [4.18.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4180). 90 | 91 | [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. 92 | 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). 93 | 94 | ## ckeditor4-vue 1.5.1 95 | 96 | Other Changes: 97 | 98 | * Updated year and company name in the license headers. 99 | * Updated default CDN CKEditor 4 dependency to [4.17.2](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4172). 100 | 101 | ## ckeditor4-vue 1.5.0 102 | 103 | New Features: 104 | 105 | * [#102](https://github.com/ckeditor/ckeditor4-vue/issues/102): Added support for CKEditor 4 [Delayed Editor Creation](https://ckeditor.com/docs/ckeditor4/latest/features/delayed_creation.html) feature. 106 | 107 | Other Changes: 108 | 109 | * Updated year and company name in the license headers. 110 | 111 | ## ckeditor4-vue 1.4.0 112 | 113 | Highlights: 114 | 115 | Updated dependency [`ckeditor4-integrations-common@1.0.0`](https://www.npmjs.com/package/ckeditor4-integrations-common) does not contain Promise polyfill anymore. This is a possible breaking change for any downstream package that relies on it. 116 | 117 | Note that `ckeditor4-vue` package already exposes two variants of the library (es6-compatible and a legacy one) and the polyfill will still be used to support the legacy version. 118 | 119 | Fixed Issues: 120 | 121 | * [#121](https://github.com/ckeditor/ckeditor4-vue/issues/121): Fixed: "Permission denied" in IE11. Thanks to [André Brás](https://github.com/whity)! 122 | 123 | Other Changes: 124 | 125 | * Updated [`ckeditor4-integrations-common`](https://www.npmjs.com/package/ckeditor4-integrations-common) package to `1.0.0` version. 126 | * Updated default CDN CKEditor 4 dependency to [4.17.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4171). 127 | 128 | ## ckeditor4-vue 1.3.2 129 | 130 | Other Changes: 131 | 132 | * Updated default CDN CKEditor 4 dependency to [4.16.2](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4162). 133 | 134 | ## ckeditor4-vue 1.3.1 135 | 136 | Other Changes: 137 | 138 | * Updated default CDN CKEditor 4 dependency to [4.16.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4161). 139 | 140 | ## ckeditor4-vue 1.3.0 141 | 142 | Other Changes: 143 | 144 | * Updated default CDN CKEditor 4 dependency to [4.16.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-416). 145 | * Updated [`ckeditor4-integrations-common`](https://www.npmjs.com/package/ckeditor4-integrations-common) package to `0.2.0` version. 146 | * Updated year in license headers. 147 | 148 | ## ckeditor4-vue 1.2.0 149 | 150 | New Features: 151 | 152 | * [#61](https://github.com/ckeditor/ckeditor4-vue/issues/61): Exposed `@namespaceloaded` event fired when [`CKEDITOR` namespace](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html) is loaded, which can be used for its easier customization. 153 | * [#52](https://github.com/ckeditor/ckeditor4-vue/issues/52): Improved typing performance by adding [customizable throttling option](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_vue.html#throttle). 154 | 155 | Other Changes: 156 | 157 | * Updated default CDN CKEditor 4 dependency to [4.15.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4151). 158 | 159 | ## ckeditor4-vue 1.1.0 160 | 161 | Other Changes: 162 | 163 | * Updated default CDN CKEditor 4 dependency to [4.15.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-415). 164 | 165 | ## ckeditor4-vue 1.0.1 166 | 167 | Other Changes: 168 | 169 | * Updated default CDN CKEditor 4 dependency to [4.14.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4141). 170 | 171 | ## ckeditor4-vue 1.0.0 172 | 173 | The first stable release of the CKEditor 4 WYSIWYG Editor Vue Integration. After a few months of the beta phase, testing and listening to community feedback, the CKEditor 4 Vue Integration is stable and can be used with full confidence. Enjoy! 174 | 175 | Fixed Issues: 176 | 177 | * [#32](https://github.com/ckeditor/ckeditor4-vue/issues/32): Fixed: Watchers may interrupt the editor initialization. Thanks to [Michael Babker](https://github.com/mbabker)! 178 | 179 | ## ckeditor4-vue 0.2.0 180 | 181 | Other Changes: 182 | 183 | * Updated the default CKEditor 4 CDN dependency to [4.14.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-414). 184 | 185 | ## ckeditor4-vue 0.1.0 186 | 187 | The first beta release of CKEditor 4 WYSIWYG Editor Vue Integration. 188 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Software License Agreement 2 | ========================== 3 | 4 | ## Software License Agreement for CKEditor 4 LTS Vue component (3.0 and above) 5 | 6 | **CKEditor 4 component for Vue.js** – https://github.com/ckeditor/ckeditor4-vue
7 | Copyright (c) 2003-2025, CKSource 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 Vue component 2.4.* and below 12 | 13 | **CKEditor 4 component for Vue.js** – https://github.com/ckeditor/ckeditor4-vue
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 | Trademarks 29 | ---------- 30 | 31 | **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. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CKEditor 4 WYSIWYG editor component for Vue.js [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Check%20out%20CKEditor%204%20Vue%20integration&url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fckeditor4-vue) 2 | 3 | [![npm version](https://badge.fury.io/js/ckeditor4-vue.svg)](https://www.npmjs.com/package/ckeditor4-vue) 4 | [![GitHub tag](https://img.shields.io/github/tag/ckeditor/ckeditor4-vue.svg)](https://github.com/ckeditor/ckeditor4-vue) 5 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/ckeditor/ckeditor4-vue/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/ckeditor/ckeditor4-vue/tree/master) 6 | 7 | [![Join newsletter](https://img.shields.io/badge/join-newsletter-00cc99.svg)](http://eepurl.com/c3zRPr) 8 | [![Follow Twitter](https://img.shields.io/badge/follow-twitter-00cc99.svg)](https://twitter.com/ckeditor) 9 | 10 | ## ⚠️ CKEditor 4: End of Life and Extended Support Model until Dec 2028 11 | 12 | CKEditor 4 was launched in 2012 and reached its End of Life (EOL) on June 30, 2023. 13 | 14 | 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**. 15 | 16 | With CKEditor 4 LTS, security updates and critical bug fixes are guaranteed until December 2028. 17 | 18 | ## About this repository 19 | 20 | ### Master branch = CKEditor 4 LTS Vue Component 21 | 22 | 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. 23 | 24 | This repository now contains the source code of CKEditor 4 LTS Vue Component that is protected by copyright law. 25 | 26 | ### Getting CKEditor 4 (Open Source) 27 | 28 | You may continue using CKEditor Vue Component 2.4.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. 29 | 30 | In order to download the open source version of CKEditor 4 Vue Component, use ****tags 2.4.0 and below****. CKEditor Vue Component 2.4.0 was the last version available under the open source license terms. 31 | 32 | ## About this package 33 | 34 | The official [CKEditor 4](https://ckeditor.com/ckeditor-4/) WYSIWYG editor component for Vue.js. 35 | 36 | 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-vue/issues/new). 37 | 38 | ![CKEditor 4 screenshot](https://c.cksource.com/a/1/img/npm/ckeditor4.png) 39 | 40 | ## Installation and usage 41 | 42 | To install the CKEditor 4 component for Vue.js from npm, simply run: 43 | 44 | ``` 45 | npm install ckeditor4-vue 46 | ``` 47 | 48 | Then use it by calling the `Vue.use()` method: 49 | 50 | ```js 51 | import Vue from 'vue'; 52 | import CKEditor from 'ckeditor4-vue'; 53 | 54 | Vue.use( CKEditor ); 55 | 56 | new Vue( { 57 | // ... options 58 | } ) 59 | ``` 60 | 61 | And use the `` component in your template: 62 | 63 | ```html 64 | 69 | ``` 70 | 71 | Instead of using ES6 imports, the component can also be added via a direct script include: 72 | 73 | ```html 74 | 75 | ``` 76 | 77 | and used in the same way as with ES6 imports: 78 | 79 | ```js 80 | Vue.use( CKEditor ); 81 | ``` 82 | 83 | Refer to the official [CKEditor 4 Vue component documentation](http://ckeditor.com/docs/ckeditor4/latest/guide/dev_vue.html#basic-usage) for more information about the installation process. 84 | 85 | ## Documentation and examples 86 | 87 | See the [CKEditor 4 WYSIWYG Editor Vue Integration](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_vue.html) article in the [CKEditor 4 documentation](https://ckeditor.com/docs/ckeditor4/latest). 88 | 89 | You can also check out the [CKEditor 4 WYSIWYG Editor Vue Integration samples](https://ckeditor.com/docs/ckeditor4/latest/examples/vue.html) in [CKEditor 4 Examples](https://ckeditor.com/docs/ckeditor4/latest/examples/). 90 | 91 | ## Browser support 92 | 93 | The CKEditor 4 Vue component works with all the [supported browsers](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_browsers.html#officially-supported-browsers) except for Internet Explorer. 94 | 95 | To enable Internet Explorer 11 support, instead of the standard import you need to import a specific `dist/legacy.js` file containing all required polyfills: 96 | 97 | ```js 98 | import CKEditor from 'ckeditor4-vue/dist/legacy.js' 99 | ``` 100 | 101 | **Note**: Even though CKEditor 4 supports older Internet Explorer versions including IE8, IE9 and IE10, the Vue integration is only supported in the latest Internet Explorer 11. 102 | 103 | ## Contributing 104 | 105 | After cloning this repository, install necessary dependencies: 106 | 107 | ``` 108 | npm install 109 | ``` 110 | 111 | ### Executing tests 112 | 113 | Run: 114 | 115 | ``` 116 | npm run test 117 | ``` 118 | 119 | If you are going to change the source files (ones located in the `src/` directory), remember about rebuilding the package. You can use `npm run develop` in order to do it automatically. 120 | 121 | ### Building the package 122 | 123 | Build a minified version of the package that is ready to be published: 124 | 125 | ``` 126 | npm run build 127 | ``` 128 | 129 | ## License 130 | 131 | Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. 132 | 133 | For full details about the license, please check the `LICENSE.md` file. 134 | 135 | ### CKEditor 4 Vue Component 2.4.0 and below for CKEditor 4 Open Source 136 | 137 | Licensed under the terms of any of the following licenses at your choice: 138 | 139 | * [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html), 140 | * [GNU Lesser General Public License Version 2.1 or later](http://www.gnu.org/licenses/lgpl.html), 141 | * [Mozilla Public License Version 1.1 or later (the "MPL")](http://www.mozilla.org/MPL/MPL-1.1.html). 142 | 143 | ### CKEditor 4 Vue Component 3.0 and above for CKEditor 4 LTS ("Long Term Support") 144 | 145 | CKEditor 4 LTS Vue Component (starting from version 3.0) is available under a commercial license only. 146 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 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 | /* eslint-env node */ 7 | 8 | const { join: joinPath } = require( 'path' ); 9 | 10 | const basePath = process.cwd(); 11 | const coverageDir = joinPath( basePath, 'coverage' ); 12 | 13 | module.exports = function( config ) { 14 | config.set( { 15 | basePath, 16 | 17 | frameworks: [ 'mocha', 'chai', 'sinon' ], 18 | 19 | files: [ 20 | 'https://cdn.ckeditor.com/4.25.1-lts/standard-all/ckeditor.js', 21 | 'tests/**/*.js' 22 | ], 23 | 24 | preprocessors: { 25 | 'tests/**/*.js': [ 'webpack' ] 26 | }, 27 | client: { 28 | mocha: { 29 | timeout: 3000 30 | }, 31 | args: [ 32 | process.env.CKEDITOR_LICENSE_KEY 33 | ] 34 | }, 35 | 36 | webpack: { 37 | mode: 'development', 38 | devtool: 'inline-source-map', 39 | 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.js$/, 44 | loader: 'babel-loader', 45 | exclude: /node_modules/, 46 | query: { 47 | compact: false, 48 | presets: [ '@babel/preset-env' ] 49 | } 50 | }, 51 | { 52 | test: /\.js$/, 53 | loader: 'istanbul-instrumenter-loader', 54 | include: /src/, 55 | exclude: [ 56 | /node_modules/ 57 | ], 58 | query: { 59 | esModules: true 60 | } 61 | } 62 | ] 63 | } 64 | }, 65 | 66 | webpackMiddleware: { 67 | noInfo: true, 68 | stats: 'minimal' 69 | }, 70 | 71 | reporters: getReporters(), 72 | 73 | coverageReporter: { 74 | reporters: [ 75 | // Prints a table after tests result. 76 | { 77 | type: 'text' 78 | }, 79 | // Generates HTML tables with the results. 80 | { 81 | dir: coverageDir, 82 | type: 'html' 83 | }, 84 | // Generates "lcov.info" file. It's used by external code coverage services. 85 | { 86 | type: 'lcovonly', 87 | dir: coverageDir 88 | } 89 | ] 90 | }, 91 | 92 | port: 9876, 93 | 94 | colors: true, 95 | 96 | logLevel: 'INFO', 97 | 98 | browsers: getBrowsers(), 99 | 100 | customLaunchers: { 101 | BrowserStack_Edge: { 102 | base: 'BrowserStack', 103 | os: 'Windows', 104 | os_version: '10', 105 | browser: 'edge' 106 | }, 107 | BrowserStack_IE11: { 108 | base: 'BrowserStack', 109 | os: 'Windows', 110 | os_version: '10', 111 | browser: 'ie', 112 | browser_version: '11.0' 113 | }, 114 | BrowserStack_Safari: { 115 | base: 'BrowserStack', 116 | os: 'OS X', 117 | os_version: 'High Sierra', 118 | browser: 'safari' 119 | } 120 | }, 121 | 122 | browserStack: { 123 | username: process.env.BROWSER_STACK_USERNAME, 124 | accessKey: process.env.BROWSER_STACK_ACCESS_KEY, 125 | build: getBuildName(), 126 | project: 'ckeditor4' 127 | }, 128 | 129 | singleRun: true, 130 | 131 | concurrency: Infinity, 132 | 133 | browserNoActivityTimeout: 0, 134 | 135 | mochaReporter: { 136 | showDiff: true 137 | } 138 | } ); 139 | }; 140 | 141 | // Formats name of the build for BrowserStack. It merges a repository name and current timestamp. 142 | // If env variable `CIRCLE_PROJECT_REPONAME` is not available, the function returns `undefined`. 143 | // 144 | // @returns {String|undefined} 145 | function getBuildName() { 146 | const repoName = process.env.CIRCLE_PROJECT_REPONAME; 147 | 148 | if ( !repoName ) { 149 | return; 150 | } 151 | 152 | const repositoryName = repoName.replace( /-/g, '_' ); 153 | const date = new Date().getTime(); 154 | 155 | return `${ repositoryName } ${ date }`; 156 | } 157 | 158 | /** 159 | * Returns the value of Karma's browser option. 160 | * 161 | * @param {Array.} browsers 162 | * @returns {Array.|null} 163 | */ 164 | function getBrowsers() { 165 | if ( shouldEnableBrowserStack() ) { 166 | return [ 167 | 'Chrome', 168 | 'Firefox', 169 | 'BrowserStack_Safari', 170 | 'BrowserStack_Edge', 171 | 'BrowserStack_IE11' 172 | ]; 173 | } 174 | 175 | return [ 176 | 'Chrome' 177 | // 'Firefox' 178 | ]; 179 | } 180 | 181 | function getReporters() { 182 | if ( shouldEnableBrowserStack() ) { 183 | return [ 184 | 'mocha', 185 | 'BrowserStack', 186 | 'coverage' 187 | ]; 188 | } 189 | 190 | return [ 191 | 'mocha', 192 | 'coverage' 193 | ]; 194 | } 195 | 196 | function shouldEnableBrowserStack() { 197 | if ( !process.env.BROWSER_STACK_USERNAME || !process.env.BROWSER_STACK_ACCESS_KEY ) { 198 | return false; 199 | } 200 | 201 | // If the CIRCLE_PR_REPONAME variable is set, it indicates that the PR comes from the forked repo. 202 | // For such builds, BrowserStack will be disabled. Read more: https://github.com/ckeditor/ckeditor5-dev/issues/358. 203 | return !( 'CIRCLE_PR_REPONAME' in process.env ); 204 | } 205 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ckeditor4-vue", 3 | "version": "3.2.1", 4 | "main": "dist/ckeditor.js", 5 | "files": [ 6 | "dist/", 7 | "samples/" 8 | ], 9 | "devDependencies": { 10 | "@babel/core": "^7.20.12", 11 | "@babel/preset-env": "^7.20.2", 12 | "@vue/test-utils": "^1.3.4", 13 | "babel-loader": "^8.3.0", 14 | "chai": "^4.3.7", 15 | "core-js": "^3.27.2", 16 | "eslint": "^8.33.0", 17 | "eslint-config-ckeditor5": "^3.1.1", 18 | "istanbul-instrumenter-loader": "^3.0.1", 19 | "karma": "^6.4.1", 20 | "karma-browserstack-launcher": "^1.6.0", 21 | "karma-chai": "^0.1.0", 22 | "karma-chrome-launcher": "^3.1.1", 23 | "karma-coverage": "^2.2.0", 24 | "karma-coveralls": "^2.0.0", 25 | "karma-firefox-launcher": "^2.1.2", 26 | "karma-mocha": "^2.0.1", 27 | "karma-mocha-reporter": "^2.2.4", 28 | "karma-sinon": "^1.0.5", 29 | "karma-sourcemap-loader": "^0.3.8", 30 | "karma-webpack": "^4.0.2", 31 | "live-server": "^1.2.2", 32 | "minimist": "^1.2.8", 33 | "mocha": "^9.2.2", 34 | "sinon": "^9.2.4", 35 | "terser-webpack-plugin": "^4.2.2", 36 | "vue": "^2.7.14", 37 | "vue-router": "^3.6.5", 38 | "vue-template-compiler": "^2.7.14", 39 | "webpack": "^4.46.0", 40 | "webpack-cli": "^4.10.0" 41 | }, 42 | "peerDependencies": { 43 | "vue": "^2.5.17" 44 | }, 45 | "dependencies": { 46 | "ckeditor4-integrations-common": "^1.0.0" 47 | }, 48 | "engines": { 49 | "node": ">=8.0.0", 50 | "npm": ">=5.7.1" 51 | }, 52 | "scripts": { 53 | "start": "npm run build && live-server --open=samples/index.html", 54 | "build": "webpack --mode production", 55 | "develop": "webpack --mode development --watch", 56 | "test": "karma start", 57 | "bump": "node ./scripts/bump.js", 58 | "preversion": "npm test", 59 | "version": "npm run build && git add -f dist/", 60 | "postversion": "git rm -r --cached dist/ && git commit -m \"Clean after release [ci skip]\" && git push origin && git push origin --tags" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "https://github.com/ckeditor/ckeditor4-vue.git" 65 | }, 66 | "keywords": [ 67 | "wysiwyg", 68 | "rich text", 69 | "editor", 70 | "rte", 71 | "html", 72 | "contentEditable", 73 | "editing", 74 | "vue", 75 | "component", 76 | "ckeditor", 77 | "ckeditor4", 78 | "ckeditor 4" 79 | ], 80 | "author": "CKSource (http://cksource.com/)", 81 | "license": "SEE LICENSE IN LICENSE.md", 82 | "bugs": { 83 | "url": "https://github.com/ckeditor/ckeditor4-vue/issues" 84 | }, 85 | "homepage": "https://github.com/ckeditor/ckeditor4-vue" 86 | } 87 | -------------------------------------------------------------------------------- /samples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CKEditor 4 – Vue.js Component – development sample 6 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |

CKEditor 4 – Vue.js Component – Development Sample

37 | 38 |
    39 |
  • Editor types
  • 40 |
  • Component events
  • 41 |
  • Two-way binding
  • 42 |
  • Delayed creation
  • 43 |
44 | 45 | 46 |
47 | 48 | 105 | 106 | 124 | 125 | 140 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /samples/main.js: -------------------------------------------------------------------------------- 1 | /* global Vue VueRouter CKEditor */ 2 | /* eslint-disable */ 3 | 4 | Vue.use( CKEditor ); 5 | 6 | var defaultMixin = { 7 | methods: { 8 | namespaceLoaded: function( ckeditorNamespace ) { 9 | // Provide license key to enable the LTS version of the editor. 10 | ckeditorNamespace.config.licenseKey = 'your-license-key'; 11 | 12 | console.log( 'CKEDITOR version: ', ckeditorNamespace.version ); 13 | } 14 | } 15 | }; 16 | 17 | var EditorTypesComponent = { 18 | name: 'editor-types', 19 | template: '#editor-types', 20 | mixins: [ defaultMixin ], 21 | data: function(){ 22 | return { 23 | readOnly: false, 24 | hide: false, 25 | show: true, 26 | editors: { 27 | classic: 'Classic editor', 28 | inline: 'Inline editor' 29 | } 30 | }; 31 | } 32 | }, 33 | eventMessages = { 34 | ready: 'Editor is ready', 35 | focus: 'Editor is focused', 36 | blur: 'Editor is blurred', 37 | input: 'Editor has changed' 38 | }, 39 | EventLoggerComponent = { 40 | name: 'component-events', 41 | template: '#component-events', 42 | mixins: [ defaultMixin ], 43 | data: function() { 44 | return { 45 | editorData: 'Check out component events.', 46 | events: [] 47 | }; 48 | }, 49 | methods: { 50 | logEvent: function( event ) { 51 | var previous = this.events[ 0 ]; 52 | var message = eventMessages[ event ]; 53 | 54 | if ( previous && event === previous.name ) { 55 | previous.counter++; 56 | 57 | var count = previous.counter > 1 ? ' (' + previous.counter + ')' : ''; 58 | 59 | previous.message = message + count; 60 | } else { 61 | if ( this.events.length > 19 ) { 62 | this.events.pop(); 63 | } 64 | 65 | this.events.unshift( { 66 | name: event, 67 | counter: 1, 68 | message: message 69 | } ); 70 | } 71 | } 72 | } 73 | }; 74 | 75 | var TwoWayBindingComponent = { 76 | name: 'data-binding', 77 | template: '#data-binding', 78 | mixins: [ defaultMixin ], 79 | data: function() { 80 | return { 81 | editorData: 'Check out how two-way data binding works.' 82 | }; 83 | } 84 | }; 85 | 86 | var DelayedCreationComponent = { 87 | name:'delayed-creation', 88 | template: '#delayed-creation', 89 | mixins: [ defaultMixin ], 90 | data: function() { 91 | return { 92 | editorContainer: null, 93 | editorTarget: null, 94 | attached: false 95 | }; 96 | }, 97 | mounted: function(){ 98 | this.editorContainer = document.getElementById( 'delayed-editor-container' ); 99 | this.editorTarget = document.getElementById( 'delayed-editor-target' ); 100 | this.editorContainer.removeChild( this.editorTarget ); 101 | }, 102 | methods: { 103 | attachAgain: function() { 104 | this.editorContainer.appendChild( this.editorTarget ); 105 | this.attached = true; 106 | } 107 | } 108 | } 109 | 110 | var routes = [ 111 | { 112 | path: '/types', 113 | component: EditorTypesComponent 114 | }, { 115 | path: '/events', 116 | component: EventLoggerComponent 117 | }, { 118 | path: '/binding', 119 | component: TwoWayBindingComponent 120 | }, { 121 | path: '/delayed', 122 | component: DelayedCreationComponent 123 | }, 124 | { 125 | path: '*', 126 | redirect: '/types' 127 | } 128 | ]; 129 | 130 | var router = new VueRouter( { routes: routes } ); 131 | 132 | new Vue( { 133 | el: '#app', 134 | router: router 135 | } ); 136 | -------------------------------------------------------------------------------- /scripts/bump.js: -------------------------------------------------------------------------------- 1 | /* global console, process, require, __dirname */ 2 | 3 | const fs = require( 'fs' ); 4 | const path = require( 'path' ); 5 | 6 | const args = process.argv; 7 | 8 | if ( !( args && args[ 2 ] && args[ 2 ].length > 2 ) ) { 9 | console.error( 'Missing CKEditor version! USAGE: npm run bump A.B.C, for example: npm run bump 4.11.5' ); 10 | process.exit( 1 ); 11 | } 12 | 13 | const version = args[ 2 ]; 14 | const cdnVersion = `${ version }-lts`; 15 | 16 | // Update the CDN link in the 'src/ckeditor.js' file. 17 | updateCdnLink( path.resolve( __dirname, '..', 'src', 'ckeditor.js' ) ); 18 | 19 | // Update the CDN link in the 'karma.conf.js' file. 20 | updateCdnLink( path.resolve( __dirname, '..', 'karma.conf.js' ) ); 21 | 22 | function updateCdnLink( path ) { 23 | const file = fs.readFileSync( path, 'utf8' ); 24 | const cdnLinkRegex = /https:\/\/cdn\.ckeditor\.com\/\d\.\d+\.\d+(?:-lts)?/g; 25 | 26 | fs.writeFileSync( path, 27 | file.replace( cdnLinkRegex, `https://cdn.ckeditor.com/${ cdnVersion }` ), 'utf8' ); 28 | } 29 | -------------------------------------------------------------------------------- /src/ckeditor.js: -------------------------------------------------------------------------------- 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 | /* global CKEDITOR */ 7 | 8 | import { debounce, getEditorNamespace } from 'ckeditor4-integrations-common'; 9 | 10 | export default { 11 | name: 'ckeditor', 12 | 13 | render( createElement ) { 14 | return createElement( 'div', {}, [ 15 | createElement( this.tagName ) 16 | ] ); 17 | }, 18 | 19 | props: { 20 | value: { 21 | type: String, 22 | default: '' 23 | }, 24 | type: { 25 | type: String, 26 | default: 'classic', 27 | validator: type => [ 'classic', 'inline' ].includes( type ) 28 | }, 29 | editorUrl: { 30 | type: String, 31 | default: 'https://cdn.ckeditor.com/4.25.1-lts/standard-all/ckeditor.js' 32 | }, 33 | config: { 34 | type: Object, 35 | default: () => {} 36 | }, 37 | tagName: { 38 | type: String, 39 | default: 'textarea' 40 | }, 41 | readOnly: { 42 | type: Boolean, 43 | default: null // Use null as the default value, so `config.readOnly` can take precedence. 44 | }, 45 | throttle: { 46 | type: Number, 47 | default: 80 48 | } 49 | }, 50 | 51 | mounted() { 52 | getEditorNamespace( this.editorUrl, namespace => { 53 | this.$emit( 'namespaceloaded', namespace ); 54 | } ).then( () => { 55 | if ( this.$_destroyed ) { 56 | return; 57 | } 58 | 59 | const config = this.prepareConfig(); 60 | const method = this.type === 'inline' ? 'inline' : 'replace'; 61 | const element = this.$el.firstElementChild; 62 | 63 | CKEDITOR[ method ]( element, config ); 64 | } ); 65 | }, 66 | 67 | beforeDestroy() { 68 | if ( this.instance ) { 69 | this.instance.destroy(); 70 | } 71 | 72 | this.$_destroyed = true; 73 | }, 74 | 75 | watch: { 76 | value( val ) { 77 | if ( this.instance && this.instance.getData() !== val ) { 78 | this.instance.setData( val ); 79 | } 80 | }, 81 | 82 | readOnly( val ) { 83 | if ( this.instance ) { 84 | this.instance.setReadOnly( val ); 85 | } 86 | } 87 | }, 88 | 89 | methods: { 90 | prepareConfig() { 91 | const config = this.config || {}; 92 | config.on = config.on || {}; 93 | 94 | if ( config.delayIfDetached === undefined ) { 95 | config.delayIfDetached = true; 96 | } 97 | if ( this.readOnly !== null ) { 98 | config.readOnly = this.readOnly; 99 | } 100 | 101 | const userInstanceReadyCallback = config.on.instanceReady; 102 | 103 | config.on.instanceReady = evt => { 104 | this.instance = evt.editor; 105 | 106 | this.$nextTick().then( () => { 107 | this.prepareComponentData(); 108 | 109 | if ( userInstanceReadyCallback ) { 110 | userInstanceReadyCallback( evt ); 111 | } 112 | } ); 113 | }; 114 | 115 | return config; 116 | }, 117 | prepareComponentData() { 118 | const data = this.value; 119 | 120 | this.instance.fire( 'lockSnapshot' ); 121 | 122 | this.instance.setData( data, { callback: () => { 123 | this.$_setUpEditorEvents(); 124 | 125 | const newData = this.instance.getData(); 126 | 127 | // Locking the snapshot prevents the 'change' event. 128 | // Trigger it manually to update the bound data. 129 | if ( data !== newData ) { 130 | this.$once( 'input', () => { 131 | this.$emit( 'ready', this.instance ); 132 | } ); 133 | 134 | this.$emit( 'input', newData ); 135 | } else { 136 | this.$emit( 'ready', this.instance ); 137 | } 138 | 139 | this.instance.fire( 'unlockSnapshot' ); 140 | } } ); 141 | }, 142 | $_setUpEditorEvents() { 143 | const editor = this.instance; 144 | 145 | const onChange = debounce( evt => { 146 | const data = editor.getData(); 147 | 148 | // Editor#change event might be fired without an actual data change. 149 | if ( this.value !== data ) { 150 | // The compatibility with the v-model and general Vue.js concept of input–like components. 151 | this.$emit( 'input', data, evt, editor ); 152 | } 153 | }, this.throttle ); 154 | 155 | editor.on( 'change', onChange ); 156 | 157 | editor.on( 'focus', evt => { 158 | this.$emit( 'focus', evt, editor ); 159 | } ); 160 | 161 | editor.on( 'blur', evt => { 162 | this.$emit( 'blur', evt, editor ); 163 | } ); 164 | } 165 | } 166 | }; 167 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 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 CKEditorComponent from './ckeditor.js'; 7 | 8 | const CKEditor = { 9 | /** 10 | * Installs the plugin, registering the `` component. 11 | * 12 | * @param {Vue} Vue The Vue object. 13 | */ 14 | install( Vue ) { 15 | Vue.component( 'ckeditor', CKEditorComponent ); 16 | }, 17 | component: CKEditorComponent 18 | }; 19 | 20 | export default CKEditor; 21 | -------------------------------------------------------------------------------- /tests/component.js: -------------------------------------------------------------------------------- 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 | // VTU use entries, which fails for IE11 7 | import 'core-js/es/object/entries'; 8 | import sinon from 'sinon'; 9 | import Vue from 'vue'; 10 | import { mount } from '@vue/test-utils'; 11 | import { getEditorNamespace } from 'ckeditor4-integrations-common'; 12 | import CKEditorComponent from '../src/ckeditor'; 13 | import { delay, deleteCkeditorScripts, activate } from './utils'; 14 | 15 | /* global window, document */ 16 | 17 | describe( 'CKEditor Component', () => { 18 | const CKEditorNamespace = window.CKEDITOR; 19 | 20 | activate( CKEditorNamespace ); 21 | 22 | let skipReady = false; 23 | let sandbox, wrapper, component, props; 24 | 25 | beforeEach( done => { 26 | sandbox = sinon.createSandbox(); 27 | wrapper = createComponent( props ); 28 | component = wrapper.vm; 29 | 30 | sandbox.spy( CKEditorNamespace, 'replace' ); 31 | sandbox.spy( CKEditorNamespace, 'inline' ); 32 | 33 | if ( skipReady ) { 34 | done(); 35 | } else { 36 | component.$once( 'ready', () => { 37 | done(); 38 | } ); 39 | } 40 | } ); 41 | 42 | afterEach( () => { 43 | skipReady = false; 44 | 45 | wrapper.destroy(); 46 | sandbox.restore(); 47 | } ); 48 | 49 | after( () => { 50 | window.CKEDITOR = CKEditorNamespace; 51 | } ); 52 | 53 | describe( 'initialization', () => { 54 | it( 'component should have a name', () => { 55 | expect( CKEditorComponent.name ).to.equal( 'ckeditor' ); 56 | } ); 57 | 58 | it( 'should render', () => { 59 | expect( wrapper.html() ).to.not.be.empty; 60 | } ); 61 | 62 | describe( 'property', () => { 63 | [ { 64 | property: 'value', 65 | defaultValue: '' 66 | }, { 67 | property: 'type', 68 | defaultValue: 'classic' 69 | }, { 70 | property: 'editorUrl', 71 | defaultValueRegex: /https:\/\/cdn\.ckeditor\.com\/4\.\d{1,2}\.\d{1,2}(-lts)?\/(standard|basic|full)(-all)?\/ckeditor\.js/ 72 | }, { 73 | property: 'config', 74 | defaultValue: undefined 75 | }, { 76 | property: 'throttle', 77 | defaultValue: 80 78 | }, { 79 | }, { 80 | property: 'tagName', 81 | defaultValue: 'textarea' 82 | }, { 83 | property: 'readOnly', 84 | defaultValue: null 85 | } ].forEach( ( { property, defaultValue, defaultValueRegex } ) => { 86 | it( `"${ property }" should have default value`, () => { 87 | if ( defaultValue ) { 88 | expect( component[ property ] ).to.equal( defaultValue ); 89 | } 90 | 91 | if ( defaultValueRegex ) { 92 | expect( component[ property ] ).to.match( defaultValueRegex ); 93 | } 94 | } ); 95 | } ); 96 | } ); 97 | 98 | // Repeat description, so test are nicely grouped in the output, 99 | // but keep them separate in code, because they need different setup. 100 | describe( 'property', () => { 101 | [ { 102 | property: 'value', 103 | value: 'foo' 104 | }, { 105 | property: 'type', 106 | value: 'inline' 107 | }, { 108 | property: 'editorUrl', 109 | value: 'https://cdn.ckeditor.com/4.10.0/basic-all/ckeditor.js' 110 | }, { 111 | property: 'config', 112 | value: {} 113 | }, { 114 | property: 'throttle', 115 | value: 200 116 | }, { 117 | }, { 118 | property: 'tagName', 119 | value: 'div' 120 | }, { 121 | property: 'readOnly', 122 | value: true 123 | } ].forEach( ( { property, value } ) => { 124 | setPropsForTestGroup( { [ property ]: value } ); 125 | 126 | it( `"${ property }" should be configurable`, () => { 127 | expect( component[ property ] ).to.equal( value ); 128 | } ); 129 | } ); 130 | } ); 131 | 132 | [ { 133 | readOnly: true, 134 | config: { readOnly: false } 135 | }, { 136 | readOnly: false, 137 | config: { readOnly: true } 138 | } ].forEach( ( { readOnly, config } ) => { 139 | describe( `when component.readOnly = ${ readOnly } and config.readOnly = ${ config.readOnly }`, () => { 140 | setPropsForTestGroup( { readOnly, config } ); 141 | 142 | it( 'should use component.readOnly', () => { 143 | expect( CKEditorNamespace.replace.lastCall.args[ 1 ] ).to.include( { readOnly } ); 144 | } ); 145 | } ); 146 | } ); 147 | 148 | describe( 'when editor type', () => { 149 | [ { 150 | type: 'unset', 151 | method: 'replace' 152 | }, { 153 | type: 'classic', 154 | method: 'replace' 155 | }, { 156 | type: 'inline', 157 | method: 'inline' 158 | } ].forEach( ( { type, method } ) => { 159 | describe( type === 'unset' ? 'unset' : `set to "${ type }"`, () => { 160 | const config = { foo: 'bar' }; 161 | 162 | if ( type !== 'unset' ) { 163 | setPropsForTestGroup( { type } ); 164 | } else { 165 | type = 'classic'; 166 | } 167 | 168 | setPropsForTestGroup( { config } ); 169 | 170 | it( `"component.type" should be "${ type }"`, () => { 171 | expect( component.type ).to.equal( type ); 172 | } ); 173 | 174 | it( `should call "CKEDITOR.${ method }" with given config`, () => { 175 | sinon.assert.calledOnce( CKEditorNamespace[ method ] ); 176 | 177 | expect( CKEditorNamespace[ method ].lastCall.args[ 1 ] ).to.include( config ); 178 | } ); 179 | } ); 180 | } ); 181 | 182 | describe( 'set to invalid value', () => { 183 | it( 'should be disallowed by validator', () => { 184 | expect( component.$options.props.type.validator( 'foo' ) ).to.be.false; 185 | } ); 186 | } ); 187 | } ); 188 | } ); 189 | 190 | describe( 'events', () => { 191 | [ 'focus', 'blur' ].forEach( evtName => { 192 | it( `should emit "${ evtName }"`, () => { 193 | const spy = sandbox.spy(); 194 | component.$on( evtName, spy ); 195 | component.instance.fire( evtName ); 196 | sinon.assert.calledOnce( spy ); 197 | } ); 198 | } ); 199 | 200 | it( 'should emit "input"', () => { 201 | const spy = sandbox.spy(); 202 | const stub = sandbox.stub( component.instance, 'getData' ).returns( '

foo

' ); 203 | const clock = sinon.useFakeTimers(); 204 | 205 | component.$on( 'input', spy ); 206 | 207 | // Change event is throttled for 80ms. 208 | component.instance.fire( 'change' ); 209 | clock.tick( 20 ); 210 | 211 | component.instance.fire( 'change' ); 212 | clock.tick( 40 ); 213 | 214 | component.instance.fire( 'change' ); 215 | clock.tick( 80 ); 216 | 217 | sinon.assert.calledOnce( spy ); 218 | 219 | // Let's verify what happens when change event occur 220 | // without real value change. 221 | stub.returns( component.value ); 222 | component.instance.fire( 'change' ); 223 | clock.tick( 80 ); 224 | 225 | sinon.assert.calledOnce( spy ); 226 | 227 | clock.restore(); 228 | } ); 229 | 230 | describe( 'with custom throttle value', () => { 231 | setPropsForTestGroup( { throttle: 200 } ); 232 | 233 | it( 'should throttle "input" for the given timeout', () => { 234 | const spy = sandbox.spy(); 235 | const clock = sinon.useFakeTimers(); 236 | 237 | sandbox.stub( component.instance, 'getData' ).returns( '

bar

' ); 238 | 239 | component.$on( 'input', spy ); 240 | 241 | component.instance.fire( 'change' ); 242 | component.instance.fire( 'change' ); 243 | component.instance.fire( 'change' ); 244 | 245 | clock.tick( 50 ); 246 | 247 | component.instance.fire( 'change' ); 248 | component.instance.fire( 'change' ); 249 | component.instance.fire( 'change' ); 250 | 251 | clock.tick( 100 ); 252 | 253 | component.instance.fire( 'change' ); 254 | component.instance.fire( 'change' ); 255 | component.instance.fire( 'change' ); 256 | 257 | clock.tick( 200 ); 258 | 259 | sinon.assert.calledOnce( spy ); 260 | clock.restore(); 261 | } ); 262 | } ); 263 | } ); 264 | 265 | describe( 'when component destroyed', () => { 266 | beforeEach( () => { 267 | sandbox.spy( component.instance, 'destroy' ); 268 | wrapper.destroy(); 269 | } ); 270 | 271 | it( 'should call "instance.destroy"', () => { 272 | sinon.assert.calledOnce( component.instance.destroy ); 273 | } ); 274 | } ); 275 | 276 | describe( 'when watched property changed before editor initialized', () => { 277 | beforeEach( () => { 278 | skipReady = true; 279 | component.instance = null; 280 | } ); 281 | 282 | describe( 'property', () => { 283 | [ 284 | { 285 | property: 'value', 286 | value: 'foobar' 287 | }, 288 | { 289 | property: 'readOnly', 290 | value: true 291 | } 292 | ].forEach( ( { property, value } ) => { 293 | it( `"${ property }" should avoid instance operations`, () => { 294 | wrapper.setProps( { 295 | [ property ]: value 296 | } ); 297 | 298 | return Vue.nextTick().then( () => { 299 | sinon.assert.pass(); 300 | } ); 301 | } ); 302 | } ); 303 | } ); 304 | } ); 305 | 306 | // This test might look a bit strange, but it's crucial to run things in proper order. 307 | describe( 'when component destroyed before getEditorNamespace resolves', () => { 308 | let resolveMockReturnedPromise, 309 | resolveMockCalled; 310 | 311 | const whenMockCalled = new Promise( res => { 312 | resolveMockCalled = res; 313 | } ); 314 | 315 | const mockReturnedPromise = new Promise( res => { 316 | resolveMockReturnedPromise = res; 317 | } ); 318 | 319 | const originalMethod = getEditorNamespace.scriptLoader; 320 | 321 | // Mock `getEditorNamespace` before component is created. 322 | before( () => { 323 | skipReady = true; 324 | 325 | getEditorNamespace.scriptLoader = () => { 326 | resolveMockCalled(); 327 | return mockReturnedPromise; 328 | }; 329 | 330 | return deleteCkeditorScripts(); 331 | } ); 332 | 333 | // When component is created. 334 | beforeEach( done => { 335 | // Wait for the mock to be called so that we are sure that `component.mounted` is called and it awaits for the promise. 336 | whenMockCalled.then( () => { 337 | wrapper.destroy(); 338 | 339 | // Wait for `component.beforeDestroy`. 340 | Vue.nextTick().then( () => { 341 | window.CKEDITOR = CKEditorNamespace; 342 | 343 | resolveMockReturnedPromise( CKEditorNamespace ); 344 | 345 | // Wait for components callback to `getEditorNamespace`. 346 | Vue.nextTick().then( done ); 347 | } ); 348 | } ); 349 | } ); 350 | 351 | after( () => { 352 | getEditorNamespace.scriptLoader = originalMethod; 353 | } ); 354 | 355 | it( 'editor shouldn\'t be initialized', () => { 356 | sinon.assert.notCalled( CKEditorNamespace.replace ); 357 | } ); 358 | } ); 359 | 360 | [ { 361 | property: 'value', 362 | value: 'foo', 363 | spyOn: [ 'setData', true ], 364 | ignore: !!CKEditorNamespace.env.ie // (#4) 365 | }, { 366 | property: 'value', 367 | value: '', 368 | spyOn: [ 'setData', false ], 369 | ignore: !!CKEditorNamespace.env.ie // (#4) 370 | }, { 371 | property: 'readOnly', 372 | value: true, 373 | spyOn: [ 'setReadOnly', true ] 374 | }, { 375 | property: 'readOnly', 376 | value: false, 377 | spyOn: [ 'setReadOnly', true ] 378 | }, { 379 | property: 'readOnly', 380 | value: null, 381 | spyOn: [ 'setReadOnly', false ] 382 | } ].forEach( ( { property, value, spyOn: [ method, spyCalled ], ignore = false } ) => { 383 | if ( ignore ) { 384 | return; 385 | } 386 | 387 | describe( `when "component.${ property }" changes to "${ value }"`, () => { 388 | let spy; 389 | 390 | beforeEach( () => { 391 | spy = sandbox.spy( component.instance, method ); 392 | wrapper.setProps( { [ property ]: value } ); 393 | return Vue.nextTick(); 394 | } ); 395 | 396 | it( `${ spyCalled ? 'should' : 'shouldn\'t' } call "instance.${ method }"`, () => { 397 | if ( spyCalled ) { 398 | sinon.assert.calledWith( spy, value ); 399 | } else { 400 | sinon.assert.notCalled( spy ); 401 | } 402 | } ); 403 | } ); 404 | } ); 405 | 406 | function setPropsForTestGroup( newProps ) { 407 | // "before" is executed before "beforeEach", so we can setup props now. 408 | before( () => { 409 | props = { ...props, ...newProps }; 410 | } ); 411 | 412 | after( () => { 413 | props = null; 414 | } ); 415 | } 416 | } ); 417 | 418 | describe( 'component on detached element', () => { 419 | let wrapper; 420 | 421 | activate( window.CKEDITOR ); 422 | 423 | afterEach( () => { 424 | wrapper.destroy(); 425 | } ); 426 | 427 | after( () => { 428 | return deleteCkeditorScripts(); 429 | } ); 430 | 431 | it( 'tries to mount component on detached element and use default interval strategy before creates', () => { 432 | const parent = document.createElement( 'div' ); 433 | // Vue will replace mount target, so we have extra parent to manipulate it. 434 | const mountTarget = document.createElement( 'div' ); 435 | parent.appendChild( mountTarget ); 436 | 437 | wrapper = createComponent( {}, mountTarget ); 438 | 439 | return delay( 100, () => { 440 | // Editor is created after namespace loads 441 | // so we need to wait for the real results 442 | expect( wrapper.vm.instance ).to.be.undefined; 443 | } ).then( () => { 444 | document.body.appendChild( parent ); 445 | } ).then( () => { 446 | return delay( 1000, () => { 447 | expect( wrapper.vm.instance ).to.be.not.null; 448 | } ); 449 | } ); 450 | } ); 451 | 452 | it( 'tries to mount component on detached element and use callback strategy', () => { 453 | const parent = document.createElement( 'div' ); 454 | // Vue will replace mount target, so we have extra parent to manipulate it. 455 | const mountTarget = document.createElement( 'div' ); 456 | parent.appendChild( mountTarget ); 457 | let createEditor; 458 | 459 | wrapper = createComponent( 460 | { 461 | config: { 462 | delayIfDetached_callback: finishCreation => { 463 | createEditor = finishCreation; 464 | } 465 | } 466 | }, 467 | mountTarget 468 | ); 469 | 470 | return delay( 100, () => { 471 | // Editor is created after namespace loads 472 | // so we need to wait for the real results 473 | expect( wrapper.vm.instance ).to.be.undefined; 474 | } ).then( () => { 475 | document.body.appendChild( parent ); 476 | createEditor(); 477 | } ).then( () => { 478 | return delay( 1000, () => { 479 | expect( wrapper.vm.instance ).to.be.not.null; 480 | } ); 481 | } ); 482 | } ); 483 | } ); 484 | 485 | function createComponent( props, mountTarget = document.body ) { 486 | const fakeParent = document.createElement( 'span' ); 487 | 488 | props = { ...props }; 489 | 490 | if ( props.config ) { 491 | props.config.observableParent = fakeParent; 492 | } else { 493 | props.config = { observableParent: fakeParent }; 494 | } 495 | 496 | return mount( CKEditorComponent, { 497 | propsData: props, 498 | attachTo: mountTarget 499 | } ); 500 | } 501 | -------------------------------------------------------------------------------- /tests/integration.js: -------------------------------------------------------------------------------- 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 | // VTU use entries, which fails for IE11 7 | import 'core-js/es/object/entries'; 8 | import Vue from 'vue'; 9 | import { mount } from '@vue/test-utils'; 10 | import CKEditor from '../src/index'; 11 | import { delay, deleteCkeditorScripts, activate } from './utils'; 12 | 13 | /* global window, document */ 14 | 15 | describe( 'Integration of CKEditor component', () => { 16 | const wrappers = []; 17 | 18 | before( () => { 19 | Vue.use( CKEditor ); 20 | } ); 21 | 22 | afterEach( () => { 23 | let wrapper; 24 | 25 | while ( ( wrapper = wrappers.pop() ) ) { 26 | wrapper.destroy(); 27 | } 28 | 29 | return deleteCkeditorScripts(); 30 | } ); 31 | 32 | it( 'should initialize classic editor', () => { 33 | return createComponent( { type: 'classic' } ).then( component => { 34 | const editor = component.instance; 35 | 36 | expect( editor.getData() ).to.equal( '

foo

\n' ); 37 | expect( editor.elementMode ).to.equal( window.CKEDITOR.ELEMENT_MODE_REPLACE ); 38 | } ); 39 | } ); 40 | 41 | it( 'should initialize inline editor', () => { 42 | return createComponent( { type: 'inline' } ).then( component => { 43 | const editor = component.instance; 44 | 45 | expect( editor.getData() ).to.equal( '

foo

\n' ); 46 | expect( editor.elementMode ).to.equal( window.CKEDITOR.ELEMENT_MODE_INLINE ); 47 | } ); 48 | } ); 49 | 50 | it( 'when component has initial data it shouldn\'t produce undo steps', () => { 51 | return createComponent( {} ).then( component => { 52 | expect( component.instance.undoManager.hasUndo ).to.equal( false ); 53 | } ); 54 | } ); 55 | 56 | it( 'should call namespace loaded directive only for the initial script load', () => { 57 | const spy = sinon.spy(); 58 | 59 | return Promise.all( [ 60 | createComponent( {}, spy ), 61 | createComponent( {}, spy ), 62 | createComponent( {}, spy ) 63 | ] ).then( () => { 64 | expect( spy.callCount ).to.equal( 1 ); 65 | } ); 66 | } ); 67 | 68 | it( 'should allow modifying global config between editors', () => { 69 | const changeLang = lang => { 70 | return ( namespace => { 71 | namespace.config.language = lang; 72 | } ); 73 | }; 74 | 75 | const expectedLang = 'fr'; 76 | 77 | return createComponent( {}, changeLang( expectedLang ) ).then( component1 => { 78 | expect( component1.instance.config.language ).to.equal( expectedLang ); 79 | return createComponent( {}, changeLang( 'en' ) ); 80 | } ).then( component2 => { 81 | expect( component2.instance.config.language ).to.equal( expectedLang ); 82 | return createComponent(); 83 | } ).then( component3 => { 84 | expect( component3.instance.config.language ).to.equal( expectedLang ); 85 | } ); 86 | } ); 87 | 88 | it( 'should use correct CKEDITOR build', () => { 89 | const basePath = 'https://cdn.ckeditor.com/4.23.0-lts/standard-all/'; 90 | 91 | return createComponent( { editorUrl: basePath + 'ckeditor.js' } ).then( comp => { 92 | expect( window.CKEDITOR.basePath ).to.equal( basePath ); 93 | } ); 94 | } ); 95 | 96 | // Because of lack of `observableParent` config option - this test needs to be at the end (#124) 97 | it( 'should initialize classic editor with default config', () => { 98 | return mountComponent( {} ).then( component => { 99 | const editor = component.instance; 100 | 101 | expect( editor.getData() ).to.equal( '

foo

\n' ); 102 | 103 | // Let's disconnect the observer in the CKE4 instance 104 | editor.setMode( 'source' ); 105 | // And wait for the effects before test case ends 106 | return delay( 500 ); 107 | } ); 108 | } ); 109 | 110 | function createComponent( props = {}, namespaceLoaded = ( () => {} ) ) { 111 | const fakeParent = window.document.createElement( 'span' ); 112 | return mountComponent( 113 | props, 114 | namespaceLoaded, 115 | { 116 | observableParent: fakeParent 117 | } 118 | ); 119 | } 120 | 121 | function mountComponent( props = {}, namespaceLoaded = ( () => {} ), config ) { 122 | return new Promise( resolve => { 123 | props = propsToString( props ); 124 | 125 | const wrapper = mount( { 126 | template: ` 127 | ` 133 | }, { 134 | attachTo: document.body, 135 | methods: { 136 | namespaceLoaded: CKEditorNamespace => { 137 | activate( CKEditorNamespace ); 138 | namespaceLoaded( CKEditorNamespace ); 139 | } 140 | }, 141 | data: () => { 142 | return { 143 | editorData: '

foo

', 144 | cfg: config 145 | }; 146 | } 147 | } ); 148 | 149 | wrappers.push( wrapper ); 150 | 151 | const component = wrapper.vm.$children[ 0 ]; 152 | 153 | component.$once( 'ready', () => { 154 | resolve( component ); 155 | } ); 156 | } ); 157 | } 158 | 159 | function propsToString( props ) { 160 | let propsValue = ''; 161 | 162 | for ( const key in props ) { 163 | propsValue += ` ${ key }="${ props[ key ] }"`; 164 | } 165 | 166 | return propsValue; 167 | } 168 | } ); 169 | -------------------------------------------------------------------------------- /tests/utils.js: -------------------------------------------------------------------------------- 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 | /* global window, document, setTimeout, __karma__ */ 7 | 8 | export function deleteCkeditorScripts() { 9 | // Give CKE4 some time for destroy actions 10 | return delay( 1000, () => { 11 | const scripts = Array.from( document.querySelectorAll( 'script' ) ); 12 | const ckeditorScripts = scripts.filter( scriptElement => { 13 | return scriptElement.src.indexOf( 'ckeditor.js' ) > -1; 14 | } ); 15 | 16 | ckeditorScripts.forEach( x => x.parentNode.removeChild( x ) ); 17 | 18 | delete window.CKEDITOR; 19 | } ); 20 | } 21 | 22 | export function delay( time, func = () => {} ) { 23 | return new Promise( resolve => { 24 | setTimeout( () => { 25 | func(); 26 | resolve(); 27 | }, time ); 28 | } ); 29 | } 30 | 31 | export function activate( CKEditorNamespace ) { 32 | CKEditorNamespace.config.licenseKey = __karma__.config.args[ 0 ]; 33 | } 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 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 | 'use strict'; 7 | 8 | /* eslint-env node */ 9 | 10 | const path = require( 'path' ); 11 | const webpack = require( 'webpack' ); 12 | const TerserWebpackPlugin = require( 'terser-webpack-plugin' ); 13 | 14 | module.exports = [ 15 | createConfig( 'ckeditor.js' ), 16 | createConfig( 'legacy.js', [ 17 | [ '@babel/preset-env', 18 | { 19 | useBuiltIns: 'usage', 20 | corejs: 3, 21 | targets: { 22 | browsers: [ 23 | 'last 2 versions', 24 | 'ie 11' 25 | ], 26 | node: 10 27 | } 28 | } 29 | ] 30 | ], [ 'core-js/es/promise' ] ) 31 | ]; 32 | 33 | function createConfig( filename, presets = [], polyfills = [] ) { 34 | return { 35 | mode: 'production', 36 | devtool: 'source-map', 37 | 38 | performance: { 39 | hints: false 40 | }, 41 | 42 | entry: [ ...polyfills, path.join( __dirname, 'src', 'index.js' ) ], 43 | 44 | output: { 45 | filename, 46 | library: 'CKEditor', 47 | path: path.join( __dirname, 'dist' ), 48 | libraryTarget: 'umd', 49 | libraryExport: 'default' 50 | }, 51 | 52 | optimization: { 53 | minimizer: [ 54 | new TerserWebpackPlugin( { 55 | sourceMap: true, 56 | terserOptions: { 57 | output: { 58 | // Preserve license comments. 59 | comments: /^!/ 60 | } 61 | } 62 | } ) 63 | ] 64 | }, 65 | 66 | plugins: [ 67 | new webpack.BannerPlugin( { 68 | banner: `/*!* 69 | * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. 70 | * For licensing, see LICENSE.md. 71 | */`, 72 | raw: true 73 | } ) 74 | ], 75 | 76 | module: { 77 | rules: [ 78 | { 79 | test: /\.js$/, 80 | loader: 'babel-loader', 81 | exclude: /node_modules/, 82 | query: { 83 | compact: false, 84 | presets 85 | } 86 | } 87 | ] 88 | } 89 | }; 90 | } 91 | --------------------------------------------------------------------------------