├── .circleci └── config.yml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── setup-workflows.yml │ ├── stalebot.yml │ └── update-deps.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package.json ├── scripts ├── build-package.js ├── bump.js └── test.js ├── src ├── .browserslistrc ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── demo-form │ │ ├── demo-form.component.css │ │ ├── demo-form.component.html │ │ ├── demo-form.component.spec.ts │ │ └── demo-form.component.ts │ ├── detachable-component │ │ ├── detachable-component.component.css │ │ ├── detachable-component.component.html │ │ ├── detachable-component.component.spec.ts │ │ └── detachable-component.component.ts │ └── simple-usage │ │ ├── simple-usage.component.css │ │ ├── simple-usage.component.html │ │ ├── simple-usage.component.spec.ts │ │ └── simple-usage.component.ts ├── assets │ └── .gitkeep ├── ckeditor │ ├── ckeditor.component.spec.ts │ ├── ckeditor.component.ts │ ├── ckeditor.module.spec.ts │ ├── ckeditor.module.ts │ ├── ckeditor.ts │ ├── ng-package.json │ └── package.json ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── test.tools.ts ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.base.json ├── tsconfig.json └── tslint.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | browser-tools: circleci/browser-tools@1.5.2 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 test 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = tab 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | 16 | -------------------------------------------------------------------------------- /.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 | 22 | 23 | 1. … 24 | 2. … 25 | 3. … 26 | 27 | ### Expected result 28 | 29 | *What is the expected result of the above steps?* 30 | 31 | ### Actual result 32 | 33 | *What is the actual result of the above steps?* 34 | 35 | ## Other details 36 | 37 | * Browser: … 38 | * OS: … 39 | * Integration version: … 40 | * CKEditor version: … 41 | * Installed CKEditor plugins: … 42 | -------------------------------------------------------------------------------- /.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 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /samples 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | local.log 37 | /typings 38 | .angular/ 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | # Ignore package-lock.json 45 | # - we don't intent to force specific 3rd party dependency version via `package-lock.json` file 46 | # Such information should be specified in the package.json file. 47 | package-lock.json 48 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CKEditor 4 Angular 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-angular 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-angular 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-angular 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-angular 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-angular 4.0.0 / 4.0.1 44 | 45 | BREAKING CHANGES: 46 | 47 | The v4.0.1 release introduces compatibility with Angular v16+. Please note that this version of Angular no longer supports Internet Explorer 11. 48 | 49 | If you want to maintain support for IE11 or haven't upgraded to Angular v16 yet, make sure to use the Angular integration in version 3.3.0. 50 | 51 | Other Changes: 52 | 53 | * [#242](https://github.com/ckeditor/ckeditor4-angular/issues/242): Updated the minimal version of Angular to `^13.4.0` to ensure compatibility with Angular 16+. Thanks to [Moez Mehri](https://github.com/Mooeeezzzz)! 54 | * Updated default CDN CKEditor 4 dependency to [4.22.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4220--4221). 55 | 56 | **Note:** Version 4.0.1 includes a patch for the distribution bundle that fixes missing support for Angular v16+ and should be used instead of v4.0.0 if you target a newer version of Angular. 57 | 58 | ## ckeditor4-angular 3.3.0 59 | 60 | Other Changes: 61 | 62 | * Updated default CDN CKEditor 4 dependency to [4.21.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4210). 63 | 64 | ## ckeditor4-angular 3.2.2 65 | 66 | Other Changes: 67 | 68 | * Updated default CDN CKEditor 4 dependency to [4.20.2](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4202). 69 | 70 | ## ckeditor4-angular 3.2.1 71 | 72 | Other Changes: 73 | 74 | * Updated default CDN CKEditor 4 dependency to [4.20.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4201). 75 | 76 | ## ckeditor4-angular 3.2.0 77 | 78 | Other Changes: 79 | 80 | * Updated default CDN CKEditor 4 dependency to [4.20](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-420). 81 | 82 | ## ckeditor4-angular 3.1.1 83 | 84 | Other Changes: 85 | 86 | * Updated default CDN CKEditor 4 dependency to [4.19.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4191). 87 | 88 | ## ckeditor4-angular 3.1.0 89 | 90 | Other Changes: 91 | 92 | * Updated default CDN CKEditor 4 dependency to [4.19.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4190). 93 | 94 | ## ckeditor4-angular 3.0.0 95 | 96 | Other Changes: 97 | 98 | * Updated default CDN CKEditor 4 dependency to [4.18.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4180). 99 | 100 | [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. 101 | 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). 102 | 103 | ## ckeditor4-angular 2.4.1 104 | 105 | Other Changes: 106 | 107 | * Updated year and company name in the license headers. 108 | * Updated default CDN CKEditor 4 dependency to [4.17.2](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4172). 109 | 110 | ## ckeditor4-angular 2.4.0 111 | 112 | New Features: 113 | 114 | * [#190](https://github.com/ckeditor/ckeditor4-angular/issues/190): Added support for CKEditor 4 [Delayed Editor Creation](https://ckeditor.com/docs/ckeditor4/latest/features/delayed_creation.html) feature. 115 | 116 | ## ckeditor4-angular 2.3.0 117 | 118 | Other Changes: 119 | 120 | * Updated default CDN CKEditor 4 dependency to [4.17.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4171). 121 | 122 | ## ckeditor4-angular 2.2.2 123 | 124 | Other Changes: 125 | 126 | * Updated default CDN CKEditor 4 dependency to [4.16.2](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4162). 127 | 128 | ## ckeditor4-angular 2.2.1 129 | 130 | Fixed Issues: 131 | 132 | * [#191](https://github.com/ckeditor/ckeditor4-angular/issues/191): Fixed: Cannot find module `ckeditor4-integrations-common` error after upgrading to `2.2.0`. 133 | 134 | Other Changes: 135 | 136 | * Updated default CDN CKEditor 4 dependency to [4.16.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4161). 137 | 138 | ## ckeditor4-angular 2.2.0 139 | 140 | New Features: 141 | 142 | * [#143](https://github.com/ckeditor/ckeditor4-angular/issues/143): 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. 143 | 144 | ## ckeditor4-angular 2.1.0 145 | 146 | Other Changes: 147 | 148 | * Updated default CDN CKEditor 4 dependency to [4.16.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-416). 149 | * Updated year in license headers. 150 | 151 | ## ckeditor4-angular 2.0.1 152 | 153 | Other Changes: 154 | 155 | * Updated default CDN CKEditor 4 dependency to [4.15.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4151). 156 | 157 | ## ckeditor4-angular 2.0.0 158 | 159 | Breaking Changes: 160 | 161 | * [#130](https://github.com/ckeditor/ckeditor4-angular/issues/130): `DIVAREA` editor type has been deprecated. Use [Div Editing Area](https://ckeditor.com/cke4/addon/divarea) plugin instead (see [migration guide](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_angular.html#using-the-div-based-editor-type)). 162 | 163 | ## ckeditor4-angular 1.3.0 164 | 165 | Other Changes: 166 | 167 | * Updated default CDN CKEditor 4 dependency to [4.15.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-415). 168 | * [#98](https://github.com/ckeditor/ckeditor4-angular/issues/98): Updated repository dependencies (no changes in the actual `ckeditor4-angular` package). 169 | * [#128](https://github.com/ckeditor/ckeditor4-angular/issues/128): Improve the stability of `getEditorNamespace()` method. 170 | 171 | ## ckeditor4-angular 1.2.2 172 | 173 | Fixed Issues: 174 | 175 | * [#110](https://github.com/ckeditor/ckeditor4-angular/issues/110): Fixed: Integration throws an error when CKEditor 4 component is removed from the DOM before CKEditor 4 is loaded. Thanks to [Benjamin Hugot](https://github.com/bhugot)! 176 | 177 | ## ckeditor4-angular 1.2.1 178 | 179 | Other Changes: 180 | 181 | * Updated the default CKEditor 4 CDN dependency to [4.14.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4141). 182 | 183 | ## ckeditor4-angular 1.2.0 184 | 185 | New Features: 186 | 187 | * [#7](https://github.com/ckeditor/ckeditor4-angular/issues/7): The CKEditor 4 Angular component now exposes more CKEditor 4 native events. Thanks to [Eduard Zintz](https://github.com/ezintz)! The newly exposed events are: 188 | * [`paste`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-paste) 189 | * [`afterPaste`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-afterPaste) 190 | * [`dragStat`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragstart) 191 | * [`dragEnd`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragend) 192 | * [`drop`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-drop) 193 | * [`fileUploadRequest`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadRequest) 194 | * [`fileUploadResponse`](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadResponse) 195 | 196 | ## ckeditor4-angular 1.1.0 197 | 198 | Other Changes: 199 | 200 | * Updated the default CKEditor 4 CDN dependency to [4.14.0](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-414). 201 | 202 | ## ckeditor4-angular 1.0.1 203 | 204 | Other Changes: 205 | 206 | * Updated the default CKEditor 4 CDN dependency to [4.13.1](https://github.com/ckeditor/ckeditor4/blob/master/CHANGES.md#ckeditor-4131). 207 | 208 | ## ckeditor4-angular 1.0.0 209 | 210 | New Features: 211 | 212 | * [#6](https://github.com/ckeditor/ckeditor4-angular/issues/6): Added support for classic (iframe-based) editor. Starting from this version classic editor is used by default. 213 | * [#40](https://github.com/ckeditor/ckeditor4-angular/pull/40): Added support for Angular 5. 214 | 215 | Fixed Issues: 216 | 217 | * [#42](https://github.com/ckeditor/ckeditor4-angular/issues/42): Fixed: The `elementRef` related error is thrown when using Angular 5. 218 | * [#54](https://github.com/ckeditor/ckeditor4-angular/issues/54): Fixed: Two-way data binding does not work when the [Undo](https://ckeditor.com/cke4/addon/undo) plugin is not present. 219 | 220 | Other Changes: 221 | 222 | * Updated the default CKEditor 4 CDN dependency to [4.13.0](https://github.com/ckeditor/ckeditor4-angular/issues/59). 223 | 224 | ## ckeditor4-angular 1.0.0-beta.2 225 | 226 | Other Changes: 227 | 228 | * Updated the default CKEditor 4 CDN dependency to [4.12.1](https://github.com/ckeditor/ckeditor4-angular/commit/2bf8a8c489f2a9ea2f2d9304e2e3d92646dbe89e). 229 | 230 | ## ckeditor4-angular 1.0.0-beta 231 | 232 | Other Changes: 233 | 234 | * [#28](https://github.com/ckeditor/ckeditor4-angular/issues/28): Updated package dev dependencies. 235 | 236 | ## ckeditor4-angular 0.1.2 237 | 238 | Other Changes: 239 | 240 | * [#20](https://github.com/ckeditor/ckeditor4-angular/issues/20): Added the "Quick Start" section to README file. 241 | * [#10](https://github.com/ckeditor/ckeditor4-angular/issues/10): Updated the LICENSE file with all required dependencies. 242 | 243 | ## ckeditor4-angular 0.1.1 244 | 245 | Other Changes: 246 | 247 | * `README.md` file improvements. 248 | 249 | ## ckeditor4-angular 0.1.0 250 | 251 | The first beta release of CKEditor 4 WYSIWYG Editor Angular Integration. 252 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Software License Agreement 2 | ========================== 3 | 4 | ## Software License Agreement for CKEditor 4 LTS Angular component (5.0 and above) 5 | 6 | **CKEditor 4 WYSIWYG editor Angular component** – https://github.com/ckeditor/ckeditor4-angular
7 | Copyright (c) 2003-2025, [CKSource](https://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 Angular component 4.0.1 and below 12 | 13 | **CKEditor 4 WYSIWYG editor Angular component** – https://github.com/ckeditor/ckeditor4-angular
14 | Copyright (c) 2003-2025, [CKSource](https://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 component for Angular under the following licenses: 34 | 35 | - load-script [MIT](https://github.com/eldargab/load-script#license) 36 | - tslib Copyright (c) 2012-2020 Microsoft [Apache-2.0](https://github.com/Microsoft/tslib/blob/master/LICENSE.txt) 37 | 38 | Trademarks 39 | ---------- 40 | 41 | **CKEditor** is a trademark of [CKSource](https://cksource.com/) Holding sp. z o.o. All other brand and product names are trademarks, registered trademarks or service marks of their respective holders. 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CKEditor 4 WYSIWYG editor Angular component [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Check%20out%20CKEditor%204%20Angular%20integration&url=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fckeditor4-angular) 2 | 3 | [![npm version](https://badge.fury.io/js/ckeditor4-angular.svg)](https://www.npmjs.com/package/ckeditor4-angular) 4 | [![GitHub tag](https://img.shields.io/github/tag/ckeditor/ckeditor4-angular.svg)](https://github.com/ckeditor/ckeditor4-angular) 5 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/ckeditor/ckeditor4-angular/tree/master.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/ckeditor/ckeditor4-angular/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 Angular 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 Angular Component that is protected by copyright law. 25 | 26 | ### Getting CKEditor 4 (Open Source) 27 | 28 | You may continue using CKEditor Angular Component 4.0.1 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 Angular Component, use ****tags 4.0.1 and below****. CKEditor Angular Component 4.0.1 was the last version available under the open source license terms. 31 | 32 | ## About this package 33 | 34 | Official [CKEditor 4](https://ckeditor.com/ckeditor-4/) WYSIWYG editor component for Angular. 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-angular/issues/new). 37 | 38 | ![CKEditor 4 screenshot](https://c.cksource.com/a/1/img/npm/ckeditor4.png) 39 | 40 | ## Usage 41 | 42 | In order to create an editor instance in Angular, install the `ckeditor4-angular` npm package as a dependency of your project: 43 | 44 | ```bash 45 | npm install --save ckeditor4-angular 46 | ``` 47 | 48 | After installing, import `CKEditorModule` to your application: 49 | 50 | ```js 51 | import { CKEditorModule } from 'ckeditor4-angular'; 52 | 53 | @NgModule( { 54 | imports: [ 55 | ... 56 | CKEditorModule, 57 | ... 58 | ], 59 | … 60 | } ) 61 | ``` 62 | 63 | You can now use the `` tag in the component template to include the rich text editor: 64 | 65 | ```html 66 | 67 | ``` 68 | 69 | The `data` attribute used in the example above is responsible for setting the editor’s data. 70 | 71 | ## Documentation and examples 72 | 73 | See the [CKEditor 4 Angular Integration](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_angular.html) article and [Angular examples](https://ckeditor.com/docs/ckeditor4/latest/examples/angular.html) in the [CKEditor 4 documentation](https://ckeditor.com/docs/ckeditor4/latest/). 74 | 75 | ## Browser support 76 | 77 | The CKEditor 4 Angular component works with all the [supported browsers](https://ckeditor.com/docs/ckeditor4/latest/guide/dev_browsers.html#officially-supported-browsers) except for Internet Explorer 8-11. 78 | 79 | ## Supported Angular versions 80 | 81 | The integration can be used together with Angular at version 5.0.0 and higher. It is an implication of Angular metadata produced for this package by the Angular builder. Note that the `package.json` used in the main repository isn't published on NPM (the production one is present in `src/ckeditor/package.json`), so there are only a few peer dependencies: 82 | 83 | * `@angular/core` >= 5.0.0 84 | * `@angular/common` >= 5.0.0 85 | * `@angular/forms` >= 5.0.0 86 | 87 | required by this package. 88 | 89 | ## Contributing 90 | 91 | Here is how you can contribute to the development of the component. Any feedback and help will be most appreciated! 92 | 93 | ### Reporting issues and feature requests 94 | 95 | All issues and feature requests should be reported in the [issues section](https://github.com/ckeditor/ckeditor4-angular/issues/new) of the official GitHub repository for the CKEditor 4 Angular integration. 96 | 97 | ### Development 98 | 99 | Clone the [CKEditor 4 Angular integration repository](https://github.com/ckeditor/ckeditor4-angular). 100 | 101 | Once you have cloned it, install dependencies by running: 102 | 103 | ```bash 104 | npm install 105 | ``` 106 | 107 | #### The structure of the repository 108 | 109 | This repository contains the following code: 110 | 111 | * `./src/ckeditor` contains the CKEditor component, 112 | * `./src/app` is a demo application using the component. 113 | 114 | #### Development server 115 | 116 | Run `ng serve` to start the development server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 117 | 118 | #### Building samples 119 | 120 | Run `ng build` to build the samples. The build artifacts will be stored in the `samples/` directory. 121 | 122 | #### Running unit tests 123 | 124 | Run `npm test` to execute unit tests via [Karma](https://karma-runner.github.io). 125 | 126 | There are two options available to alternate the testing process: 127 | 128 | * `url` / `u` - pass custom URL to Karma, for example custom CKEditor 4 build. 129 | * `watch` / `w` - tell Karma to watch for changes. 130 | 131 | For example: 132 | 133 | ``` 134 | npm run test -- -u http://localhost:5000/ckeditor.js -w 135 | ``` 136 | 137 | #### Running end-to-end tests 138 | 139 | Run `ng e2e` to execute the end-to-end tests via [Protractor](https://www.protractortest.org/). 140 | 141 | #### Publishing 142 | 143 | To build and publish the package, run `npm run publish`. 144 | 145 | You can also manually build the package with `npm run build-package` which will be stored in `dist/`. Then you can publish it with `npm publish dist/`. 146 | 147 | ## License 148 | 149 | Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. 150 | 151 | For full details about the license, please check the `LICENSE.md` file. 152 | 153 | ### CKEditor 4 Angular Component 4.0.1 and below for CKEditor 4 Open Source 154 | 155 | Licensed under the terms of any of the following licenses at your choice: 156 | 157 | * [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/gpl.html), 158 | * [GNU Lesser General Public License Version 2.1 or later](http://www.gnu.org/licenses/lgpl.html), 159 | * [Mozilla Public License Version 1.1 or later (the "MPL")](http://www.mozilla.org/MPL/MPL-1.1.html). 160 | 161 | ### CKEditor 4 Angular Component 5.0 and above for CKEditor 4 LTS ("Long Term Support") 162 | 163 | CKEditor 4 LTS Angular Component (starting from version 5.0) is available under a commercial license only. 164 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ckeditor4-angular": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "aot": true, 17 | "outputPath": "samples/ckeditor4-angular", 18 | "index": "src/index.html", 19 | "main": "src/main.ts", 20 | "polyfills": "src/polyfills.ts", 21 | "tsConfig": "src/tsconfig.app.json", 22 | "assets": [ 23 | "src/favicon.ico", 24 | "src/assets" 25 | ], 26 | "styles": [ 27 | "src/styles.css" 28 | ], 29 | "scripts": [] 30 | }, 31 | "configurations": { 32 | "production": { 33 | "fileReplacements": [ 34 | { 35 | "replace": "src/environments/environment.ts", 36 | "with": "src/environments/environment.prod.ts" 37 | } 38 | ], 39 | "optimization": true, 40 | "outputHashing": "all", 41 | "sourceMap": false, 42 | "extractCss": true, 43 | "namedChunks": false, 44 | "aot": true, 45 | "extractLicenses": true, 46 | "vendorChunk": false, 47 | "buildOptimizer": true 48 | } 49 | } 50 | }, 51 | "serve": { 52 | "builder": "@angular-devkit/build-angular:dev-server", 53 | "options": { 54 | "browserTarget": "ckeditor4-angular:build" 55 | }, 56 | "configurations": { 57 | "production": { 58 | "browserTarget": "ckeditor4-angular:build:production" 59 | } 60 | } 61 | }, 62 | "extract-i18n": { 63 | "builder": "@angular-devkit/build-angular:extract-i18n", 64 | "options": { 65 | "browserTarget": "ckeditor4-angular:build" 66 | } 67 | }, 68 | "test": { 69 | "builder": "@angular-devkit/build-angular:karma", 70 | "options": { 71 | "main": "src/test.ts", 72 | "polyfills": "src/polyfills.ts", 73 | "tsConfig": "src/tsconfig.spec.json", 74 | "karmaConfig": "src/karma.conf.js", 75 | "styles": [ 76 | "src/styles.css" 77 | ], 78 | "scripts": [], 79 | "assets": [ 80 | "src/favicon.ico", 81 | "src/assets" 82 | ] 83 | } 84 | }, 85 | "lint": { 86 | "builder": "@angular-devkit/build-angular:tslint", 87 | "options": { 88 | "tsConfig": [ 89 | "src/tsconfig.app.json", 90 | "src/tsconfig.spec.json" 91 | ], 92 | "exclude": [ 93 | "**/node_modules/**" 94 | ] 95 | } 96 | } 97 | } 98 | }, 99 | "ckeditor4-angular-e2e": { 100 | "root": "e2e/", 101 | "projectType": "application", 102 | "architect": { 103 | "e2e": { 104 | "builder": "@angular-devkit/build-angular:protractor", 105 | "options": { 106 | "protractorConfig": "e2e/protractor.conf.js", 107 | "devServerTarget": "ckeditor4-angular:serve" 108 | }, 109 | "configurations": { 110 | "production": { 111 | "devServerTarget": "ckeditor4-angular:serve:production" 112 | } 113 | } 114 | }, 115 | "lint": { 116 | "builder": "@angular-devkit/build-angular:tslint", 117 | "options": { 118 | "tsConfig": "e2e/tsconfig.e2e.json", 119 | "exclude": [ 120 | "**/node_modules/**" 121 | ] 122 | } 123 | } 124 | } 125 | } 126 | }, 127 | "defaultProject": "ckeditor4-angular" 128 | } 129 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.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 { AppPage } from './app.po'; 7 | import { element, by, protractor, WebElement } from 'protractor'; 8 | 9 | describe( 'workspace-project App', () => { 10 | let page: AppPage, 11 | editables: WebElement[]; 12 | 13 | beforeEach( () => { 14 | page = new AppPage; 15 | } ); 16 | 17 | describe( 'simple-usage', () => { 18 | beforeEach( () => { 19 | page.navigateTo( 'simple-usage' ); 20 | } ); 21 | 22 | beforeEach( async () => { 23 | editables = [ await page.getEditable(), await page.getInlineEditable() ]; 24 | } ); 25 | 26 | it( 'should display welcome message', () => { 27 | expect( page.getParagraphText() ).toEqual( 'CKEditor 4 integration with Angular' ); 28 | } ); 29 | 30 | it( 'should display editor with initial content', () => { 31 | editables.forEach( editable => expect( page.getHtmlString( editable ) ) 32 | .toBe( '

Getting used to an entirely different culture can be challeng' + 33 | 'ing. While it’s also nice to learn about cultures online or from books, nothing comes close to experiencing cultural d' + 34 | 'iversity in person. You learn to appreciate each and every single one of the differences while you become more cultura' + 35 | 'lly fluid.

' ) 36 | ); 37 | } ); 38 | 39 | describe( 'typing', () => { 40 | it( `in editor1 should update editors content`, testTyping( 0 ) ); 41 | it( `in editor2 should update editors content`, testTyping( 1 ) ); 42 | } ); 43 | } ); 44 | 45 | describe( 'demo-forms', () => { 46 | beforeEach( () => { 47 | page.navigateTo( 'forms' ); 48 | } ); 49 | 50 | beforeEach( async () => { 51 | editables = [ await page.getEditable() ]; 52 | } ); 53 | 54 | it( 'should display editor with initial content', () => { 55 | expect( page.getHtmlString( editables[ 0 ] ) ) 56 | .toBe( '

A really nice fellow.

' ); 57 | } ); 58 | 59 | it( `typing should update editor content`, testTyping( 0 ) ); 60 | } ); 61 | 62 | describe( 'detachable-component', () => { 63 | beforeEach( () => { 64 | page.navigateTo( 'detachable' ); 65 | } ); 66 | 67 | it( 'should allow to attach editor with initial content', async () => { 68 | const button = element( by.css( 'button' ) ); 69 | 70 | button.click(); 71 | 72 | const editable = await page.getEditable(); 73 | 74 | expect( page.getHtmlString( editable ) ) 75 | .toBe( '

Hi, I am CKEditor 4!

' ); 76 | } ); 77 | } ); 78 | 79 | function testTyping( elementIndex: number ) { 80 | return async function() { 81 | const text = 'Foo! Bar?'; 82 | 83 | await page.updateValue( editables[ elementIndex ], text ); 84 | 85 | editables.forEach( item => { 86 | expect( page.getHtmlString( item ) ) 87 | .toBe( '

Foo! Bar?

' ); 88 | } ); 89 | }; 90 | } 91 | } ); 92 | -------------------------------------------------------------------------------- /e2e/src/app.po.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 { protractor, browser, by, element, WebElement, ElementFinder } from 'protractor'; 7 | 8 | export class AppPage { 9 | navigateTo( target: string = '' ) { 10 | return browser.get( `/${target}` ); 11 | } 12 | 13 | getParagraphText() { 14 | return element( by.css( 'app-root h1' ) ).getText(); 15 | } 16 | 17 | async waitForElement( el: ElementFinder ) { 18 | return browser.wait( protractor.ExpectedConditions.presenceOf( el ) ).then( () => el ); 19 | } 20 | 21 | async getEditable() { 22 | return this.getElementByCss( '.cke_editable:not(.cke_editable_inline)' ); 23 | } 24 | 25 | async getInlineEditable() { 26 | return this.getElementByCss( '.cke_editable_inline' ); 27 | } 28 | 29 | async getElementByCss( query ) { 30 | const el = await element( by.css( query ) ); 31 | await this.waitForElement( el ); 32 | return el; 33 | } 34 | 35 | async getHtmlString( el: WebElement ) { 36 | return el.getAttribute( 'innerHTML' ); 37 | } 38 | 39 | async updateValue( el: WebElement, text: string ) { 40 | await el.clear(); 41 | await el.click(); 42 | 43 | for ( let i = 0; i < text.length; i++ ) { 44 | await el.sendKeys( text.charAt( i ) ); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ckeditor4-angular", 3 | "version": "5.2.1", 4 | "description": "Official CKEditor 4 component for Angular.", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "build-package": "node ./scripts/build-package.js", 10 | "test": "node ./scripts/test.js", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e", 13 | "bump": "node ./scripts/bump.js", 14 | "publish:dry": "npm publish dist/ --dry-run", 15 | "publish": "npm publish dist/", 16 | "preversion": "npm run test", 17 | "version": "npm run build-package && git add -f dist/", 18 | "postversion": "git rm -r --cached dist/ && git commit -m \"Clean after release [ci skip]\" && git push origin && git push origin --tags" 19 | }, 20 | "author": "CKSource (https://cksource.com/)", 21 | "license": "SEE LICENSE IN LICENSE.md", 22 | "homepage": "https://ckeditor.com/", 23 | "bugs": "https://github.com/ckeditor/ckeditor4-angular/issues", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/ckeditor/ckeditor4-angular.git" 27 | }, 28 | "private": true, 29 | "dependencies": { 30 | "ckeditor4-integrations-common": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "^13.3.11", 34 | "@angular/animations": "^13.4.0", 35 | "@angular/cli": "^13.3.11", 36 | "@angular/common": "^13.4.0", 37 | "@angular/compiler": "^13.4.0", 38 | "@angular/compiler-cli": "^13.4.0", 39 | "@angular/core": "^13.4.0", 40 | "@angular/forms": "^13.4.0", 41 | "@angular/language-service": "^13.4.0", 42 | "@angular/platform-browser": "^13.4.0", 43 | "@angular/platform-browser-dynamic": "^13.4.0", 44 | "@angular/router": "^13.4.0", 45 | "@types/jasmine": "^4.3.1", 46 | "@types/jasminewd2": "^2.0.10", 47 | "@types/node": "18.11.9", 48 | "browserslist": "^4.21.5", 49 | "classlist.js": "^1.1.20150312", 50 | "codelyzer": "^6.0.2", 51 | "core-js": "3.28", 52 | "fs-extra": "^10.1.0", 53 | "jasmine-core": "^4.5.0", 54 | "jasmine-spec-reporter": "^7.0.0", 55 | "karma": "^6.4.1", 56 | "karma-browserstack-launcher": "^1.6.0", 57 | "karma-chrome-launcher": "~3.1.0", 58 | "karma-coverage-istanbul-reporter": "~3.0.3", 59 | "karma-firefox-launcher": "^2.1.2", 60 | "karma-jasmine": "^4.0.2", 61 | "karma-jasmine-html-reporter": "^1.7.0", 62 | "karma-spec-reporter": "^0.0.36", 63 | "ng-packagr": "^13.3.1", 64 | "protractor": "~7.0.0", 65 | "rxjs": "^7.8.0", 66 | "ts-node": "^10.9.1", 67 | "tslib": "^2.5.0", 68 | "tslint": "^6.1.3", 69 | "typescript": "4.6.4", 70 | "wait-until-promise": "^1.0.0", 71 | "zone.js": "^0.11.8" 72 | }, 73 | "browserslist": [ 74 | "> 0.5%", 75 | "last 2 versions", 76 | "Firefox ESR", 77 | "IE 11" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /scripts/build-package.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 | const fs = require( 'fs-extra' ), 9 | childProcess = require( 'child_process' ), 10 | path = require( 'path' ), 11 | 12 | // Build package using ng-packagr. 13 | output = childProcess.execSync( 'ng-packagr -p src/ckeditor/ng-package.json' ); 14 | 15 | console.log( output.toString() ); 16 | 17 | // And copy markdown files. 18 | const filesToCopy = [ 19 | 'CHANGELOG.md', 20 | 'LICENSE.md', 21 | 'README.md' 22 | ]; 23 | 24 | for ( const file of filesToCopy ) { 25 | const src = path.join( process.cwd(), file ); 26 | const dest = path.join( process.cwd(), 'dist', file ); 27 | 28 | fs.copyFileSync( src, dest ); 29 | } 30 | 31 | // Update the version of package in dist/package.json 32 | const srcPackageJsonPath = path.join( process.cwd(), 'package.json' ), 33 | distPackageJsonPath = path.join( process.cwd(), 'dist', 'package.json' ), 34 | 35 | srcPackageJson = fs.readJsonSync( srcPackageJsonPath ), 36 | distPackageJson = fs.readJsonSync( distPackageJsonPath ); 37 | 38 | distPackageJson.version = srcPackageJson.version; 39 | 40 | fs.writeJsonSync( distPackageJsonPath, distPackageJson, { spaces: 2 } ); 41 | -------------------------------------------------------------------------------- /scripts/bump.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 | /* global process, require, __dirname */ 9 | 10 | const fs = require( 'fs' ); 11 | const path = require( 'path' ); 12 | 13 | const args = process.argv; 14 | 15 | if ( !( args && args[ 2 ] && args[ 2 ].length > 2 ) ) { 16 | console.error( 'Missing CKEditor version! USAGE: npm run bump A.B.C, for example: npm run bump 4.11.5' ); 17 | process.exit( 1 ); 18 | } 19 | 20 | const version = args[ 2 ]; 21 | 22 | // Update the CDN link in the 'src/ckeditor/ckeditor.component.ts file. 23 | updateCdnLink( path.resolve( __dirname, '..', 'src', 'ckeditor', 'ckeditor.component.ts' ) ); 24 | 25 | // Update the CDN link in the 'src/ckeditor/ckeditor.component.spec.ts' file. 26 | updateCdnLink( path.resolve( __dirname, '..', 'src', 'ckeditor', 'ckeditor.component.spec.ts' ) ); 27 | 28 | function updateCdnLink( path ) { 29 | const file = fs.readFileSync( path, 'utf8' ); 30 | const cdnLinkRegex = /https:\/\/cdn\.ckeditor\.com\/\d\.\d+\.\d+(?:-lts)?/g; 31 | 32 | fs.writeFileSync( path, file.replace( cdnLinkRegex, `https://cdn.ckeditor.com/${ version }-lts` ), 'utf8' ); 33 | } 34 | -------------------------------------------------------------------------------- /scripts/test.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 | // This script is a workaround for Angular CLI not allowing to run tests with custom options. 9 | // Manually running `karma start ./src/karma.conf.js` doesn't work either. 10 | // Some of the plugins check if test is run by Angular CLI, and if not, they throw errors. 11 | // https://github.com/angular/angular-cli/issues/12305 12 | 13 | const minimist = require( 'minimist' ); 14 | const { spawn } = require( 'child_process' ); 15 | const options = parseArguments( process.argv.slice( 2 ) ); 16 | const env = Object.create( process.env ); 17 | 18 | env.KARMA_OPTIONS = JSON.stringify( options ); 19 | 20 | const testProcess = spawn( 'ng', [ 'test' ], { 21 | stdio: 'inherit', // Pass parent's stdio's to child. Without that option no logs will be visible. 22 | env 23 | } ); 24 | 25 | testProcess.on( 'close', ( code ) => { 26 | process.exitCode = code; 27 | } ); 28 | 29 | /** 30 | * @param {Array.} args CLI arguments and options. 31 | * @returns {Object} options 32 | * @returns {Boolean} options.url The `ckeditor.js` url to be included by karma. 33 | * @returns {Boolean} options.watch Whether to watch the files for changes. 34 | */ 35 | function parseArguments( args ) { 36 | const config = { 37 | string: [ 38 | 'url' 39 | ], 40 | 41 | boolean: [ 42 | 'watch' 43 | ], 44 | 45 | alias: { 46 | u: 'url', 47 | w: 'watch' 48 | } 49 | }; 50 | 51 | const options = minimist( args, config ); 52 | 53 | // Delete all aliases because we don't want to use them in the code. 54 | // They are useful when calling command but useless after that. 55 | for ( const alias of Object.keys( config.alias ) ) { 56 | delete options[ alias ]; 57 | } 58 | 59 | return options; 60 | } 61 | -------------------------------------------------------------------------------- /src/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | IE 11 11 | not dead 12 | not IE 9-10 13 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 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 | :host { 7 | display: block; 8 | font-family: sans-serif; 9 | font-size: 14px; 10 | padding: 1em; 11 | max-width: 800px; 12 | min-width: 500px; 13 | margin: 0 auto; 14 | } 15 | 16 | nav a.active { 17 | font-weight: bold; 18 | } 19 | 20 | :host ::ng-deep h2, 21 | :host ::ng-deep h3, 22 | :host ::ng-deep h4 { 23 | border-bottom: 1px solid #ddd; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

CKEditor 4 integration with Angular

2 | 3 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/app.component.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 { Component } from '@angular/core'; 7 | 8 | @Component( { 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styleUrls: [ './app.component.css' ] 12 | } ) 13 | export class AppComponent { 14 | } 15 | -------------------------------------------------------------------------------- /src/app/app.module.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 { BrowserModule } from '@angular/platform-browser'; 7 | import { NgModule } from '@angular/core'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { RouterModule, Routes } from '@angular/router'; 10 | import { AppComponent } from './app.component'; 11 | 12 | import { CKEditorModule } from '../ckeditor/ckeditor.module'; 13 | import { SimpleUsageComponent } from './simple-usage/simple-usage.component'; 14 | import { DemoFormComponent } from './demo-form/demo-form.component'; 15 | import { DetachableComponent } from './detachable-component/detachable-component.component'; 16 | 17 | const appRoutes: Routes = [ 18 | { path: '', redirectTo: '/simple-usage', pathMatch: 'full' }, 19 | { path: 'simple-usage', component: SimpleUsageComponent }, 20 | { path: 'forms', component: DemoFormComponent }, 21 | { path: 'detachable', component: DetachableComponent } 22 | ]; 23 | 24 | @NgModule( { 25 | imports: [ 26 | BrowserModule, 27 | FormsModule, 28 | CKEditorModule, 29 | RouterModule.forRoot( appRoutes ) 30 | ], 31 | declarations: [ 32 | AppComponent, 33 | DemoFormComponent, 34 | SimpleUsageComponent, 35 | DetachableComponent 36 | ], 37 | providers: [], 38 | bootstrap: [ AppComponent ] 39 | } ) 40 | 41 | export class AppModule { 42 | } 43 | -------------------------------------------------------------------------------- /src/app/demo-form/demo-form.component.css: -------------------------------------------------------------------------------- 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 | textarea { 7 | font-family: monospace; 8 | } 9 | 10 | form { 11 | background: #eee; 12 | padding: 1em 1.5em; 13 | } 14 | 15 | form label { 16 | display: block; 17 | font-weight: bold; 18 | } 19 | 20 | form input { 21 | width: 30em; 22 | } 23 | 24 | form > div { 25 | margin-bottom: 1em; 26 | } 27 | 28 | pre { 29 | word-wrap: break-word; 30 | white-space: pre-wrap; 31 | } 32 | 33 | p.alert { 34 | color: orange; 35 | } 36 | 37 | p.alert::before { 38 | content: "⚠️ "; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/demo-form/demo-form.component.html: -------------------------------------------------------------------------------- 1 |

Integration with forms (ngModel)

2 | 3 |
4 |

User profile form

5 | 6 |
7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 |
15 | 16 | 17 | 19 | 24 | 25 | 26 |

Description is "dirty".

27 |

Description has been "touched".

28 | 29 |

30 | 31 |

32 |

33 | 34 | (Open the console first)

35 |
36 | 37 |

Editor data preview (read only)

38 |

39 | Note that it's only a prove of concept of using the `ngModel`. 40 | It allows editing, but the editor instantly strips out unknown tags and autoparagraphs text outside of block 41 | elements. 42 |

43 | 44 | 45 |

Form data preview

46 |
{{ formDataPreview }}
47 | -------------------------------------------------------------------------------- /src/app/demo-form/demo-form.component.spec.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 { DebugElement } from '@angular/core'; 7 | import { FormsModule } from '@angular/forms'; 8 | import { By } from '@angular/platform-browser'; 9 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 10 | 11 | import { DemoFormComponent } from './demo-form.component'; 12 | import { CKEditorModule } from '../../ckeditor/ckeditor.module'; 13 | import { CKEditorComponent } from '../../ckeditor/ckeditor.component'; 14 | 15 | import { whenEvent } from '../../test.tools'; 16 | 17 | declare var CKEDITOR: any; 18 | declare var __karma__: { 19 | config: { 20 | args: [ string ]; 21 | } 22 | }; 23 | 24 | describe( 'DemoFormComponent', () => { 25 | let component: DemoFormComponent, 26 | fixture: ComponentFixture, 27 | ckeditorComponent: CKEditorComponent, 28 | debugElement: DebugElement, 29 | originalTimeout: number, 30 | config: Object; 31 | 32 | beforeEach( async( () => { 33 | TestBed.configureTestingModule( { 34 | declarations: [ DemoFormComponent ], 35 | imports: [ FormsModule, CKEditorModule ] 36 | } ).compileComponents(); 37 | } ) ); 38 | 39 | beforeEach( done => { 40 | fixture = TestBed.createComponent( DemoFormComponent ); 41 | component = fixture.componentInstance; 42 | debugElement = fixture.debugElement.query( By.directive( CKEditorComponent ) ); 43 | ckeditorComponent = debugElement.componentInstance; 44 | 45 | ckeditorComponent.namespaceLoaded.subscribe( () => { 46 | CKEDITOR.config.licenseKey = __karma__.config.args[ 0 ]; 47 | } ); 48 | 49 | ckeditorComponent.config = config; 50 | 51 | fixture.detectChanges(); 52 | 53 | originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; 54 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; 55 | 56 | whenEvent( 'ready', ckeditorComponent ).then( done ); 57 | } ); 58 | 59 | afterEach( done => { 60 | if ( ckeditorComponent.instance ) { 61 | ckeditorComponent.instance.once( 'destroy', done ); 62 | } 63 | 64 | config = {}; 65 | 66 | fixture.destroy(); 67 | 68 | jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; 69 | } ); 70 | 71 | it( 'should create', () => { 72 | expect( component ).toBeTruthy(); 73 | } ); 74 | 75 | it( 'on form submission should log the model to the console', () => { 76 | const spy = spyOn( console, 'log' ); 77 | 78 | const submitButton: HTMLButtonElement = fixture.debugElement.query( By.css( 'button[type=submit]' ) ).nativeElement; 79 | submitButton.click(); 80 | 81 | expect( spy ).toHaveBeenCalledTimes( 1 ); 82 | expect( spy.calls.first().args ).toEqual( [ 83 | 'Form submit, model', 84 | { 85 | name: 'John', 86 | surname: 'Doe', 87 | description: '

A really nice fellow.

\n' 88 | } 89 | ] ); 90 | } ); 91 | 92 | // This test passes when run solo or testes as first, but throws a type error when run after other tests. 93 | it( 'when change event is emitted should show form data preview', done => { 94 | whenEvent( 'change', ckeditorComponent ).then( () => { 95 | fixture.detectChanges(); 96 | expect( component.formDataPreview ).toEqual( '{"name":"John","surname":"Doe","description":"

An unidentified person

\\n"}' ); 97 | done(); 98 | } ); 99 | 100 | ckeditorComponent.instance.setData( '

An unidentified person

' ); 101 | } ); 102 | 103 | it( 'when reset button is clicked should reset form', done => { 104 | const resetButton: HTMLButtonElement = fixture.debugElement.query( By.css( 'button[type=reset]' ) ).nativeElement; 105 | resetButton.click(); 106 | 107 | fixture.detectChanges(); 108 | expect( component.formDataPreview ).toEqual( '{"name":null,"surname":null,"description":null}' ); 109 | 110 | done(); 111 | } ); 112 | 113 | [ { 114 | newConfig: {}, 115 | msg: 'with undo plugin' 116 | }, { 117 | newConfig: { removePlugins: 'undo' }, 118 | msg: 'without undo plugin' 119 | } ].forEach( ( { newConfig, msg } ) => { 120 | describe( 'should emit onChange event', () => { 121 | beforeAll( () => { 122 | config = newConfig; 123 | } ); 124 | 125 | it( msg, done => { 126 | const spy = spyOn( ckeditorComponent, 'onChange' ); 127 | 128 | whenEvent( 'dataChange', ckeditorComponent ).then( () => { 129 | fixture.detectChanges(); 130 | expect( spy ).toHaveBeenCalledTimes( 1 ); 131 | done(); 132 | } ); 133 | 134 | ckeditorComponent.instance.setData( '

An unidentified person

' ); 135 | } ); 136 | } ); 137 | } ); 138 | 139 | } ); 140 | -------------------------------------------------------------------------------- /src/app/demo-form/demo-form.component.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 { 7 | Component, 8 | ViewChild, 9 | AfterViewInit 10 | } from '@angular/core'; 11 | 12 | import { NgForm } from '@angular/forms'; 13 | 14 | @Component( { 15 | selector: 'app-demo-form', 16 | templateUrl: './demo-form.component.html', 17 | styleUrls: [ './demo-form.component.css' ] 18 | } ) 19 | 20 | export class DemoFormComponent implements AfterViewInit { 21 | @ViewChild( 'demoForm', { static: true } ) demoForm?: NgForm; 22 | 23 | public model = { 24 | name: 'John', 25 | surname: 'Doe', 26 | description: '

A really nice fellow.

' 27 | }; 28 | 29 | public formDataPreview?: string; 30 | 31 | ngAfterViewInit() { 32 | this.demoForm!.control.valueChanges 33 | .subscribe( values => this.formDataPreview = JSON.stringify( values ) ); 34 | } 35 | 36 | onSubmit() { 37 | console.log( 'Form submit, model', this.model ); 38 | } 39 | 40 | reset() { 41 | this.demoForm!.reset(); 42 | } 43 | 44 | get description() { 45 | return this.demoForm!.controls.description; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/detachable-component/detachable-component.component.css: -------------------------------------------------------------------------------- 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 | :host ::ng-deep .ck.ck-editor__editable:not([contenteditable]) { 7 | background: #fafafa; 8 | color: #888; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/detachable-component/detachable-component.component.html: -------------------------------------------------------------------------------- 1 |

Detachable example

2 | 3 |

Note: Open the console for additional logs.

4 | 5 |

Detachable editor:

6 |
7 |
8 | 10 | 11 |
12 |
13 | 14 |

15 | -------------------------------------------------------------------------------- /src/app/detachable-component/detachable-component.component.spec.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 { async, ComponentFixture, TestBed } from '@angular/core/testing'; 7 | import { DebugElement } from '@angular/core'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { By } from '@angular/platform-browser'; 10 | 11 | import { DetachableComponent } from './detachable-component.component'; 12 | import { CKEditorModule } from '../../ckeditor/ckeditor.module'; 13 | import { CKEditorComponent } from '../../ckeditor/ckeditor.component'; 14 | import { whenEvent } from '../../test.tools'; 15 | 16 | declare var CKEDITOR: any; 17 | declare var __karma__: { 18 | config: { 19 | args: [ string ]; 20 | } 21 | }; 22 | 23 | describe( 'DetachableComponent', () => { 24 | let component: DetachableComponent, 25 | fixture: ComponentFixture, 26 | ckeditorComponents: CKEditorComponent[], 27 | debugElements: DebugElement[]; 28 | 29 | beforeEach( async( () => { 30 | TestBed.configureTestingModule( { 31 | declarations: [ DetachableComponent ], 32 | imports: [ CKEditorModule, FormsModule ] 33 | } ).compileComponents(); 34 | } ) ); 35 | 36 | afterEach( done => { 37 | whenEach( ckeditorComponent => 38 | new Promise( res => { 39 | if ( ckeditorComponent.instance ) { 40 | ckeditorComponent.instance.once( 'destroy', res ); 41 | } 42 | } ) 43 | ).then( done ); 44 | 45 | fixture.destroy(); 46 | } ); 47 | 48 | it( 'should create editor after adding it the DOM without throwing any errors ', async () => { 49 | fixture = TestBed.createComponent( DetachableComponent ); 50 | component = fixture.componentInstance; 51 | 52 | // When there is `*ngIf` directive on component instance, we need another detectChanges. 53 | fixture.detectChanges(); 54 | 55 | debugElements = fixture.debugElement.queryAll( By.directive( CKEditorComponent ) ); 56 | ckeditorComponents = debugElements.map( debugElement => debugElement.componentInstance ); 57 | 58 | ckeditorComponents.forEach( ( ckeditorComponent ) => { 59 | ckeditorComponent.namespaceLoaded.subscribe( () => { 60 | CKEDITOR.config.licenseKey = __karma__.config.args[ 0 ]; 61 | } ); 62 | } ); 63 | 64 | fixture.detectChanges(); 65 | 66 | await wait( 500 ); 67 | 68 | component.reattachEditor(); 69 | 70 | return whenEach( ckeditorComponent => whenEvent( 'ready', ckeditorComponent ) ); 71 | } ); 72 | 73 | function whenEach( callback ) { 74 | return Promise.all( ckeditorComponents.map( ckeditorComponent => callback( ckeditorComponent ) ) ); 75 | } 76 | 77 | function wait( time ) { 78 | return new Promise( resolve => { 79 | setTimeout( resolve, time ); 80 | } ); 81 | } 82 | } ); 83 | -------------------------------------------------------------------------------- /src/app/detachable-component/detachable-component.component.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 { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; 7 | 8 | @Component( { 9 | selector: 'app-detachable-component', 10 | templateUrl: './detachable-component.component.html', 11 | styleUrls: [ './detachable-component.component.css' ] 12 | } ) 13 | export class DetachableComponent implements AfterViewInit { 14 | isReattached = false; 15 | 16 | @ViewChild( 'container' ) private containerElement: ElementRef; 17 | @ViewChild( 'editor' ) private editorElement: ElementRef; 18 | 19 | ngAfterViewInit(): void { 20 | console.log( 'Component loaded, deataching' ); 21 | 22 | this.containerElement.nativeElement.removeChild( this.editorElement.nativeElement ); 23 | } 24 | 25 | reattachEditor() { 26 | console.log( 'Button clicked, reattaching editor' ); 27 | 28 | this.isReattached = true; 29 | this.containerElement.nativeElement.appendChild( this.editorElement.nativeElement ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/simple-usage/simple-usage.component.css: -------------------------------------------------------------------------------- 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 | :host ::ng-deep .ck.ck-editor__editable:not([contenteditable]) { 7 | background: #fafafa; 8 | color: #888; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/simple-usage/simple-usage.component.html: -------------------------------------------------------------------------------- 1 |

Simple usage

2 | 3 |

Note: Open the console for additional logs.

4 | 5 |
6 |

{{ editor }} Editor:

7 | 9 | 30 |
31 | 32 |

Note: editors have it's data synchronized, so every change in one of editors propagates to another.

33 | 34 |

Component controls:

35 | 36 |

37 | 38 |

39 |

40 | 41 |

42 |

43 | 44 |

45 | -------------------------------------------------------------------------------- /src/app/simple-usage/simple-usage.component.spec.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 { async, ComponentFixture, TestBed } from '@angular/core/testing'; 7 | import { DebugElement } from '@angular/core'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { By } from '@angular/platform-browser'; 10 | 11 | import { SimpleUsageComponent } from './simple-usage.component'; 12 | import { getEditorNamespace } from 'ckeditor4-integrations-common'; 13 | import { CKEditorModule } from '../../ckeditor/ckeditor.module'; 14 | import { CKEditorComponent } from '../../ckeditor/ckeditor.component'; 15 | import { whenEvent } from '../../test.tools'; 16 | 17 | import Spy = jasmine.Spy; 18 | 19 | declare var CKEDITOR: any; 20 | declare var __karma__: { 21 | config: { 22 | args: [ string ]; 23 | } 24 | }; 25 | 26 | describe( 'SimpleUsageComponent', () => { 27 | let component: SimpleUsageComponent, 28 | fixture: ComponentFixture, 29 | ckeditorComponents: CKEditorComponent[], 30 | debugElements: DebugElement[], 31 | spy: Spy; 32 | 33 | beforeEach( async( () => { 34 | TestBed.configureTestingModule( { 35 | declarations: [ SimpleUsageComponent ], 36 | imports: [ CKEditorModule, FormsModule ] 37 | } ).compileComponents(); 38 | } ) ); 39 | 40 | beforeEach( done => { 41 | fixture = TestBed.createComponent( SimpleUsageComponent ); 42 | component = fixture.componentInstance; 43 | 44 | // When there is `*ngIf` directive on component instance, we need another detectChanges. 45 | fixture.detectChanges(); 46 | 47 | debugElements = fixture.debugElement.queryAll( By.directive( CKEditorComponent ) ); 48 | ckeditorComponents = debugElements.map( debugElement => debugElement.componentInstance ); 49 | 50 | ckeditorComponents.forEach( ( ckeditorComponent ) => { 51 | ckeditorComponent.namespaceLoaded.subscribe( () => { 52 | CKEDITOR.config.licenseKey = __karma__.config.args[ 0 ]; 53 | } ); 54 | } ); 55 | 56 | fixture.detectChanges(); 57 | 58 | whenEach( ckeditorComponent => whenEvent( 'ready', ckeditorComponent ) ).then( done ); 59 | } ); 60 | 61 | afterEach( done => { 62 | whenEach( ckeditorComponent => 63 | new Promise( res => { 64 | if ( ckeditorComponent.instance ) { 65 | ckeditorComponent.instance.once( 'destroy', res ); 66 | } 67 | } ) 68 | ).then( done ); 69 | 70 | fixture.destroy(); 71 | } ); 72 | 73 | it( 'should create', () => { 74 | expect( component ).toBeTruthy(); 75 | } ); 76 | 77 | it( 'readOnly should be set to false at start', () => { 78 | expect( component.isReadOnly ).toBeFalsy(); 79 | } ); 80 | 81 | it( 'when component readOnly is changed on component editor readOnly should reflect change', () => { 82 | component.toggleDisableEditors(); 83 | fixture.detectChanges(); 84 | 85 | expect( component.isReadOnly ).toBeTruthy(); 86 | each( ckeditorComponent => { 87 | expect( ckeditorComponent.readOnly ).toBeTruthy(); 88 | } ); 89 | 90 | component.toggleDisableEditors(); 91 | fixture.detectChanges(); 92 | 93 | expect( component.isReadOnly ).toBeFalsy(); 94 | each( ckeditorComponent => { 95 | expect( ckeditorComponent.readOnly ).toBeFalsy(); 96 | } ); 97 | } ); 98 | 99 | describe( 'data', () => { 100 | it( 'should set initial data on the CKEditor component', () => { 101 | each( ckeditorComponent => { 102 | expect( ckeditorComponent.data ) 103 | .toContain( '

Getting used to an entirely different culture can be challenging.' ); 104 | } ); 105 | } ); 106 | 107 | it( 'should be synced with editorData property', () => { 108 | return getEditorNamespace( ckeditorComponents[ 0 ].editorUrl ) 109 | .then( CKEDITOR => { 110 | component.editorData = '

foo

\n'; 111 | 112 | fixture.detectChanges(); 113 | 114 | each( ckeditorComponent => { 115 | expect( ckeditorComponent.data ).toEqual( '

foo

\n' ); 116 | } ); 117 | } ); 118 | } ); 119 | } ); 120 | 121 | describe( 'listeners', () => { 122 | beforeEach( () => { 123 | spy = spyOn( console, 'log' ); 124 | } ); 125 | 126 | it( 'ready should be called on ckeditorComponent.ready()', () => { 127 | each( ( ckeditorComponent, name ) => { 128 | ckeditorComponent.ready.emit(); 129 | 130 | expect( spy ).toHaveBeenCalledWith( `${ name } editor is ready.` ); 131 | } ); 132 | } ); 133 | 134 | it( 'change should be called on ckeditorComponent.change()', () => { 135 | each( ( ckeditorComponent, name ) => { 136 | ckeditorComponent.change.emit(); 137 | 138 | expect( spy ).toHaveBeenCalledWith( `${ name } editor model changed.` ); 139 | } ); 140 | } ); 141 | 142 | it( 'focus should be called on ckeditorComponent.focus()', () => { 143 | each( ( ckeditorComponent, name ) => { 144 | ckeditorComponent.focus.emit(); 145 | 146 | name = name.toLowerCase(); 147 | 148 | expect( spy ).toHaveBeenCalledWith( `Focused ${ name } editing view.` ); 149 | } ); 150 | } ); 151 | 152 | it( 'blur should be called on ckeditorComponent.blur()', () => { 153 | each( ( ckeditorComponent, name ) => { 154 | ckeditorComponent.blur.emit(); 155 | 156 | name = name.toLowerCase(); 157 | 158 | expect( spy ).toHaveBeenCalledWith( `Blurred ${ name } editing view.` ); 159 | } ); 160 | } ); 161 | } ); 162 | 163 | function whenEach( callback ) { 164 | return Promise.all( ckeditorComponents.map( ckeditorComponent => callback( ckeditorComponent ) ) ); 165 | } 166 | 167 | function each( callback ) { 168 | ckeditorComponents.forEach( item => { 169 | let name: String = item.type; 170 | 171 | name = name[ 0 ].toUpperCase() + name.slice( 1 ); 172 | 173 | callback( item, name ); 174 | } ); 175 | } 176 | } ); 177 | -------------------------------------------------------------------------------- /src/app/simple-usage/simple-usage.component.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 { Component } from '@angular/core'; 7 | 8 | import { CKEditor4 } from '../../ckeditor/ckeditor'; 9 | 10 | @Component( { 11 | selector: 'app-simple-usage', 12 | templateUrl: './simple-usage.component.html', 13 | styleUrls: [ './simple-usage.component.css' ] 14 | } ) 15 | export class SimpleUsageComponent { 16 | public isReadOnly = false; 17 | public editorData = 18 | `

Getting used to an entirely different culture can be challenging. 19 | While it’s also nice to learn about cultures online or from books, nothing comes close to experiencing cultural diversity in person. 20 | You learn to appreciate each and every single one of the differences while you become more culturally fluid.

`; 21 | 22 | editors = [ 'Classic', 'Inline' ]; 23 | 24 | isHidden = false; 25 | 26 | isRemoved = false; 27 | 28 | public componentEvents: string[] = []; 29 | 30 | toggleDisableEditors() { 31 | this.isReadOnly = !this.isReadOnly; 32 | } 33 | 34 | onReady( editor: CKEditor4.EventInfo, editorName: string ): void { 35 | console.log( `${editorName} editor is ready.` ); 36 | } 37 | 38 | onChange( event: CKEditor4.EventInfo, editorName: string ): void { 39 | console.log( `${editorName} editor model changed.` ); 40 | } 41 | 42 | onFocus( event: CKEditor4.EventInfo, editorName: string ): void { 43 | console.log( `Focused ${editorName.toLowerCase()} editing view.` ); 44 | } 45 | 46 | onBlur( event: CKEditor4.EventInfo, editorName: string ): void { 47 | console.log( `Blurred ${editorName.toLowerCase()} editing view.` ); 48 | } 49 | 50 | onPaste( event: CKEditor4.EventInfo, editorName: string ): void { 51 | console.log( `Pasted into ${editorName.toLowerCase()} editing view.` ); 52 | } 53 | 54 | onAfterPaste( event: CKEditor4.EventInfo, editorName: string ): void { 55 | console.log( `After pasted fired in ${editorName.toLowerCase()} editing view.` ); 56 | } 57 | 58 | onDragStart( event: CKEditor4.EventInfo, editorName: string ): void { 59 | console.log( `Drag started in ${editorName.toLowerCase()} editing view.` ); 60 | } 61 | 62 | onDragEnd( event: CKEditor4.EventInfo, editorName: string ): void { 63 | console.log( `Drag ended in ${editorName.toLowerCase()} editing view.` ); 64 | } 65 | 66 | onDrop( event: CKEditor4.EventInfo, editorName: string ): void { 67 | console.log( `Dropped in ${editorName.toLowerCase()} editing view.` ); 68 | } 69 | 70 | onFileUploadRequest( event: CKEditor4.EventInfo, editorName: string ): void { 71 | console.log( `File upload requested in ${editorName.toLowerCase()} editor.` ); 72 | } 73 | 74 | onFileUploadResponse( event: CKEditor4.EventInfo, editorName: string ): void { 75 | console.log( `File upload responded in ${editorName.toLowerCase()} editor.` ); 76 | } 77 | 78 | onNamespaceLoaded( event: CKEditor4.EventInfo, editorName: string ): void { 79 | console.log( `Namespace loaded by ${editorName.toLowerCase()} editor.` ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckeditor/ckeditor4-angular/b9c388579934798a2407ebf319a7648b65025ad0/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/ckeditor/ckeditor.component.spec.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 waitUntil from 'wait-until-promise'; 7 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 8 | import { CKEditorComponent } from './ckeditor.component'; 9 | import { 10 | fireDragEvent, 11 | mockDropEvent, 12 | setDataMultipleTimes, 13 | whenDataReady, 14 | whenEvent 15 | } from '../test.tools'; 16 | import { CKEditor4 } from './ckeditor'; 17 | import EditorType = CKEditor4.EditorType; 18 | import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; 19 | import { By } from '@angular/platform-browser'; 20 | 21 | declare var CKEDITOR: any; 22 | declare var __karma__: { 23 | config: { 24 | args: [ string ]; 25 | } 26 | }; 27 | 28 | describe( 'CKEditorComponent', () => { 29 | let component: CKEditorComponent, 30 | fixture: ComponentFixture, 31 | config: Object; 32 | 33 | beforeEach( () => { 34 | return TestBed.configureTestingModule( { 35 | declarations: [ CKEditorComponent ] 36 | } ).compileComponents(); 37 | } ) 38 | 39 | beforeEach( () => { 40 | fixture = TestBed.createComponent( CKEditorComponent ); 41 | component = fixture.componentInstance; 42 | component.config = config; 43 | 44 | component.namespaceLoaded.subscribe( () => { 45 | CKEDITOR.config.licenseKey = __karma__.config.args[ 0 ]; 46 | } ); 47 | 48 | fixture.detectChanges(); 49 | } ); 50 | 51 | afterEach( () => { 52 | config = {}; 53 | fixture.destroy(); 54 | } ); 55 | 56 | [ 57 | EditorType.INLINE, 58 | EditorType.CLASSIC 59 | ].forEach( editorType => { 60 | describe( `type="${editorType}"`, () => { 61 | beforeEach( () => { 62 | component.type = editorType; 63 | } ); 64 | 65 | describe( 'on initialization', () => { 66 | const method = editorType === 'inline' ? 'inline' : 'replace'; 67 | 68 | it( `should create editor with CKEDITOR.${method}`, () => { 69 | fixture.detectChanges(); 70 | 71 | return whenEvent( 'ready', component ).then( () => { 72 | expect( component.instance.elementMode ).toEqual( editorType == 'inline' ? 3 : 1 ); 73 | } ); 74 | } ); 75 | 76 | it( 'should have editorUrl pointing to the latest CKEditor 4 version', () => { 77 | fixture.detectChanges(); 78 | 79 | return whenEvent( 'ready', component ).then( () => { 80 | expect( component.editorUrl ).toEqual( 'https://cdn.ckeditor.com/4.25.1-lts/standard-all/ckeditor.js' ); 81 | } ); 82 | } ); 83 | 84 | it( 'should have proper editor type', () => { 85 | return whenEvent( 'ready', component ).then( () => { 86 | fixture.detectChanges(); 87 | expect( component.instance.editable().isInline() ) 88 | .toBe( component.type !== EditorType.CLASSIC ); 89 | } ); 90 | } ); 91 | 92 | it( 'should emit ready event', () => { 93 | const spy = jasmine.createSpy(); 94 | component.ready.subscribe( spy ); 95 | 96 | fixture.detectChanges(); 97 | 98 | return whenEvent( 'ready', component ).then( () => { 99 | expect( spy ).toHaveBeenCalledTimes( 1 ); 100 | } ); 101 | } ); 102 | 103 | describe( 'with tagName unset', () => { 104 | it( 'editor should be initialized using textarea element', () => { 105 | fixture.detectChanges(); 106 | 107 | return whenEvent( 'ready', component ).then( () => { 108 | expect( fixture.nativeElement.firstElementChild.tagName ).toEqual( 'TEXTAREA' ); 109 | } ); 110 | } ); 111 | } ); 112 | 113 | describe( 'with tagName set to div', () => { 114 | beforeEach( () => { 115 | component.tagName = 'div'; 116 | } ); 117 | 118 | it( 'editor should be initialized using div element', () => { 119 | fixture.detectChanges(); 120 | 121 | return whenEvent( 'ready', component ).then( () => { 122 | // IE browsers use SPAN elements instead of DIV as a main CKEditor wrapper 123 | // when replace() method for creation is used. 124 | const expectedElement = CKEDITOR.env.ie && method !== 'inline' ? 'SPAN' : 'DIV'; 125 | expect( fixture.nativeElement.lastChild.tagName ).toEqual( expectedElement ); 126 | } ); 127 | } ); 128 | } ); 129 | 130 | describe( 'when set with config', () => { 131 | beforeEach( () => { 132 | component.config = { 133 | readOnly: true, 134 | width: 1000, 135 | height: 1000 136 | }; 137 | fixture.detectChanges(); 138 | 139 | return whenEvent( 'ready', component ); 140 | } ); 141 | 142 | it( 'editor should be readOnly', () => { 143 | expect( component.instance.readOnly ).toBeTruthy(); 144 | } ); 145 | 146 | it( 'editor should have width and height', () => { 147 | expect( component.instance.config.width ).toBe( 1000 ); 148 | expect( component.instance.config.height ).toBe( 1000 ); 149 | } ); 150 | 151 | it( 'editor should have undo plugin', () => { 152 | expect( component.instance.plugins.undo ).not.toBeUndefined(); 153 | } ); 154 | 155 | it( 'should register changes', () => { 156 | const spy = jasmine.createSpy(); 157 | 158 | component.registerOnChange( spy ); 159 | 160 | return setDataMultipleTimes( component.instance, [ 161 | '

Hello World!

', 162 | '

I am CKEditor for Angular!

' 163 | ] ).then( () => { 164 | expect( spy ).toHaveBeenCalledTimes( 2 ); 165 | } ); 166 | } ); 167 | } ); 168 | 169 | describe( 'when set without undo plugin', () => { 170 | beforeEach( () => { 171 | component.config = { 172 | removePlugins: 'undo' 173 | }; 174 | fixture.detectChanges(); 175 | return whenEvent( 'ready', component ); 176 | } ); 177 | 178 | it( 'editor should not have undo plugin', () => { 179 | expect( component.instance.plugins.undo ).toBeUndefined(); 180 | } ); 181 | 182 | it( 'should register changes without undo plugin', () => { 183 | const spy = jasmine.createSpy(); 184 | 185 | component.registerOnChange( spy ); 186 | 187 | return setDataMultipleTimes( component.instance, [ 188 | '

Hello World!

', 189 | '

I am CKEditor for Angular!

' 190 | ] ).then( () => { 191 | expect( spy ).toHaveBeenCalledTimes( 2 ); 192 | } ); 193 | } ); 194 | } ); 195 | } ); 196 | 197 | describe( 'on destroy', () => { 198 | it ( 'should not have call runOutsideAngular when destroy before DOM loaded', () => { 199 | spyOn( fixture.ngZone, 'runOutsideAngular' ); 200 | 201 | fixture.detectChanges(); 202 | 203 | return waitUntil( () => { 204 | fixture.destroy(); 205 | return true; 206 | }, 0 ).then( () => { 207 | expect( fixture.ngZone.runOutsideAngular ).toHaveBeenCalledTimes( 1 ); 208 | } ); 209 | } ); 210 | 211 | it ( 'should not have call runOutsideAngular when destroy before DOM loaded', () => { 212 | spyOn( fixture.ngZone, 'runOutsideAngular' ); 213 | 214 | fixture.detectChanges(); 215 | 216 | return waitUntil( () => { 217 | fixture.destroy(); 218 | return true; 219 | }, 200 ).then( () => { 220 | expect( fixture.ngZone.runOutsideAngular ).toHaveBeenCalledTimes( 1 ); 221 | } ); 222 | } ); 223 | } ); 224 | 225 | describe( 'when component is ready', () => { 226 | beforeEach( () => { 227 | fixture.detectChanges(); 228 | return whenEvent( 'ready', component ); 229 | } ); 230 | 231 | it( 'should be initialized', () => { 232 | expect( component ).toBeTruthy(); 233 | } ); 234 | 235 | it( `editor ${editorType === 'inline' ? 'should' : 'shouldn\'t'} be inline`, () => { 236 | const expectation = expect( component.instance.editable().hasClass( 'cke_editable_inline' ) ); 237 | 238 | editorType === 'inline' 239 | ? expectation.toBeTruthy() 240 | : expectation.toBeFalsy(); 241 | } ); 242 | 243 | it( 'editor shouldn\'t be read-only', () => { 244 | fixture.detectChanges(); 245 | 246 | expect( component.readOnly ).toBeFalsy(); 247 | expect( component.instance.readOnly ).toBeFalsy(); 248 | } ); 249 | 250 | describe( 'with changed read-only mode', () => { 251 | it( 'should allow to enable read-only mode', () => { 252 | component.readOnly = true; 253 | 254 | expect( component.readOnly ).toBeTruthy(); 255 | expect( component.instance.readOnly ).toBeTruthy(); 256 | } ); 257 | 258 | it( 'should allow to disable read-only mode', () => { 259 | component.readOnly = false; 260 | 261 | expect( component.readOnly ).toBeFalsy(); 262 | expect( component.instance.readOnly ).toBeFalsy(); 263 | } ); 264 | } ); 265 | 266 | it( 'initial data should be empty', () => { 267 | fixture.detectChanges(); 268 | 269 | expect( component.data ).toEqual( null ); 270 | expect( component.instance.getData() ).toEqual( '' ); 271 | } ); 272 | 273 | describe( 'component data', () => { 274 | const data = 'foo', 275 | expected = '

foo

\n'; 276 | 277 | it( 'should be configurable at the start of the component', async () => { 278 | fixture.detectChanges(); 279 | 280 | await whenDataReady( component.instance, () => component.data = data ); 281 | 282 | expect( component.data ).toEqual( expected ); 283 | expect( component.instance.getData() ).toEqual( expected ); 284 | } ); 285 | 286 | it( 'should be writeable by ControlValueAccessor', async () => { 287 | fixture.detectChanges(); 288 | 289 | const editor = component.instance; 290 | 291 | await whenDataReady( editor, () => component.writeValue( data ) ); 292 | 293 | expect( component.instance.getData() ).toEqual( expected ); 294 | 295 | await whenDataReady( editor, () => component.writeValue( '

baz

' ) ); 296 | 297 | expect( component.instance.getData() ).toEqual( '

baz

\n' ); 298 | } ); 299 | } ); 300 | 301 | describe( 'editor event', () => { 302 | it( 'change should emit component change', () => { 303 | fixture.detectChanges(); 304 | 305 | const spy = jasmine.createSpy(); 306 | component.change.subscribe( spy ); 307 | 308 | component.instance.fire( 'change' ); 309 | 310 | expect( spy ).toHaveBeenCalledTimes( 1 ); 311 | } ); 312 | 313 | it( 'focus should emit component focus', () => { 314 | fixture.detectChanges(); 315 | 316 | const spy = jasmine.createSpy(); 317 | component.focus.subscribe( spy ); 318 | 319 | component.instance.fire( 'focus' ); 320 | 321 | expect( spy ).toHaveBeenCalledTimes( 1 ); 322 | } ); 323 | 324 | it( 'blur should emit component blur', () => { 325 | fixture.detectChanges(); 326 | 327 | const spy = jasmine.createSpy(); 328 | component.blur.subscribe( spy ); 329 | 330 | component.instance.fire( 'blur' ); 331 | 332 | expect( spy ).toHaveBeenCalledTimes( 1 ); 333 | } ); 334 | 335 | it( 'paste should emit component paste', () => { 336 | fixture.detectChanges(); 337 | 338 | const spy = jasmine.createSpy(); 339 | component.paste.subscribe( spy ); 340 | 341 | const editable = component.instance.editable(); 342 | const editor = editable.getEditor( false ); 343 | 344 | const eventPromise = whenEvent( 'paste', component ).then( () => { 345 | expect( spy ).toHaveBeenCalledTimes( 1 ); 346 | expect( component.instance.getData() ).toEqual( '

bam

\n' ); 347 | } ); 348 | 349 | editor.fire( 'paste', { 350 | dataValue: '

bam

' 351 | } ); 352 | 353 | return eventPromise; 354 | } ); 355 | 356 | it( 'afterPaste should emit component afterPaste', () => { 357 | fixture.detectChanges(); 358 | 359 | const spy = jasmine.createSpy(); 360 | component.afterPaste.subscribe( spy ); 361 | 362 | const editable = component.instance.editable(); 363 | const editor = editable.getEditor( false ); 364 | 365 | const eventPromise = whenEvent( 'afterPaste', component ).then( () => { 366 | expect( spy ).toHaveBeenCalledTimes( 1 ); 367 | expect( component.instance.getData() ).toEqual( '

bam

\n' ); 368 | } ); 369 | 370 | editor.fire( 'paste', { 371 | dataValue: '

bam

' 372 | } ); 373 | 374 | return eventPromise; 375 | } ); 376 | 377 | it( 'drag/drop events should emit component dragStart, dragEnd and drop', () => { 378 | fixture.detectChanges(); 379 | 380 | const spyDragStart = jasmine.createSpy( 'dragstart' ); 381 | component.dragStart.subscribe( spyDragStart ); 382 | 383 | const spyDragEnd = jasmine.createSpy( 'dragend' ); 384 | component.dragEnd.subscribe( spyDragEnd ); 385 | 386 | const spyDrop = jasmine.createSpy( 'drop' ); 387 | component.drop.subscribe( spyDrop ); 388 | 389 | const dropEvent = mockDropEvent(); 390 | const paragraph = component.instance.editable().findOne( 'p' ); 391 | 392 | component.instance.getSelection().selectElement( paragraph ); 393 | 394 | fireDragEvent( 'dragstart', component.instance, dropEvent ); 395 | 396 | expect( spyDragStart ).toHaveBeenCalledTimes( 1 ); 397 | 398 | fireDragEvent( 'dragend', component.instance, dropEvent ); 399 | 400 | expect( spyDragEnd ).toHaveBeenCalledTimes( 1 ); 401 | 402 | // There is some issue in Firefox with simulating drag-drop flow. The drop event 403 | // is not fired making this assertion fail. Let's skip it for now. 404 | if ( !CKEDITOR.env.gecko ) { 405 | fireDragEvent( 'drop', component.instance, dropEvent ); 406 | 407 | expect( spyDrop ).toHaveBeenCalledTimes( 1 ); 408 | } 409 | } ); 410 | 411 | it( 'fileUploadRequest should emit component fileUploadRequest', () => { 412 | fixture.detectChanges(); 413 | 414 | const spy = jasmine.createSpy(); 415 | component.fileUploadRequest.subscribe( spy ); 416 | 417 | const fileLoaderMock = { 418 | fileLoader: { 419 | file: Blob ? new Blob() : '', 420 | fileName: 'fileName', 421 | xhr: { 422 | open: function() {}, 423 | send: function() {} 424 | } 425 | }, 426 | requestData: {} 427 | }; 428 | 429 | component.instance.fire( 'fileUploadRequest', fileLoaderMock ); 430 | 431 | expect( spy ).toHaveBeenCalledTimes( 1 ); 432 | } ); 433 | 434 | it( 'fileUploadResponse should emit component fileUploadResponse', () => { 435 | fixture.detectChanges(); 436 | 437 | const spy = jasmine.createSpy(); 438 | component.fileUploadResponse.subscribe( spy ); 439 | 440 | const data = { 441 | fileLoader: { 442 | xhr: { responseText: 'Not a JSON.' }, 443 | lang: { 444 | filetools: { responseError: 'Error' } 445 | } 446 | } 447 | }; 448 | 449 | component.instance.fire( 'fileUploadResponse', data ); 450 | 451 | expect( spy ).toHaveBeenCalledTimes( 1 ); 452 | } ); 453 | } ); 454 | 455 | describe( 'when control value accessor callbacks are set', () => { 456 | it( 'onTouched callback should be called when editor is blurred', () => { 457 | fixture.detectChanges(); 458 | 459 | const spy = jasmine.createSpy(); 460 | component.registerOnTouched( spy ); 461 | 462 | component.instance.fire( 'blur' ); 463 | 464 | expect( spy ).toHaveBeenCalled(); 465 | } ); 466 | 467 | it( 'onChange callback should be called when editor model changes', () => { 468 | fixture.detectChanges(); 469 | 470 | const spy = jasmine.createSpy(); 471 | component.registerOnChange( spy ); 472 | 473 | return setDataMultipleTimes( component.instance, [ 474 | 'initial', 'initial', 'modified' 475 | ] ).then( () => { 476 | expect( spy ).toHaveBeenCalledTimes( 2 ); 477 | } ); 478 | } ); 479 | } ); 480 | } ); 481 | } ); 482 | } ); 483 | } ); 484 | 485 | // (#190) 486 | describe( 'CKEditorComponent detached', () => { 487 | @Component( { 488 | selector: 'detachable-callback', 489 | template: `
490 |
491 | 492 |
493 |
` 494 | } ) 495 | class DetachableCallbackComponent implements AfterViewInit { 496 | createEditor: Function; 497 | 498 | editorConfig = { 499 | delayIfDetached_callback: ( creator ) => { 500 | console.log( creator ); 501 | this.createEditor = creator; 502 | } 503 | } 504 | 505 | @ViewChild( 'container' ) private containerElement: ElementRef; 506 | @ViewChild( 'editor' ) private editorElement: ElementRef; 507 | 508 | onNamespaceLoaded() { 509 | CKEDITOR.config.licenseKey = __karma__.config.args[ 0 ]; 510 | } 511 | 512 | ngAfterViewInit(): void { 513 | this.containerElement.nativeElement.removeChild( this.editorElement.nativeElement ); 514 | } 515 | 516 | reattachEditor() { 517 | this.containerElement.nativeElement.appendChild( this.editorElement.nativeElement ); 518 | } 519 | } 520 | 521 | let fixture: ComponentFixture; 522 | 523 | beforeEach( () => { 524 | return TestBed.configureTestingModule( { 525 | declarations: [ CKEditorComponent, DetachableCallbackComponent ] 526 | } ).compileComponents(); 527 | } ); 528 | 529 | afterEach( () => { 530 | if ( fixture ) { 531 | fixture.destroy(); 532 | } 533 | } ); 534 | 535 | it( 'should set config.delayIfDetached to true by default', async () => { 536 | fixture = TestBed.createComponent( CKEditorComponent ); 537 | const component = fixture.componentInstance as CKEditorComponent; 538 | 539 | fixture.detectChanges(); 540 | 541 | await whenEvent( 'ready', component ); 542 | 543 | expect( component.instance.config.delayIfDetached ).toBeTrue(); 544 | } ); 545 | 546 | it( 'should allow overriding config.delayIfDetached', async () => { 547 | fixture = TestBed.createComponent( CKEditorComponent ); 548 | const component = fixture.componentInstance as CKEditorComponent; 549 | component.config = { 550 | delayIfDetached: false 551 | }; 552 | 553 | fixture.detectChanges(); 554 | 555 | await whenEvent( 'ready', component ); 556 | 557 | expect( component.instance.config.delayIfDetached ).toBeFalse(); 558 | } ); 559 | 560 | it( 'should invoke user provided config.on.instanceReady', async () => { 561 | fixture = TestBed.createComponent( CKEditorComponent ); 562 | const spy = jasmine.createSpy(); 563 | const component = fixture.componentInstance as CKEditorComponent; 564 | component.config = { 565 | on: { 566 | instanceReady: spy 567 | } 568 | }; 569 | 570 | fixture.detectChanges(); 571 | 572 | await whenEvent( 'ready', component ); 573 | 574 | expect( spy ).toHaveBeenCalledTimes( 1 ); 575 | } ); 576 | 577 | it( 'should support creating editor using provided config.delayIfDetached_callback', async () => { 578 | fixture = TestBed.createComponent( DetachableCallbackComponent ); 579 | const component = fixture.componentInstance as DetachableCallbackComponent; 580 | 581 | fixture.detectChanges(); 582 | 583 | const debugElements = fixture.debugElement.queryAll( By.directive( CKEditorComponent ) ); 584 | const ckeditorComponents = debugElements.map( debugElement => debugElement.componentInstance ); 585 | 586 | fixture.detectChanges(); 587 | 588 | await wait( 500 ); 589 | 590 | expect( component.createEditor ).toBeInstanceOf( Function ); 591 | 592 | component.reattachEditor(); 593 | component.createEditor(); 594 | 595 | return whenEach( ckeditorComponents, ckeditorComponent => whenEvent( 'ready', ckeditorComponent ) ); 596 | } ); 597 | } ); 598 | 599 | function wait( time ) { 600 | return new Promise( resolve => { 601 | setTimeout( resolve, time ); 602 | } ); 603 | } 604 | 605 | function whenEach( ckeditorComponents, callback ) { 606 | return Promise.all( ckeditorComponents.map( ckeditorComponent => callback( ckeditorComponent ) ) ); 607 | } 608 | -------------------------------------------------------------------------------- /src/ckeditor/ckeditor.component.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 { 7 | Component, 8 | NgZone, 9 | Input, 10 | Output, 11 | EventEmitter, 12 | forwardRef, 13 | ElementRef, 14 | AfterViewInit, OnDestroy 15 | } from '@angular/core'; 16 | 17 | import { 18 | ControlValueAccessor, 19 | NG_VALUE_ACCESSOR 20 | } from '@angular/forms'; 21 | 22 | import { getEditorNamespace } from 'ckeditor4-integrations-common'; 23 | 24 | import { CKEditor4 } from './ckeditor'; 25 | 26 | declare let CKEDITOR: any; 27 | 28 | @Component( { 29 | selector: 'ckeditor', 30 | template: '', 31 | 32 | providers: [ 33 | { 34 | provide: NG_VALUE_ACCESSOR, 35 | useExisting: forwardRef( () => CKEditorComponent ), 36 | multi: true, 37 | } 38 | ] 39 | } ) 40 | export class CKEditorComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { 41 | /** 42 | * The configuration of the editor. 43 | * 44 | * See https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html 45 | * to learn more. 46 | */ 47 | @Input() config?: CKEditor4.Config; 48 | 49 | /** 50 | * CKEditor 4 script url address. Script will be loaded only if CKEDITOR namespace is missing. 51 | * 52 | * Defaults to 'https://cdn.ckeditor.com/4.25.1-lts/standard-all/ckeditor.js' 53 | */ 54 | @Input() editorUrl = 'https://cdn.ckeditor.com/4.25.1-lts/standard-all/ckeditor.js'; 55 | 56 | /** 57 | * Tag name of the editor component. 58 | * 59 | * The default tag is `textarea`. 60 | */ 61 | @Input() tagName = 'textarea'; 62 | 63 | /** 64 | * The type of the editor interface. 65 | * 66 | * By default editor interface will be initialized as `classic` editor. 67 | * You can also choose to create an editor with `inline` interface type instead. 68 | * 69 | * See https://ckeditor.com/docs/ckeditor4/latest/guide/dev_uitypes.html 70 | * and https://ckeditor.com/docs/ckeditor4/latest/examples/fixedui.html 71 | * to learn more. 72 | */ 73 | @Input() type: CKEditor4.EditorType = CKEditor4.EditorType.CLASSIC; 74 | 75 | /** 76 | * Keeps track of the editor's data. 77 | * 78 | * It's also decorated as an input which is useful when not using the ngModel. 79 | * 80 | * See https://angular.io/api/forms/NgModel to learn more. 81 | */ 82 | @Input() set data( data: string ) { 83 | if ( data === this._data ) { 84 | return; 85 | } 86 | 87 | if ( this.instance ) { 88 | this.instance.setData( data ); 89 | // Data may be changed by ACF. 90 | this._data = this.instance.getData(); 91 | return; 92 | } 93 | 94 | this._data = data; 95 | } 96 | 97 | get data(): string { 98 | return this._data; 99 | } 100 | 101 | /** 102 | * When set to `true`, the editor becomes read-only. 103 | * 104 | * See https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#property-readOnly 105 | * to learn more. 106 | */ 107 | @Input() set readOnly( isReadOnly: boolean ) { 108 | if ( this.instance ) { 109 | this.instance.setReadOnly( isReadOnly ); 110 | return; 111 | } 112 | 113 | // Delay setting read-only state until editor initialization. 114 | this._readOnly = isReadOnly; 115 | } 116 | 117 | get readOnly(): boolean { 118 | if ( this.instance ) { 119 | return this.instance.readOnly; 120 | } 121 | 122 | return this._readOnly; 123 | } 124 | 125 | /** 126 | * Fired when the CKEDITOR https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html namespace 127 | * is loaded. It only triggers once, no matter how many CKEditor 4 components are initialised. 128 | * Can be used for convenient changes in the namespace, e.g. for adding external plugins. 129 | */ 130 | @Output() namespaceLoaded = new EventEmitter(); 131 | 132 | /** 133 | * Fires when the editor is ready. It corresponds with the `editor#instanceReady` 134 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-instanceReady 135 | * event. 136 | */ 137 | @Output() ready = new EventEmitter(); 138 | 139 | /** 140 | * Fires when the editor data is loaded, e.g. after calling setData() 141 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#method-setData 142 | * editor's method. It corresponds with the `editor#dataReady` 143 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dataReady event. 144 | */ 145 | @Output() dataReady = new EventEmitter(); 146 | 147 | /** 148 | * Fires when the content of the editor has changed. It corresponds with the `editor#change` 149 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-change 150 | * event. For performance reasons this event may be called even when data didn't really changed. 151 | * Please note that this event will only be fired when `undo` plugin is loaded. If you need to 152 | * listen for editor changes (e.g. for two-way data binding), use `dataChange` event instead. 153 | */ 154 | @Output() change = new EventEmitter(); 155 | 156 | /** 157 | * Fires when the content of the editor has changed. In contrast to `change` - only emits when 158 | * data really changed thus can be successfully used with `[data]` and two way `[(data)]` binding. 159 | * 160 | * See more: https://angular.io/guide/template-syntax#two-way-binding--- 161 | */ 162 | @Output() dataChange = new EventEmitter(); 163 | 164 | /** 165 | * Fires when the native dragStart event occurs. It corresponds with the `editor#dragstart` 166 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragstart 167 | * event. 168 | */ 169 | @Output() dragStart = new EventEmitter(); 170 | 171 | /** 172 | * Fires when the native dragEnd event occurs. It corresponds with the `editor#dragend` 173 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragend 174 | * event. 175 | */ 176 | @Output() dragEnd = new EventEmitter(); 177 | 178 | /** 179 | * Fires when the native drop event occurs. It corresponds with the `editor#drop` 180 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-drop 181 | * event. 182 | */ 183 | @Output() drop = new EventEmitter(); 184 | 185 | /** 186 | * Fires when the file loader response is received. It corresponds with the `editor#fileUploadResponse` 187 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadResponse 188 | * event. 189 | */ 190 | @Output() fileUploadResponse = new EventEmitter(); 191 | 192 | /** 193 | * Fires when the file loader should send XHR. It corresponds with the `editor#fileUploadRequest` 194 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadRequest 195 | * event. 196 | */ 197 | @Output() fileUploadRequest = new EventEmitter(); 198 | 199 | /** 200 | * Fires when the editing area of the editor is focused. It corresponds with the `editor#focus` 201 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-focus 202 | * event. 203 | */ 204 | @Output() focus = new EventEmitter(); 205 | 206 | /** 207 | * Fires after the user initiated a paste action, but before the data is inserted. 208 | * It corresponds with the `editor#paste` 209 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-paste 210 | * event. 211 | */ 212 | @Output() paste = new EventEmitter(); 213 | 214 | /** 215 | * Fires after the `paste` event if content was modified. It corresponds with the `editor#afterPaste` 216 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-afterPaste 217 | * event. 218 | */ 219 | @Output() afterPaste = new EventEmitter(); 220 | 221 | /** 222 | * Fires when the editing view of the editor is blurred. It corresponds with the `editor#blur` 223 | * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-blur 224 | * event. 225 | */ 226 | @Output() blur = new EventEmitter(); 227 | 228 | /** 229 | * A callback executed when the content of the editor changes. Part of the 230 | * `ControlValueAccessor` (https://angular.io/api/forms/ControlValueAccessor) interface. 231 | * 232 | * Note: Unset unless the component uses the `ngModel`. 233 | */ 234 | onChange?: ( data: string ) => void; 235 | 236 | /** 237 | * A callback executed when the editor has been blurred. Part of the 238 | * `ControlValueAccessor` (https://angular.io/api/forms/ControlValueAccessor) interface. 239 | * 240 | * Note: Unset unless the component uses the `ngModel`. 241 | */ 242 | onTouched?: () => void; 243 | 244 | /** 245 | * The instance of the editor created by this component. 246 | */ 247 | instance: any; 248 | 249 | /** 250 | * If the component is read–only before the editor instance is created, it remembers that state, 251 | * so the editor can become read–only once it is ready. 252 | */ 253 | private _readOnly: boolean = null; 254 | 255 | private _data: string = null; 256 | 257 | private _destroyed: boolean = false; 258 | 259 | constructor( private elementRef: ElementRef, private ngZone: NgZone ) {} 260 | 261 | ngAfterViewInit(): void { 262 | getEditorNamespace( this.editorUrl, namespace => { 263 | this.namespaceLoaded.emit( namespace ); 264 | } ).then( () => { 265 | // Check if component instance was destroyed before `ngAfterViewInit` call (#110). 266 | // Here, `this.instance` is still not initialized and so additional flag is needed. 267 | if ( this._destroyed ) { 268 | return; 269 | } 270 | 271 | this.ngZone.runOutsideAngular( this.createEditor.bind( this ) ); 272 | } ).catch( window.console.error ); 273 | } 274 | 275 | ngOnDestroy(): void { 276 | this._destroyed = true; 277 | 278 | this.ngZone.runOutsideAngular( () => { 279 | if ( this.instance ) { 280 | this.instance.destroy(); 281 | this.instance = null; 282 | } 283 | } ); 284 | } 285 | 286 | writeValue( value: string ): void { 287 | this.data = value; 288 | } 289 | 290 | registerOnChange( callback: ( data: string ) => void ): void { 291 | this.onChange = callback; 292 | } 293 | 294 | registerOnTouched( callback: () => void ): void { 295 | this.onTouched = callback; 296 | } 297 | 298 | private createEditor(): void { 299 | const element = document.createElement( this.tagName ); 300 | this.elementRef.nativeElement.appendChild( element ); 301 | 302 | const userInstanceReadyCallback = this.config?.on?.instanceReady; 303 | const defaultConfig: Partial = { 304 | delayIfDetached: true 305 | }; 306 | const config: Partial = { ...defaultConfig, ...this.config }; 307 | 308 | if ( typeof config.on === 'undefined' ) { 309 | config.on = {}; 310 | } 311 | 312 | config.on.instanceReady = evt => { 313 | const editor = evt.editor; 314 | 315 | this.instance = editor; 316 | 317 | // Read only state may change during instance initialization. 318 | this.readOnly = this._readOnly !== null ? this._readOnly : this.instance.readOnly; 319 | 320 | this.subscribe( this.instance ); 321 | 322 | const undo = editor.undoManager; 323 | 324 | if ( this.data !== null ) { 325 | undo && undo.lock(); 326 | 327 | editor.setData( this.data, { callback: () => { 328 | // Locking undoManager prevents 'change' event. 329 | // Trigger it manually to updated bound data. 330 | if ( this.data !== editor.getData() ) { 331 | undo ? editor.fire( 'change' ) : editor.fire( 'dataReady' ); 332 | } 333 | undo && undo.unlock(); 334 | 335 | this.ngZone.run( () => { 336 | if ( typeof userInstanceReadyCallback === 'function' ) { 337 | userInstanceReadyCallback( evt ); 338 | } 339 | 340 | this.ready.emit( evt ); 341 | } ); 342 | } } ); 343 | } else { 344 | this.ngZone.run( () => { 345 | if ( typeof userInstanceReadyCallback === 'function' ) { 346 | userInstanceReadyCallback( evt ); 347 | } 348 | 349 | this.ready.emit( evt ); 350 | } ); 351 | } 352 | } 353 | 354 | if ( this.type === CKEditor4.EditorType.INLINE ) { 355 | CKEDITOR.inline( element, config ); 356 | } else { 357 | CKEDITOR.replace( element, config ); 358 | } 359 | } 360 | 361 | private subscribe( editor: any ): void { 362 | editor.on( 'focus', evt => { 363 | this.ngZone.run( () => { 364 | this.focus.emit( evt ); 365 | } ); 366 | } ); 367 | 368 | editor.on( 'paste', evt => { 369 | this.ngZone.run( () => { 370 | this.paste.emit( evt ); 371 | } ); 372 | } ); 373 | 374 | editor.on( 'afterPaste', evt => { 375 | this.ngZone.run( () => { 376 | this.afterPaste.emit( evt ); 377 | } ); 378 | } ); 379 | 380 | editor.on( 'dragend', evt => { 381 | this.ngZone.run( () => { 382 | this.dragEnd.emit( evt ); 383 | } ); 384 | }); 385 | 386 | editor.on( 'dragstart', evt => { 387 | this.ngZone.run( () => { 388 | this.dragStart.emit( evt ); 389 | } ); 390 | } ); 391 | 392 | editor.on( 'drop', evt => { 393 | this.ngZone.run( () => { 394 | this.drop.emit( evt ); 395 | } ); 396 | } ); 397 | 398 | editor.on( 'fileUploadRequest', evt => { 399 | this.ngZone.run( () => { 400 | this.fileUploadRequest.emit(evt); 401 | } ); 402 | } ); 403 | 404 | editor.on( 'fileUploadResponse', evt => { 405 | this.ngZone.run( () => { 406 | this.fileUploadResponse.emit(evt); 407 | } ); 408 | } ); 409 | 410 | editor.on( 'blur', evt => { 411 | this.ngZone.run( () => { 412 | if ( this.onTouched ) { 413 | this.onTouched(); 414 | } 415 | 416 | this.blur.emit( evt ); 417 | } ); 418 | } ); 419 | 420 | editor.on( 'dataReady', this.propagateChange, this ); 421 | 422 | if ( this.instance.undoManager ) { 423 | editor.on( 'change', this.propagateChange, this ); 424 | } 425 | // If 'undo' plugin is not loaded, listen to 'selectionCheck' event instead. (#54). 426 | else { 427 | editor.on( 'selectionCheck', this.propagateChange, this ); 428 | } 429 | } 430 | 431 | private propagateChange( event: any ): void { 432 | this.ngZone.run( () => { 433 | const newData = this.instance.getData(); 434 | 435 | if ( event.name === 'change' ) { 436 | this.change.emit( event ); 437 | } else if ( event.name === 'dataReady' ) { 438 | this.dataReady.emit( event ); 439 | } 440 | 441 | if ( newData === this.data ) { 442 | return; 443 | } 444 | 445 | this._data = newData; 446 | this.dataChange.emit( newData ); 447 | 448 | if ( this.onChange ) { 449 | this.onChange( newData ); 450 | } 451 | } ); 452 | } 453 | 454 | } 455 | -------------------------------------------------------------------------------- /src/ckeditor/ckeditor.module.spec.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 { CKEditorModule } from './ckeditor.module'; 7 | 8 | describe( 'CKEditorModule', () => { 9 | let ckeditorModule: CKEditorModule; 10 | 11 | beforeEach( () => { 12 | ckeditorModule = new CKEditorModule(); 13 | } ); 14 | 15 | it( 'should create an instance', () => { 16 | expect( ckeditorModule ).toBeTruthy(); 17 | } ); 18 | } ); 19 | -------------------------------------------------------------------------------- /src/ckeditor/ckeditor.module.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 { NgModule } from '@angular/core'; 7 | import { CommonModule } from '@angular/common'; 8 | import { FormsModule } from '@angular/forms'; 9 | import { CKEditorComponent } from './ckeditor.component'; 10 | 11 | @NgModule( { 12 | imports: [ FormsModule, CommonModule ], 13 | declarations: [ CKEditorComponent ], 14 | exports: [ CKEditorComponent ] 15 | } ) 16 | export class CKEditorModule { 17 | } 18 | export * from './ckeditor'; 19 | export { CKEditorComponent } from './ckeditor.component'; 20 | -------------------------------------------------------------------------------- /src/ckeditor/ckeditor.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 | * Basic typings for the CKEditor4 elements. 8 | */ 9 | export namespace CKEditor4 { 10 | /** 11 | * The CKEditor4 editor constructor. 12 | */ 13 | export interface Config { 14 | [ key: string ]: any; 15 | } 16 | 17 | /** 18 | * The CKEditor4 editor. 19 | */ 20 | export interface Editor { 21 | [ key: string ]: any; 22 | } 23 | 24 | /** 25 | * The CKEditor4 editor interface type. 26 | * See https://ckeditor.com/docs/ckeditor4/latest/guide/dev_uitypes.html 27 | * to learn more. 28 | */ 29 | export const enum EditorType { 30 | INLINE = 'inline', 31 | CLASSIC = 'classic' 32 | } 33 | 34 | /** 35 | * The event object passed to CKEditor4 event callbacks. 36 | * 37 | * See https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_eventInfo.html 38 | * to learn more. 39 | */ 40 | export interface EventInfo { 41 | readonly name: string; 42 | readonly editor: any; 43 | readonly data: { 44 | [ key: string ]: any; 45 | }; 46 | readonly listenerData: { 47 | [ key: string ]: any; 48 | }; 49 | readonly sender: { 50 | [ key: string ]: any; 51 | }; 52 | 53 | cancel(): void; 54 | 55 | removeListener(): void; 56 | 57 | stop(): void; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ckeditor/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "lib": { 3 | "entryFile": "./ckeditor.module.ts" 4 | }, 5 | "allowedNonPeerDependencies": ["ckeditor4-integrations-common"], 6 | "dest": "../../dist" 7 | } 8 | -------------------------------------------------------------------------------- /src/ckeditor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ckeditor4-angular", 3 | "version": "1.0.0", 4 | "description": "Official CKEditor 4 component for Angular.", 5 | "keywords": [ 6 | "wysiwyg", 7 | "rich text editor", 8 | "rte", 9 | "editor", 10 | "html", 11 | "contentEditable", 12 | "editing", 13 | "angular", 14 | "angular5", 15 | "ng", 16 | "component", 17 | "ckeditor", 18 | "ckeditor4", 19 | "ckeditor 4" 20 | ], 21 | "peerDependencies": { 22 | "@angular/core": ">=13.4.0", 23 | "@angular/common": ">=13.4.0", 24 | "@angular/forms": ">=13.4.0" 25 | }, 26 | "author": "CKSource (https://cksource.com/)", 27 | "license": "SEE LICENSE IN LICENSE.md", 28 | "homepage": "https://ckeditor.com/", 29 | "bugs": "https://github.com/ckeditor/ckeditor4-angular/issues", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/ckeditor/ckeditor4-angular.git" 33 | }, 34 | "dependencies": { 35 | "ckeditor4-integrations-common": "^1.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ckeditor/ckeditor4-angular/b9c388579934798a2407ebf319a7648b65025ad0/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CKEditor 4 component for Angular 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | let options = process.env.KARMA_OPTIONS; 5 | options = options ? JSON.parse( options ) : {}; 6 | 7 | module.exports = function ( config ) { 8 | config.set( { 9 | basePath: '', 10 | frameworks: [ 'jasmine', '@angular-devkit/build-angular' ], 11 | plugins: getPlugins(), 12 | client: { 13 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 14 | captureConsole: false, 15 | jasmine: { 16 | random: false 17 | }, 18 | args: [ 19 | process.env.CKEDITOR_LICENSE_KEY 20 | ] 21 | }, 22 | coverageIstanbulReporter: { 23 | dir: require( 'path' ).join( __dirname, '../coverage' ), 24 | reports: [ 'html', 'lcovonly' ], 25 | fixWebpackSourcePaths: true 26 | }, 27 | reporters: [ 'spec', 'kjhtml' ], 28 | port: 9876, 29 | colors: true, 30 | logLevel: config.LOG_INFO, 31 | autoWatch: true, 32 | browsers: getBrowsers(), 33 | singleRun: !options.watch, 34 | 35 | concurrency: 2, 36 | captureTimeout: 60000, 37 | browserDisconnectTimeout: 60000, 38 | browserDisconnectTolerance: 3, 39 | browserNoActivityTimeout: 60000, 40 | 41 | specReporter: { 42 | suppressPassed: shouldEnableBrowserStack() 43 | }, 44 | 45 | ...( options.url && { files: [ options.url ] } ), 46 | 47 | customLaunchers: { 48 | BrowserStack_Chrome: { 49 | base: 'BrowserStack', 50 | os: 'Windows', 51 | os_version: '11', 52 | browser: 'chrome' 53 | }, 54 | BrowserStack_Edge: { 55 | base: 'BrowserStack', 56 | os: 'Windows', 57 | os_version: '10', 58 | browser: 'edge' 59 | }, 60 | BrowserStack_Safari: { 61 | base: 'BrowserStack', 62 | os: 'OS X', 63 | os_version: 'Big Sur', 64 | browser: 'safari' 65 | } 66 | }, 67 | 68 | browserStack: { 69 | username: process.env.BROWSER_STACK_USERNAME, 70 | accessKey: process.env.BROWSER_STACK_ACCESS_KEY, 71 | build: getBuildName(), 72 | project: 'ckeditor4' 73 | }, 74 | } ); 75 | }; 76 | 77 | // Formats name of the build for BrowserStack. It merges a repository name and current timestamp. 78 | // If env variable `CIRCLE_PROJECT_REPONAME` is not available, the function returns `undefined`. 79 | // 80 | // @returns {String|undefined} 81 | function getBuildName() { 82 | const repoName = process.env.CIRCLE_PROJECT_REPONAME; 83 | 84 | if ( !repoName ) { 85 | return; 86 | } 87 | 88 | const repositoryName = repoName.replace( /-/g, '_' ); 89 | const date = new Date().getTime(); 90 | 91 | return `${ repositoryName } ${ date }`; 92 | } 93 | 94 | function getPlugins() { 95 | const plugins = [ 96 | require( 'karma-jasmine' ), 97 | require( 'karma-chrome-launcher' ), 98 | require( 'karma-firefox-launcher' ), 99 | require( 'karma-jasmine-html-reporter' ), 100 | require( 'karma-coverage-istanbul-reporter' ), 101 | require( '@angular-devkit/build-angular/plugins/karma' ), 102 | require( 'karma-spec-reporter' ) 103 | ]; 104 | 105 | if ( shouldEnableBrowserStack() ) { 106 | plugins.push( 107 | require( 'karma-browserstack-launcher' ) 108 | ); 109 | } 110 | 111 | return plugins; 112 | } 113 | 114 | function getBrowsers() { 115 | if ( shouldEnableBrowserStack() ) { 116 | return [ 117 | 'BrowserStack_Chrome', 118 | 'BrowserStack_Safari', 119 | 'Firefox', 120 | 'BrowserStack_Edge' 121 | ]; 122 | } 123 | 124 | return [ 125 | 'Chrome' 126 | // 'Firefox' 127 | ]; 128 | } 129 | 130 | function shouldEnableBrowserStack() { 131 | if ( !process.env.BROWSER_STACK_USERNAME ) { 132 | return false; 133 | } 134 | 135 | if ( !process.env.BROWSER_STACK_ACCESS_KEY ) { 136 | return false; 137 | } 138 | 139 | // If the CIRCLE_PR_REPONAME variable is set, it indicates that the PR comes from the forked repo. 140 | // For such builds, BrowserStack will be disabled. Read more: https://github.com/ckeditor/ckeditor5-dev/issues/358. 141 | return !( 'CIRCLE_PR_REPONAME' in process.env ); 142 | } 143 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if ( environment.production ) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule( AppModule ) 12 | .catch( err => console.error( err ) ); 13 | 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /** 5 | * Web Animations `@angular/platform-browser/animations` 6 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 7 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 8 | **/ 9 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 10 | 11 | /** 12 | * By default, zone.js will patch all possible macroTask and DomEvents 13 | * user can disable parts of macroTask/DomEvents patch by setting following flags 14 | */ 15 | 16 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 17 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 18 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 19 | 20 | /** 21 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 22 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 23 | */ 24 | // (window as any).__Zone_enable_cross_context_check = true; 25 | 26 | /*************************************************************************************************** 27 | * Browser Polyfills 28 | * IE11 needs those in general. Currently we use only some of them, but they may come in handy later. 29 | */ 30 | 31 | import 'core-js/es/symbol'; 32 | import 'core-js/es/object'; 33 | import 'core-js/es/function'; 34 | import 'core-js/es/parse-int'; 35 | import 'core-js/es/parse-float'; 36 | import 'core-js/es/number'; 37 | import 'core-js/es/math'; 38 | import 'core-js/es/string'; 39 | import 'core-js/es/date'; 40 | import 'core-js/es/array'; 41 | import 'core-js/es/regexp'; 42 | import 'core-js/es/map'; 43 | 44 | /*************************************************************************************************** 45 | * Zone JS is required by default for Angular itself. 46 | */ 47 | import 'zone.js/dist/zone'; // Included with Angular CLI. 48 | 49 | /*************************************************************************************************** 50 | * APPLICATION IMPORTS 51 | */ 52 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.tools.ts: -------------------------------------------------------------------------------- 1 | import { CKEditorComponent } from './ckeditor/ckeditor.component'; 2 | 3 | declare var CKEDITOR: any; 4 | 5 | export function whenEvent( evtName: string, component: CKEditorComponent ) { 6 | return new Promise( res => { 7 | component[ evtName ].subscribe( res ); 8 | } ); 9 | } 10 | 11 | export function whenDataReady( editor: any, callback?: Function ) { 12 | return new Promise( res => { 13 | editor.once( 'dataReady', () => { 14 | res(); 15 | }, null, null, 9999 ); 16 | 17 | callback && callback(); 18 | } ); 19 | } 20 | 21 | export function setDataMultipleTimes( editor: any, data: Array ) { 22 | return new Promise( res => { 23 | if ( !editor.editable().isInline() ) { 24 | // Due to setData() issue with iframe based editor, subsequent setData() calls 25 | // should be performed asynchronously (https://github.com/ckeditor/ckeditor4/issues/3669). 26 | setDataHelper( editor, data, res ); 27 | } else { 28 | data.forEach( content => editor.setData( content ) ); 29 | res(); 30 | } 31 | } ); 32 | } 33 | 34 | export function mockDropEvent() { 35 | const dataTransfer = mockNativeDataTransfer(); 36 | let target = new CKEDITOR.dom.element( 'div' ); 37 | 38 | target.isReadOnly = function() { 39 | return false; 40 | }; 41 | 42 | return { 43 | $: { 44 | dataTransfer: dataTransfer 45 | }, 46 | preventDefault: function() {}, 47 | getTarget: function() { 48 | return target; 49 | }, 50 | setTarget: function( t: any ) { 51 | target = t; 52 | } 53 | }; 54 | } 55 | 56 | export function fireDragEvent( eventName: string, editor: any, evt: any ) { 57 | const dropTarget = CKEDITOR.plugins.clipboard.getDropTarget( editor ); 58 | 59 | dropTarget.fire( eventName, evt ); 60 | } 61 | 62 | function setDataHelper( editor: any, data: Array, done: Function ) { 63 | if ( data.length ) { 64 | const content: string = data.shift(); 65 | 66 | setTimeout( () => { 67 | editor.setData( content ); 68 | setDataHelper( editor, data, done ); 69 | }, 100 ); 70 | } else { 71 | setTimeout( done, 100 ); 72 | } 73 | } 74 | 75 | function mockNativeDataTransfer() { 76 | return { 77 | types: [], 78 | files: [], 79 | _data: {}, 80 | setData: function( type, data ) { 81 | if ( type == 'text/plain' || type == 'Text' ) { 82 | this._data[ 'text/plain' ] = data; 83 | this._data.Text = data; 84 | } else { 85 | this._data[ type ] = data; 86 | } 87 | 88 | this.types.push( type ); 89 | }, 90 | getData: function( type ) { 91 | return this._data[ type ]; 92 | }, 93 | clearData: function( type ) { 94 | const index = CKEDITOR.tools.indexOf( this.types, type ); 95 | 96 | if ( index !== -1 ) { 97 | delete this._data[ type ]; 98 | this.types.splice( index, 1 ); 99 | } 100 | } 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context( './', true, /\.spec\.ts$/ ); 19 | // And load the modules. 20 | context.keys().map( context ); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs", 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": [ 26 | true, 27 | "tabs" 28 | ], 29 | "interface-over-type-literal": true, 30 | "label-position": true, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-access": false, 36 | "member-ordering": [ 37 | true, 38 | { 39 | "order": [ 40 | "static-field", 41 | "instance-field", 42 | "static-method", 43 | "instance-method" 44 | ] 45 | } 46 | ], 47 | "no-arg": true, 48 | "no-bitwise": true, 49 | "no-console": [ 50 | true, 51 | "debug", 52 | "info", 53 | "time", 54 | "timeEnd", 55 | "trace" 56 | ], 57 | "no-construct": true, 58 | "no-debugger": true, 59 | "no-duplicate-super": true, 60 | "no-empty": false, 61 | "no-empty-interface": true, 62 | "no-eval": true, 63 | "no-inferrable-types": [ 64 | true, 65 | "ignore-params" 66 | ], 67 | "no-misused-new": true, 68 | "no-non-null-assertion": false, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-unused-variable": true, 77 | "no-use-before-declare": true, 78 | "no-var-keyword": true, 79 | "object-literal-sort-keys": false, 80 | "one-line": [ 81 | true, 82 | "check-open-brace", 83 | "check-catch", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "importHelpers": true, 6 | "outDir": "./samples/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "es2020", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2017", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. 3 | It is not intended to be used to perform a compilation. 4 | 5 | To learn more about this file see: https://angular.io/config/solution-tsconfig. 6 | */ 7 | { 8 | "files": [], 9 | "references": [ 10 | { 11 | "path": "./src/tsconfig.app.json" 12 | }, 13 | { 14 | "path": "./src/tsconfig.spec.json" 15 | }, 16 | { 17 | "path": "./e2e/tsconfig.e2e.json" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "tabs" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-redundant-jsdoc": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-var-keyword": true, 75 | "object-literal-sort-keys": false, 76 | "one-line": [ 77 | true, 78 | "check-open-brace", 79 | "check-catch", 80 | "check-else", 81 | "check-whitespace" 82 | ], 83 | "prefer-const": true, 84 | "quotemark": [ 85 | true, 86 | "single" 87 | ], 88 | "radix": true, 89 | "semicolon": [ 90 | true, 91 | "always" 92 | ], 93 | "triple-equals": [ 94 | true, 95 | "allow-null-check" 96 | ], 97 | "typedef-whitespace": [ 98 | true, 99 | { 100 | "call-signature": "nospace", 101 | "index-signature": "nospace", 102 | "parameter": "nospace", 103 | "property-declaration": "nospace", 104 | "variable-declaration": "nospace" 105 | } 106 | ], 107 | "unified-signatures": true, 108 | "variable-name": false, 109 | "whitespace": [ 110 | true, 111 | "check-branch", 112 | "check-decl", 113 | "check-operator", 114 | "check-separator", 115 | "check-type" 116 | ], 117 | "no-output-on-prefix": true, 118 | "use-input-property-decorator": true, 119 | "use-output-property-decorator": true, 120 | "use-host-property-decorator": true, 121 | "no-input-rename": true, 122 | "no-output-rename": true, 123 | "use-life-cycle-interface": true, 124 | "use-pipe-transform-interface": true, 125 | "component-class-suffix": true, 126 | "directive-class-suffix": true 127 | } 128 | } 129 | --------------------------------------------------------------------------------