├── .gitignore ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | private/ 3 | img/ 4 | js/ 5 | node_modules/ 6 | .eslintrc 7 | *.log 8 | _index.html 9 | dist/ 10 | .npmrc 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-current, Artur Arseniev 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | - Neither the name "GrapesJS" nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrapesJS CKEditor 2 | 3 | This plugin replaces the default Rich Text Editor with the CKEditor 4 | 5 | [Demo](https://grapesjs.com/demo-newsletter-editor.html) 6 | 7 |

GrapesJS

8 |
9 | 10 | ## Summary 11 | 12 | * Plugin name: `grapesjs-plugin-ckeditor` 13 | 14 | 15 | 16 | 17 | ## Options 18 | 19 | |Option|Description|Default| 20 | |-|-|- 21 | |`options`|CKEditor's configuration [object](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html), eg. `{ language: 'en', toolbar: [...], ...}`|`{}`| 22 | |`position`|Position side of the toolbar, options: `left, center, right`|`left`| 23 | |`ckeditor`|Pass CKEDITOR constructor or the CDN string from which the CKEDITOR will be loaded.|`https://cdn.ckeditor.com/4.21.0/standard-all/ckeditor.js`| 24 | |`customRte`|Extend the default [customRTE interface](https://grapesjs.com/docs/guides/Replace-Rich-Text-Editor.html).|`{}`| 25 | |`onToolbar`|Customize CKEditor toolbar element once created, eg. `onToolbar: (el) => { el.style.minWidth = '350px' }`|``| 26 | 27 | 28 | 29 | 30 | ## Download 31 | 32 | * CDN 33 | * `https://unpkg.com/grapesjs-plugin-ckeditor` 34 | * NPM 35 | * `npm i grapesjs-plugin-ckeditor` 36 | * GIT 37 | * `git clone https://github.com/GrapesJS/ckeditor.git` 38 | 39 | 40 | 41 | 42 | ## Usage 43 | 44 | ```html 45 | 46 | 47 | 48 | 49 |
50 | 51 | 60 | ``` 61 | 62 | 63 | 64 | ## Development 65 | 66 | Clone the repository 67 | 68 | ```sh 69 | $ git clone https://github.com/GrapesJS/ckeditor.git 70 | $ cd ckeditor 71 | ``` 72 | 73 | Install dependencies 74 | 75 | ```sh 76 | $ npm i 77 | ``` 78 | 79 | Start the dev server 80 | 81 | ```sh 82 | $ npm start 83 | ``` 84 | 85 | 86 | 87 | ## License 88 | 89 | BSD 3-Clause 90 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GrapesJS CKEditor 6 | 7 | 8 | 9 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |

Title Heading

26 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 27 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor 28 | in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, 29 | sunt in culpa qui officia deserunt mollit anim id est laborum.

30 |
31 |
32 |

Title Heading 2

33 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 34 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor 35 | in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, 36 | sunt in culpa qui officia deserunt mollit anim id est laborum.

37 |
38 |
39 |

Title Heading 3

40 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 41 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor 42 | in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, 43 | sunt in culpa qui officia deserunt mollit anim id est laborum.

44 |
45 |
46 | 72 |
73 | 74 | 75 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjs-plugin-ckeditor", 3 | "version": "1.0.1", 4 | "description": "Replace the built-in RTE with CKEditor", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/" 8 | ], 9 | "scripts": { 10 | "build": "grapesjs-cli build", 11 | "start": "grapesjs-cli serve" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/GrapesJS/ckeditor" 16 | }, 17 | "keywords": [ 18 | "grapesjs", 19 | "plugin", 20 | "ckeditor", 21 | "wysiwyg" 22 | ], 23 | "author": "Artur Arseniev", 24 | "license": "BSD-3-Clause", 25 | "dependencies": { 26 | "@types/ckeditor4": "^4.20.0" 27 | }, 28 | "devDependencies": { 29 | "grapesjs": "^0.21.2", 30 | "grapesjs-cli": "^4.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, CustomRTE } from 'grapesjs'; 2 | import type CKE from 'ckeditor4'; 3 | 4 | export type PluginOptions = { 5 | /** 6 | * CKEditor's configuration options. 7 | * @see https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_config.html 8 | * @default {} 9 | */ 10 | options?: CKE.config; 11 | 12 | /** 13 | * Pass CKEDITOR constructor or the CDN string from which the CKEDITOR will be loaded. 14 | * If this option is empty, the plugin will also check the global scope (eg. window.CKEDITOR) 15 | * @default 'https://cdn.ckeditor.com/4.21.0/standard-all/ckeditor.js' 16 | */ 17 | ckeditor?: CKE.CKEditorStatic | string; 18 | 19 | /** 20 | * Position side of the toolbar. 21 | * @default 'left' 22 | */ 23 | position?: 'left' | 'center' | 'right'; 24 | 25 | /** 26 | * Extend the default customRTE interface. 27 | * @see https://grapesjs.com/docs/guides/Replace-Rich-Text-Editor.html 28 | * @default {} 29 | * @example 30 | * customRte: { parseContent: true, ... }, 31 | */ 32 | customRte?: Partial; 33 | 34 | /** 35 | * Customize CKEditor toolbar element once created. 36 | * @example 37 | * onToolbar: (el) => { 38 | * el.style.minWidth = '350px'; 39 | * } 40 | */ 41 | onToolbar?: (toolbar: HTMLElement) => void; 42 | }; 43 | 44 | const isString = (value: any): value is string => typeof value === 'string'; 45 | 46 | const loadFromCDN = (url: string) => { 47 | const scr = document.createElement('script'); 48 | scr.src = url; 49 | document.head.appendChild(scr); 50 | return scr; 51 | } 52 | 53 | const forEach = (items: Iterable, clb: (item: T) => void) => { 54 | [].forEach.call(items, clb); 55 | } 56 | 57 | const stopPropagation = (ev: Event) => ev.stopPropagation(); 58 | 59 | const plugin: Plugin = (editor, options = {}) => { 60 | const opts: Required = { 61 | options: {}, 62 | customRte: {}, 63 | position: 'left', 64 | ckeditor: 'https://cdn.ckeditor.com/4.21.0/standard-all/ckeditor.js', 65 | onToolbar: () => {}, 66 | ...options, 67 | }; 68 | 69 | let ck: CKE.CKEditorStatic | undefined; 70 | const { ckeditor } = opts; 71 | const hasWindow = typeof window !== 'undefined'; 72 | let dynamicLoad = false; 73 | 74 | // Check and load CKEDITOR constructor 75 | if (ckeditor) { 76 | if (isString(ckeditor)) { 77 | if (hasWindow) { 78 | dynamicLoad = true; 79 | const scriptEl = loadFromCDN(ckeditor); 80 | scriptEl.onload = () => { 81 | ck = window.CKEDITOR; 82 | } 83 | } 84 | } else if (ckeditor.inline!) { 85 | ck = ckeditor; 86 | } 87 | } else if (hasWindow) { 88 | ck = window.CKEDITOR; 89 | } 90 | 91 | const updateEditorToolbars = () => setTimeout(() => editor.refresh(), 0); 92 | const logCkError = () => { 93 | editor.log('CKEDITOR instance not found', { level: 'error' }) 94 | }; 95 | 96 | if (!ck && !dynamicLoad) { 97 | return logCkError(); 98 | } 99 | 100 | const focus = (el: HTMLElement, rte?: CKE.editor) => { 101 | if (rte?.focusManager?.hasFocus) return; 102 | el.contentEditable = 'true'; 103 | rte?.focus(); 104 | updateEditorToolbars(); 105 | }; 106 | 107 | 108 | editor.setCustomRte({ 109 | getContent(el, rte: CKE.editor) { 110 | return rte.getData(); 111 | }, 112 | 113 | enable(el, rte?: CKE.editor) { 114 | // If already exists I'll just focus on it 115 | if(rte && rte.status != 'destroyed') { 116 | focus(el, rte); 117 | return rte; 118 | } 119 | 120 | if (!ck) { 121 | logCkError(); 122 | return; 123 | } 124 | 125 | // Seems like 'sharedspace' plugin doesn't work exactly as expected 126 | // so will help hiding other toolbars already created 127 | const rteToolbar = editor.RichTextEditor.getToolbarEl(); 128 | forEach(rteToolbar.children as Iterable, (child) => { 129 | child.style.display = 'none'; 130 | }); 131 | 132 | // Check for the mandatory options 133 | const ckOptions = { ...opts.options }; 134 | const plgName = 'sharedspace'; 135 | 136 | if (ckOptions.extraPlugins) { 137 | if (typeof ckOptions.extraPlugins === 'string') { 138 | ckOptions.extraPlugins += `,${plgName}`; 139 | } else if (Array.isArray(ckOptions.extraPlugins)) { 140 | (ckOptions.extraPlugins as string[]).push(plgName); 141 | } 142 | } else { 143 | ckOptions.extraPlugins = plgName; 144 | } 145 | 146 | if(!ckOptions.sharedSpaces) { 147 | ckOptions.sharedSpaces = { top: rteToolbar }; 148 | } 149 | 150 | // Init CKEDITOR 151 | rte = ck!.inline(el, ckOptions); 152 | 153 | // Make click event propogate 154 | rte.on('contentDom', () => { 155 | const editable = rte!.editable(); 156 | editable.attachListener(editable, 'click', () => el.click()); 157 | }); 158 | 159 | // The toolbar is not immediatly loaded so will be wrong positioned. 160 | // With this trick we trigger an event which updates the toolbar position 161 | rte.on('instanceReady', () => { 162 | const toolbar = rteToolbar.querySelector(`#cke_${rte!.name}`); 163 | if (toolbar) { 164 | toolbar.style.display = 'block'; 165 | opts.onToolbar(toolbar); 166 | } 167 | // Update toolbar position 168 | editor.refresh(); 169 | // Update the position again as the toolbar dimension might have a new changed 170 | updateEditorToolbars(); 171 | }); 172 | 173 | // Prevent blur when some of CKEditor's element is clicked 174 | rte.on('dialogShow', () => { 175 | const els = document.querySelectorAll('.cke_dialog_background_cover, .cke_dialog_container'); 176 | forEach(els, (child) => { 177 | child.removeEventListener('mousedown', stopPropagation); 178 | child.addEventListener('mousedown', stopPropagation); 179 | }); 180 | }); 181 | 182 | // On ENTER CKEditor doesn't trigger `input` event 183 | rte.on('key', (ev: any) => { 184 | ev.data.keyCode === 13 && updateEditorToolbars(); 185 | }); 186 | 187 | focus(el, rte); 188 | 189 | return rte; 190 | }, 191 | 192 | disable(el, rte?: CKE.editor) { 193 | el.contentEditable = 'false'; 194 | rte?.focusManager?.blur(true); 195 | }, 196 | 197 | ...opts.customRte, 198 | }); 199 | 200 | // Update RTE toolbar position 201 | editor.on('rteToolbarPosUpdate', (pos: any) => { 202 | const { elRect } = pos; 203 | 204 | switch (opts.position) { 205 | case 'center': 206 | pos.left = (elRect.width / 2) - (pos.targetWidth / 2); 207 | break; 208 | case 'right': 209 | pos.left = '' 210 | pos.right = 0; 211 | break; 212 | } 213 | }); 214 | }; 215 | 216 | export default plugin; 217 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/grapesjs-cli/dist/template/tsconfig.json", 3 | "include": ["src"] 4 | } --------------------------------------------------------------------------------